美文网首页
RecyclerView中的SnapHelper

RecyclerView中的SnapHelper

作者: 慎独静思 | 来源:发表于2023-06-18 22:15 被阅读0次

    RecyclerView相关的文章预计会写六篇,此处是第三篇

    1. RecyclerView中的position
    2. RecyclerView中的DiffUtil
    3. RecyclerView中的SnapHelper
    4. RecyclerView中的Selection
    5. RecyclerView中的ConcatAdapter
    6. RecyclerView中的Glide预加载

    SnapHelper是什么

    SnapHelper是RecyclerView的辅助类,用来辅助RecyclerView在滚动结束时对齐到某个位置。


    SnapHelper

    LinearSnapHelper可以让RecyclerView滚动停止时相应的Item停留中间位置,PagerSnapHelper可以使RecyclerView实现像ViewPager一样的效果,一次只能滑一页,而且居中显示。

    怎么用

    使用比较简单

    LinearSnapHelper().attachToRecyclerView(recyclerView)
    
    PagerSnapHelper().attachToRecyclerView(recyclerView)
    

    源码浅析

    以LinearSnapHelper为例,简单学习一下原理,为接下来自定义SnapHelper打基础。

        /**
         * Attaches the {@link SnapHelper} to the provided RecyclerView, by calling
         * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
         * You can call this method with {@code null} to detach it from the current RecyclerView.
         *
         * @param recyclerView The RecyclerView instance to which you want to add this helper or
         *                     {@code null} if you want to remove SnapHelper from the current
         *                     RecyclerView.
         *
         * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
         * attached to the provided {@link RecyclerView}.
         *
         */
        public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
                throws IllegalStateException {
            if (mRecyclerView == recyclerView) {
                return; // nothing to do
            }
            if (mRecyclerView != null) {
                destroyCallbacks();
            }
            mRecyclerView = recyclerView;
            if (mRecyclerView != null) {
                setupCallbacks();
                mGravityScroller = new Scroller(mRecyclerView.getContext(),
                        new DecelerateInterpolator());
                snapToTargetExistingView();
            }
        }
    
        private void destroyCallbacks() {
            mRecyclerView.removeOnScrollListener(mScrollListener);
            mRecyclerView.setOnFlingListener(null);
        }
    
        private void setupCallbacks() throws IllegalStateException {
            if (mRecyclerView.getOnFlingListener() != null) {
                throw new IllegalStateException("An instance of OnFlingListener already set.");
            }
            mRecyclerView.addOnScrollListener(mScrollListener);
            mRecyclerView.setOnFlingListener(this);
        }
    

    首先看一下attachToRecyclerView,取消snap Helper时可以设置attachToRecyclerView(null),会去掉已添加的scroll listener和fling listener。
    添加回调监听时,如果RecyclerView已添加了fling listener,会抛异常。

        /**
         * Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
         * method is used to snap the view when the {@link RecyclerView} is first attached; when
         * snapping was triggered by a scroll and when the fling is at its final stages.
         */
        void snapToTargetExistingView() {
            if (mRecyclerView == null) {
                return;
            }
            RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
            if (layoutManager == null) {
                return;
            }
            View snapView = findSnapView(layoutManager);
            if (snapView == null) {
                return;
            }
            int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
            if (snapDistance[0] != 0 || snapDistance[1] != 0) {
                mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
            }
        }
    

    接下来看一下snapToTargetExistingView,根据方法名称,我们指定它是用来移动到已存在的目标View,方法逻辑比较简单,其中两个抽象方法findSnapView和calculateDistanceToFinalSnap是我们自定义SnapHelper需要复写的两个抽象方法。
    findSnapView - 提供要滚动的目标View
    calculateDistanceToFinalSnap - 计算要滚动的距离

        // Handles the snap on scroll case.
        private final RecyclerView.OnScrollListener mScrollListener =
                new RecyclerView.OnScrollListener() {
                    boolean mScrolled = false;
    
                    @Override
                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                        super.onScrollStateChanged(recyclerView, newState);
                        if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                            mScrolled = false;
                            snapToTargetExistingView();
                        }
                    }
    
                    @Override
                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                        if (dx != 0 || dy != 0) {
                            mScrolled = true;
                        }
                    }
                };
    

    接下来看一下给RecyclerView添加的scroll listener,可以看到在滚动结束时会snap到目标的View。

        @Override
        public boolean onFling(int velocityX, int velocityY) {
            RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
            if (layoutManager == null) {
                return false;
            }
            RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
            if (adapter == null) {
                return false;
            }
            int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
            return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                    && snapFromFling(layoutManager, velocityX, velocityY);
        }
    

    初始设置的fling listener会触发onFling回调,最终触发snapFromFling

        /**
         * Helper method to facilitate for snapping triggered by a fling.
         *
         * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
         *                      {@link RecyclerView}.
         * @param velocityX     Fling velocity on the horizontal axis.
         * @param velocityY     Fling velocity on the vertical axis.
         *
         * @return true if it is handled, false otherwise.
         */
        private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
                int velocityY) {
            if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
                return false;
            }
    
            RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
            if (smoothScroller == null) {
                return false;
            }
    
            int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
            if (targetPosition == RecyclerView.NO_POSITION) {
                return false;
            }
    
            smoothScroller.setTargetPosition(targetPosition);
            layoutManager.startSmoothScroll(smoothScroller);
            return true;
        }
    

    其中findTargetSnapPosition是一个需要复写的抽象方法,用来提供要滚动的目标adapter 位置。
    我们对SnapHelper有了一个大体的了解,接下来看一下LinearSnapHelper是怎么重写这几个抽象方法的。

        @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
            if (layoutManager.canScrollVertically()) {
                return findCenterView(layoutManager, getVerticalHelper(layoutManager));
            } else if (layoutManager.canScrollHorizontally()) {
                return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
            }
            return null;
        }
    

    首先是findSnapView,其中findCenterView,顾名思义就是查找中间的View。

        @Override
        public int[] calculateDistanceToFinalSnap(
                @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
            int[] out = new int[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;
        }
    

    calculateDistanceToFinalSnap的逻辑也很简单。

        @Override
        public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                int velocityY) {
            if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
                return RecyclerView.NO_POSITION;
            }
    
            final int itemCount = layoutManager.getItemCount();
            if (itemCount == 0) {
                return RecyclerView.NO_POSITION;
            }
    
            final View currentView = findSnapView(layoutManager);
            if (currentView == null) {
                return RecyclerView.NO_POSITION;
            }
    
            final int currentPosition = layoutManager.getPosition(currentView);
            if (currentPosition == RecyclerView.NO_POSITION) {
                return RecyclerView.NO_POSITION;
            }
    
            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
            // 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.
            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
            if (vectorForEnd == null) {
                // cannot get a vector for the given position.
                return RecyclerView.NO_POSITION;
            }
    
            int vDeltaJump, hDeltaJump;
            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;
            }
    
            int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
            if (deltaJump == 0) {
                return RecyclerView.NO_POSITION;
            }
    
            int targetPos = currentPosition + deltaJump;
            if (targetPos < 0) {
                targetPos = 0;
            }
            if (targetPos >= itemCount) {
                targetPos = itemCount - 1;
            }
            return targetPos;
        }
    

    findTargetSnapPosition的逻辑稍显复杂,但感觉也还好。

    自定义SnapHelper

    接下来按照LinearSnapHelper,咱们自定义一个item停留顶部或左侧的SnapHelper。

        override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
            if (layoutManager!!.canScrollVertically()) {
                return findStartView(layoutManager, getVerticalHelper(layoutManager))
            } else if (layoutManager.canScrollHorizontally()) {
                return findStartView(layoutManager, getHorizontalHelper(layoutManager))
            }
            return null
        }
    
        private fun findStartView(
            layoutManager: RecyclerView.LayoutManager,
            helper: OrientationHelper?
        ): View? {
            if (layoutManager !is LinearLayoutManager) {
                return null
            }
    
            var firstPos = layoutManager.findFirstVisibleItemPosition();
            if (firstPos == RecyclerView.NO_POSITION) {
                return null
            }
    
            if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) {
                return null
            }
    
            var firstView = layoutManager.findViewByPosition(firstPos)
            if (helper!!.getDecoratedEnd(firstView) > 0
                && helper.getDecoratedEnd(firstView) >= helper.getDecoratedMeasurement(firstView) / 2
            ) {
                return firstView
            }
    
            return layoutManager.findViewByPosition(firstPos + 1)
        }
    

    首先是findSnapView,参考了LinearSnapHelper和让你明明白白的使用RecyclerView——SnapHelper详解的写法。

        override fun calculateDistanceToFinalSnap(
            layoutManager: RecyclerView.LayoutManager,
            targetView: View
        ): IntArray? {
            val out = IntArray(2)
            if (layoutManager.canScrollHorizontally()) {
                out[0] = distanceToStart(targetView,
                    getHorizontalHelper(layoutManager)!!
                )
            } else {
                out[0] = 0
            }
    
            if (layoutManager.canScrollVertically()) {
                out[1] = distanceToStart(targetView,
                    getVerticalHelper(layoutManager)!!
                )
            } else {
                out[1] = 0
            }
            return out
        }
    
        private fun distanceToStart(
            targetView: View, helper: OrientationHelper
        ): Int {
            return helper.getDecoratedStart(targetView) - helper.startAfterPadding
        }
    

    calculateDistanceToFinalSnap稍微简单一点

        override fun findTargetSnapPosition(
            layoutManager: RecyclerView.LayoutManager?,
            velocityX: Int,
            velocityY: Int
        ): Int {
            if (layoutManager !is 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
            }
    
            // 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.
            // 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 = layoutManager.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
        }
    

    findTargetSnapPosition这部分直接拷贝的LinearSnapHelper的源码。

    Demo 地址

    参考:

    1. SnapHelper
    2. 让你明明白白的使用RecyclerView——SnapHelper详解
    3. SnapHelper硬核讲解

    相关文章

      网友评论

          本文标题:RecyclerView中的SnapHelper

          本文链接:https://www.haomeiwen.com/subject/opvsydtx.html