• RecyclerView的好朋友 — SnapHelpter


    SnapHelpter,相信很多人可能都不知道它或者没怎么关注过它,但是通过它实现的效果肯定都见过。比如短视频应用中切换视频时一划划一页的效果,这可不是ViewPager实现的啊,使用ViewPager实现的话成本太高,所以这类效果都是通过RecyclerVIew + SnapHelper来实现的,拿刚才讲的短视频切换效果来说,使用的就是RecyclerVIew和SnapHelper的子类PagerSnapHelper来实现的。

    一、SnapHelper初解

    说了这些,那么SnapHelper到底是什么东西呢?见名思意,Snap,翻译成中文有‘移到某位置’的意思,那么SnapHelper可以理解为‘移到某位置的帮手’,而这个被移到某位置的东西显然就是RecyclerVIew中的Item。

    public abstract class SnapHelper extends RecyclerView.OnFlingListener {
    
    //....
    
    @Nullable
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
            @NonNull View targetView);
    
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);
    
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,int velocityY);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到SnapHelper是一个抽象类,并继承了RecyclerView.OnFlingListener这个类,其中还包括三个抽象方法,我们通过实现这三个方法,就可以帮助RecyclerView移动item到‘某位置’。

    为了更好理解SnapHelper的这三个方法,先说说RecyclerView.OnFlingListener这个类。

    public abstract static class OnFlingListener {
    
        /**
         * 可用于实现自定义投掷行为
         *
         * @param velocityX X轴上的抛掷速度
         * @param velocityY Y轴上的抛掷速度
         *
         * @return 如果处理了投掷,则为 true,否则为 false。
         */
        public abstract boolean onFling(int velocityX, int velocityY);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这也是个抽象类,并且里面只有一个抽象方法,那这个类又是干啥的呢?我们都知道RecyclerView是可以滑动的,在我们手指离开屏幕后,RecyclerView还会继续顺着我们手指的方向再滑动一段距离,这个操作就是通过实现OnFlingListener接口来做到的。

    SnapHelper继承了OnFlingListener实现了onFling方法,并在调用attachToRecyclerView()方法的时候将OnFlingListener设置给了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();
            }
        }
    
    /**
     * Called when an instance of a {@link RecyclerView} is attached.
     */
    private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    二、三个方法

    接着我们继续看SnapHelper中的三个抽象方法。

    1、calculateDistanceToFinalSnap()

    /**
     * 计算将目标item移动到最终位置所需距离
     *
     * @param layoutManager 
     * @param targetView 需要被移动的item
     *
     * @return 输出坐标将结果,out[0] 是水平轴上的距离,out[1] 是垂直轴上的距离。
     */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
            @NonNull View targetView);public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,@NonNull View targetView);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这个方法是SnapHelper中另外两个抽象方法findSnapView()和findTargetSnapPosition()的下游方法,其参数中的targetView就是这两个方法提供的

    通过findSnapView()提供

    void snapToTargetExistingView() {
       	/***/
        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]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通过findTargetSnapPosition()提供

    private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        /**/
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    findTargetSnapPosition()被调用后,将找到的位置设置给smoothScroller,然后再通过layoutManager调用startSmoothScroll()方法启动smoothScroller

    public void startSmoothScroll(SmoothScroller smoothScroller) {
        if (mSmoothScroller != null && smoothScroller != mSmoothScroller
                && mSmoothScroller.isRunning()) {
            mSmoothScroller.stop();
        }
        mSmoothScroller = smoothScroller;
        mSmoothScroller.start(mRecyclerView, this);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在smoothScroller的start()方法中找到targetView

    void start(RecyclerView recyclerView, LayoutManager layoutManager) {
      
        /***/
        mTargetView = findViewByPosition(getTargetPosition());
        onStart();
        mRecyclerView.mViewFlinger.postOnAnimation();
    
        mStarted = true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最后回调到SnapHelper中创建的SmoothScroller中的onTargetFound()方法

    @Nullable
    @Deprecated
    protected LinearSmoothScroller createSnapScroller(RecyclerView.LayoutManager layoutManager) {
       /***/
        return new LinearSmoothScroller(mRecyclerView.getContext()) {
            @Override
            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
              /***/
                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
                        targetView);
                final int dx = snapDistances[0];
                final int dy = snapDistances[1];
                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                if (time > 0) {
                    action.update(dx, dy, time, mDecelerateInterpolator);
                }
            }
    
            @Override
            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
            }
        };
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2、findSnapView()

    /**
     * 找到需要被移动的item.
     * 如果返回 {@code null}, 则SnapHelper 不需要移动任何item.
     *
     * @param layoutManager
     *
     * @return 需要被移动的item
     */
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个方法会在SnapHelper绑定到RecyclerView时和RecyclerView停止滑动时被调用

    void snapToTargetExistingView() {
      	/***/
        View snapView = findSnapView(layoutManager);
       /***/
    }
    
    //绑定RecyclerView时被调用
    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
                throws IllegalStateException {
         		/***/
            if (mRecyclerView != null) {
               /***/
                snapToTargetExistingView();
            }
    }
    
    //RecyclerView停止滑到时被调用
    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) {
                       /***/
                    }
                };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    3、findTargetSnapPosition()

    /**
     * 找到需要被移动的目标item在adapter中的位置
     *
     * @param layoutManager 
     * @param 水平轴上的抛掷速度
     * @param 纵轴上的抛掷速度
     *
     * @return 返回需要被移动的目标item在adapter中的位置或者无需移动时返回 {@link RecyclerView#NO_POSITION}
     */
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,int velocityY);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个方法会在RecyclerView触发fling操作时被调用

    private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
      	/***/
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
    
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }
    
     @Override
        public boolean onFling(int velocityX, int velocityY) {
          /***/
            return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                    && snapFromFling(layoutManager, velocityX, velocityY);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    三、自定义SnapHelper实战

    了解了SnapHelper三个方法的作用以及何时会调用后,我们趁热打铁,自己实现一个SnapHelper,如果想更多了解关于SnapHelper的实现,可以去看看官方实现的LinearSnapHelperPagerSnapHelper

    这次我们继承SnapHelper,实现对RecyclerView一滑滑一页的效果,类似官方的PagerSnapHelper,但是比它更灵活,因为它的一页是一条item,我们的一页可以是多个item。

    其实这次要实现的效果在很多App中都能看到,尤其是应用商城类的App。

    光说还是有点懵,先看看实现的最终效果吧~

    public class MyGallerySnapHelper extends SnapHelper {
    
        protected RecyclerView mRecyclerView;
    
        @Nullable
        private OrientationHelper mHorizontalHelper;
    
        private int pageSize;
    
        @Override
        public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException {
            mRecyclerView = recyclerView;
            super.attachToRecyclerView(recyclerView);
        }
    
        @Nullable
        @Override
        public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View view) {
            int[] out = new int[2];
          	//RecyclerView为横向方向时
            if (layoutManager.canScrollHorizontally()) {
                out[0] = distance2Start(layoutManager, view,
                        getHorizontalHelper(layoutManager));
            }
            return out;
        }
    
        private int distance2Start(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView, OrientationHelper helper) {
            //获取item的宽度
            int columnWidth = helper.getDecoratedMeasurement(targetView);
            //获取item的下标
            int position = layoutManager.getPosition(targetView);
            //计算RecyclerView一屏可以展示多少item
            pageSize = (mRecyclerView.getWidth() - mRecyclerView.getPaddingStart() - mRecyclerView.getPaddingEnd()) / getHorizontalHelper(layoutManager).getDecoratedMeasurement(targetView);
            //计算item处于第几屏
            int pageIndex = position / pageSize;
            //计算上一步所得屏数中第一个item的下标
            int currentPageStart = pageIndex * pageSize;
            //计算传入item和它所属屏数第一个item的距离
            int distance = ((position - currentPageStart)) * columnWidth;
            //获取传入item的顶部在RecyclerView中的位置(像素)
            final int childStart = helper.getDecoratedStart(targetView);
            return childStart - distance;
        }
    
        @Nullable
        @Override
        public View findSnapView(RecyclerView.LayoutManager layoutManager) {
            return findStartView(layoutManager, getHorizontalHelper(layoutManager));
        }
    
        private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
    
            int childCount = layoutManager.getChildCount();
            if (childCount == 0) return null;
    
            int lastPosition = 0;
         		//获取最后一个完整可见item的下标
            if (layoutManager instanceof LinearLayoutManager) {
                lastPosition = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
            }
          
            int absClosest = Integer.MAX_VALUE;
            View snapView = null;
    				//如最后一个完整可见item的下标等于列表最后一个item的下标
            if (lastPosition == layoutManager.getItemCount() - 1) {
                snapView = layoutManager.getChildAt(lastPosition);
            } else {
                //找到距离RecyclerView顶部最近的item
                for (int i = 0; i < childCount; i++) {
                    View child = layoutManager.getChildAt(i);
                    int absDistance = helper.getDecoratedStart(child);
                    if (absDistance < absClosest) {
                        absClosest = absDistance;
                        snapView = child;
                    }
                }
            }
            return snapView;
        }
    
        @Override
        public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
            //找到距离RecyclerView顶部最近的item
            View snapView = findSnapView(layoutManager);
            if (snapView == null) return RecyclerView.NO_POSITION;
            //得到距离RecyclerView顶部最近的item的下标
            int startMostPosition = layoutManager.getPosition(snapView);
            if (startMostPosition == RecyclerView.NO_POSITION) return RecyclerView.NO_POSITION;
    
            //滑动方向,ture为正方向滑动 false为反方向滑动
            final boolean forwardDirection;
            if (layoutManager.canScrollHorizontally()) {
                forwardDirection = velocityX > 0;
            } else {
                forwardDirection = velocityY > 0;
            }
    
            View childAt = layoutManager.getChildAt(0);
    
            //计算RecyclerView一屏可以展示多少item
            if (childAt != null) {
                pageSize = (mRecyclerView.getWidth() - mRecyclerView.getPaddingStart() - mRecyclerView.getPaddingEnd()) / getHorizontalHelper(layoutManager).getDecoratedMeasurement(childAt);
            }
            //计算item处于第几屏
            int pageIndex = startMostPosition / pageSize;
            //计算上一步所得屏数中第一个item的下标
            int currentPageStart = pageIndex * pageSize;
            //根据滑动方向,在当前屏首的下标上加减数量
            return forwardDirection ? Math.min(currentPageStart + pageSize, layoutManager.getItemCount() - 1) : Math.max(0, currentPageStart + pageSize - 1);
        }
    
        @Nullable
        @Override
        protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
            return !(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider) ? null : new LinearSmoothScroller(this.mRecyclerView.getContext()) {
                protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {
                    if (mRecyclerView != null) {
                        int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
                        int dx = snapDistances[0];
                        int dy = snapDistances[1];
                        int time = this.calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
                        if (time > 0) {
                            action.update(dx, dy, time, this.mDecelerateInterpolator);
                        }
                    }
                }
    
                protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                    return 50.0F / (float) displayMetrics.densityDpi;
                }
            };
        }
    
        @NonNull
        private OrientationHelper getHorizontalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
            if (this.mHorizontalHelper == null || this.mHorizontalHelper.getLayoutManager() != layoutManager) {
                this.mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
            }
            return this.mHorizontalHelper;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142

    写的有点粗糙,只支持了LayoutManager为LinearLayoutManager时的水平方向,没有竖直方向的逻辑,也没有RecyclerView倒序时的逻辑,也没多少代码,懒得写了,想用的自己加吧。。

  • 相关阅读:
    【毕业设计】基于生成对抗网络的照片上色动态算法设计与实现 - 深度学习 opencv python
    JAVA主要API
    Word控件Spire.Doc 【图像形状】教程(4) 用 C# 中的文本替换 Word 中的图像
    three.js创建基础模型
    基于Python实现的全球新冠病毒数据分析
    PlantUML绘制类图
    【PR #5 A】双向奔赴(状压DP)
    c++学习---第四部分下
    Jmeter接口测试, 快速完成一个单接口请求
    MySQL数据库 || 增删改查操作详解
  • 原文地址:https://blog.csdn.net/Ever69/article/details/125587138