RecyclerView SnapHelper fails to show first/last items

前端 未结 4 1963
再見小時候
再見小時候 2021-02-08 19:10

I have a RecyclerView which is attached to a LinearSnapHelper to snap to center item. When I scroll to the first or last items, these items are not ful

相关标签:
4条回答
  • 2021-02-08 19:28

    This issue happens when center of item which is next to the first/last is closer to the center of container. So, we should make some changes on snapping functionality to ignore this case. Since we need some fields in LinearSnapHelper class, we can copy its source code and make change on findCenterView method as following:

    MyLinearSnapHelper.kt

    /*
     * Copyright (C) 2016 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package com.aminography.view.component
    
    import android.support.v7.widget.LinearLayoutManager
    import android.support.v7.widget.OrientationHelper
    import android.support.v7.widget.RecyclerView
    import android.support.v7.widget.SnapHelper
    import android.view.View
    
    /**
     * Implementation of the [SnapHelper] supporting snapping in either vertical or horizontal
     * orientation.
     *
     *
     * The implementation will snap the center of the target child view to the center of
     * the attached [RecyclerView]. If you intend to change this behavior then override
     * [SnapHelper.calculateDistanceToFinalSnap].
     */
    class MyLinearSnapHelper : SnapHelper() {
        // Orientation helpers are lazily created per LayoutManager.
        private var mVerticalHelper: OrientationHelper? = null
        private var mHorizontalHelper: OrientationHelper? = null
        override fun calculateDistanceToFinalSnap(
                layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
            val out = IntArray(2)
            if (layoutManager.canScrollHorizontally()) {
                out[0] = distanceToCenter(layoutManager, targetView,
                        getHorizontalHelper(layoutManager))
            } else {
                out[0] = 0
            }
            if (layoutManager.canScrollVertically()) {
                out[1] = distanceToCenter(layoutManager, targetView,
                        getVerticalHelper(layoutManager))
            } else {
                out[1] = 0
            }
            return out
        }
    
        override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int,
                                            velocityY: Int): Int {
            if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
                return RecyclerView.NO_POSITION
            }
            val itemCount = layoutManager.itemCount
            if (itemCount == 0) {
                return RecyclerView.NO_POSITION
            }
            val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
            val currentPosition = layoutManager.getPosition(currentView)
            if (currentPosition == RecyclerView.NO_POSITION) {
                return RecyclerView.NO_POSITION
            }
            val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
            // deltaJumps sign comes from the velocity which may not match the order of children in
            // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
            // get the direction.
            val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1)
                    ?: // cannot get a vector for the given position.
                    return RecyclerView.NO_POSITION
            var vDeltaJump: Int
            var hDeltaJump: Int
            if (layoutManager.canScrollHorizontally()) {
                hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                        getHorizontalHelper(layoutManager), velocityX, 0)
                if (vectorForEnd.x < 0) {
                    hDeltaJump = -hDeltaJump
                }
            } else {
                hDeltaJump = 0
            }
            if (layoutManager.canScrollVertically()) {
                vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                        getVerticalHelper(layoutManager), 0, velocityY)
                if (vectorForEnd.y < 0) {
                    vDeltaJump = -vDeltaJump
                }
            } else {
                vDeltaJump = 0
            }
            val deltaJump = if (layoutManager.canScrollVertically()) vDeltaJump else hDeltaJump
            if (deltaJump == 0) {
                return RecyclerView.NO_POSITION
            }
            var targetPos = currentPosition + deltaJump
            if (targetPos < 0) {
                targetPos = 0
            }
            if (targetPos >= itemCount) {
                targetPos = itemCount - 1
            }
            return targetPos
        }
    
        override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
            if (layoutManager.canScrollVertically()) {
                return findCenterView(layoutManager, getVerticalHelper(layoutManager))
            } else if (layoutManager.canScrollHorizontally()) {
                return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
            }
            return null
        }
    
        private fun distanceToCenter(layoutManager: RecyclerView.LayoutManager,
                                     targetView: View, helper: OrientationHelper): Int {
            val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
            val containerCenter: Int = if (layoutManager.clipToPadding) {
                helper.startAfterPadding + helper.totalSpace / 2
            } else {
                helper.end / 2
            }
            return childCenter - containerCenter
        }
    
        /**
         * Estimates a position to which SnapHelper will try to scroll to in response to a fling.
         *
         * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
         * [RecyclerView].
         * @param helper        The [OrientationHelper] that is created from the LayoutManager.
         * @param velocityX     The velocity on the x axis.
         * @param velocityY     The velocity on the y axis.
         *
         * @return The diff between the target scroll position and the current position.
         */
        private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager,
                                                     helper: OrientationHelper, velocityX: Int, velocityY: Int): Int {
            val distances = calculateScrollDistance(velocityX, velocityY)
            val distancePerChild = computeDistancePerChild(layoutManager, helper)
            if (distancePerChild <= 0) {
                return 0
            }
            val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
            return Math.round(distance / distancePerChild)
        }
    
        /**
         * Return the child view that is currently closest to the center of this parent.
         *
         * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
         * [RecyclerView].
         * @param helper The relevant [OrientationHelper] for the attached [RecyclerView].
         *
         * @return the child view that is currently closest to the center of this parent.
         */
        private fun findCenterView(layoutManager: RecyclerView.LayoutManager,
                                   helper: OrientationHelper): View? {
            // ----- Added by aminography
            if (layoutManager is LinearLayoutManager) {
                if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
                    return layoutManager.getChildAt(0)
                } else if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) {
                    return layoutManager.getChildAt(layoutManager.itemCount - 1)
                }
            }
            // -----
    
            val childCount = layoutManager.childCount
            if (childCount == 0) {
                return null
            }
            var closestChild: View? = null
            val center: Int = if (layoutManager.clipToPadding) {
                helper.startAfterPadding + helper.totalSpace / 2
            } else {
                helper.end / 2
            }
            var absClosest = Integer.MAX_VALUE
            for (i in 0 until childCount) {
                val child = layoutManager.getChildAt(i)
                val childCenter = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2
                val absDistance = Math.abs(childCenter - center)
                /** if child center is closer than previous closest, set it as closest   */
                if (absDistance < absClosest) {
                    absClosest = absDistance
                    closestChild = child
                }
            }
            return closestChild
        }
    
        /**
         * Computes an average pixel value to pass a single child.
         *
         *
         * Returns a negative value if it cannot be calculated.
         *
         * @param layoutManager The [RecyclerView.LayoutManager] associated with the attached
         * [RecyclerView].
         * @param helper        The relevant [OrientationHelper] for the attached
         * [RecyclerView.LayoutManager].
         *
         * @return A float value that is the average number of pixels needed to scroll by one view in
         * the relevant direction.
         */
        private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager,
                                            helper: OrientationHelper): Float {
            var minPosView: View? = null
            var maxPosView: View? = null
            var minPos = Integer.MAX_VALUE
            var maxPos = Integer.MIN_VALUE
            val childCount = layoutManager.childCount
            if (childCount == 0) {
                return INVALID_DISTANCE
            }
            for (i in 0 until childCount) {
                val child = layoutManager.getChildAt(i)
                val pos = layoutManager.getPosition(child!!)
                if (pos == RecyclerView.NO_POSITION) {
                    continue
                }
                if (pos < minPos) {
                    minPos = pos
                    minPosView = child
                }
                if (pos > maxPos) {
                    maxPos = pos
                    maxPosView = child
                }
            }
            if (minPosView == null || maxPosView == null) {
                return INVALID_DISTANCE
            }
            val start = Math.min(helper.getDecoratedStart(minPosView),
                    helper.getDecoratedStart(maxPosView))
            val end = Math.max(helper.getDecoratedEnd(minPosView),
                    helper.getDecoratedEnd(maxPosView))
            val distance = end - start
            return if (distance == 0) {
                INVALID_DISTANCE
            } else 1f * distance / (maxPos - minPos + 1)
        }
    
        private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
            if (mVerticalHelper == null || mVerticalHelper!!.layoutManager !== layoutManager) {
                mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
            }
            return mVerticalHelper!!
        }
    
        private fun getHorizontalHelper(
                layoutManager: RecyclerView.LayoutManager): OrientationHelper {
            if (mHorizontalHelper == null || mHorizontalHelper!!.layoutManager !== layoutManager) {
                mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
            }
            return mHorizontalHelper!!
        }
    
        companion object {
            private const val INVALID_DISTANCE = 1f
        }
    
    }
    
    0 讨论(0)
  • 2021-02-08 19:38

    I tried to implement a simple solution. Basically I checked if the first/last items are completely visible. If so, we don't need to perform the snap. See the solution below:

    class CarouselSnapHelper : LinearSnapHelper() {
    
        override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
            val linearLayoutManager = layoutManager as? LinearLayoutManager
                ?: return super.findSnapView(layoutManager)
    
            return linearLayoutManager
                .takeIf { isValidSnap(it) }
                ?.run { super.findSnapView(layoutManager) }
        }
    
        private fun isValidSnap(linearLayoutManager: LinearLayoutManager) =
            linearLayoutManager.findFirstCompletelyVisibleItemPosition() != 0 &&
                linearLayoutManager.findLastCompletelyVisibleItemPosition() != linearLayoutManager.itemCount - 1
    }
    
    0 讨论(0)
  • 2021-02-08 19:43

    I know I am late but I want to suggest an simple solution written in Java code:

    Create CustomSnapHelper class:

     public class CustomSnapHelper extends LinearSnapHelper {
            @Override
            public View findSnapView(RecyclerView.LayoutManager layoutManager) {
                if(layoutManager instanceof LinearLayoutManager){
                    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                    if(needToDoSnap(linearLayoutManager)==false){
                        return null;
                    }
                }
                return super.findSnapView(layoutManager);
            }
            public boolean needToDoSnap(LinearLayoutManager linearLayoutManager){
                return linearLayoutManager.findFirstCompletelyVisibleItemPosition()!=0&&linearLayoutManager.findLastCompletelyVisibleItemPosition()!=linearLayoutManager.getItemCount()-1;
            }
        }
    

    Attach an object of CustomSnapHelper for recycler view:

    CustomSnapHelper mSnapHelper = new CustomSnapHelper();
    mSnapHelper.attachToRecyclerView(mRecyclerView);
    
    0 讨论(0)
  • 2021-02-08 19:44

    I found a less invasive answer:

    private class PagerSelectSnapHelper : LinearSnapHelper() {
    
        override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
            // Use existing LinearSnapHelper but override when the itemDecoration calculations are off
            val snapView = super.findSnapView(layoutManager)
            return if (!snapView.isViewInCenterOfParent(layoutManager.width)) {
                val endView = layoutManager.findViewByPosition(layoutManager.itemCount - 1)
                val startView = layoutManager.findViewByPosition(0)
    
                when {
                    endView.isViewInCenterOfParent(layoutManager.width) -> endView
                    startView.isViewInCenterOfParent(layoutManager.width) -> startView
                    else -> snapView
                }
            } else {
                snapView
            }
        }
    
        private fun View?.isViewInCenterOfParent(parentWidth: Int): Boolean {
            if (this == null || width == 0) {
                return false
            }
            val parentCenter = parentWidth / 2
            return left < parentCenter && parentCenter < right
        }
    }
    
    0 讨论(0)
提交回复
热议问题