SnapHelper

1、简介

SnapHelper是RecyclerView的辅助类,可用来控制在滑动结束后,RecyclerView中item的对齐方式。

SnapHelper继承自RecyclerView.OnFlingListener,并实现了onFling,支持SnapHelper的RecyclerView.LayoutManager必须实现了RecyclerView.SmoothScroller.ScrollVectorProvider接口,或者自己实现onFling(int,int)方法手动处理。SnapHeper 有以下几个重要方法:

  • attachToRecyclerView:将SnapHelper attach 到指定的RecyclerView 上。
  • calculateDistanceToFinalSnap:复写这个方法指定滑动到TargetView或容器指定点的距离。这是一个抽象方法,由子类实现,返回一个长度为2的int数组out,out[0]表示x方向上对齐要移动的距离,out[1]表示y方向上对齐要移动的距离。抽象方法,需要子类实现。
  • calculateScrollDistance:用于Fling操作,根据每个方向上指定的速度计算出滑动的距离。
  • findSnapView:提供一个指定的目标View来对齐。抽象方法,需要子类实现。
  • findTargetSnapPosition:提供一个用于对齐的Adapter 目标position,抽象方法,需要子类自己实现。
  • onFling:根据给定的x和 y 轴上的速度处理Fling。

2、 LinearSnapHelper & PagerSnapHelper

SnapHelper是一个抽象类,系统内置了两个默认实现类:LinearSnapHelper & PagerSnapHelper

  • LinearSnapHelper:使RecyclerView的item居中显示。
  • PagerSnapHelper:使RecyclerView像ViewPager一样,每次只能滑动一页,PagerSnapHelper也是Item居中对齐。

接下来看下使用方法和效果。

2.1、LinearSnapHelper

LinearSnapHelper 使当前Item居中显示,常用场景是横向的RecyclerView, 类似ViewPager效果,但是又可以快速滑动(滑动多页)。代码如下:

 LinearLayoutManager manager = new LinearLayoutManager(getContext());
 manager.setOrientation(LinearLayoutManager.HORIZONTAL);
 mRecyclerView.setLayoutManager(manager);
// 将SnapHelper attach 到RecyclrView
 LinearSnapHelper snapHelper = new LinearSnapHelper();
 snapHelper.attachToRecyclerView(mRecyclerView);

代码很简单,new 一个SnapHelper对象,然后 Attach到RecyclerView 即可。
效果如下:


效果一

如上图所示,简单几行代码就可以用RecyclerView 实现一个类似ViewPager的效果,并且效果更赞。可以快速滑动多页,当前页居中显示,并且显示前一页和后一页的部分。除了上面的效果外,如果你想要和ViewPager 一样,限制一次只让它滑动一页,那么你就可以使用PagerSnapHelper了,接下来看一下PagerSnapHelper的使用效果。

2.2、PagerSnapHelper

PagerSnapHelper的展示效果和LineSnapHelper是一样的,只是PagerSnapHelper 限制一次只能滑动一页,不能快速滑动。代码如下:

PagerSnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);

PagerSnapHelper效果如下:


效果二

3、SnapHelper源码分析

上面介绍了SnapHelper的使用,那么接下来我们来看一下SnapHelper到底是怎么实现的。

3.1、snapToTargetExistingView

这个方法用于

  • 调用attachToRecyclerView绑定到RecyclerView时来完成对齐TargetView。
  • Scroll被触发时和Fling操作的末尾阶段时对齐TargetView。
    attachToRecyclerViewonScrollStateChanged中都调用了这个方法。
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 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;
            }
        }
    };

snapToTargetExistingView

 /**
     *
     * 1,判断RecyclerView 和LayoutManager是否为null
     *
     * 2,调用findSnapView  方法来获取需要对齐的目标View(这是个抽象方法,需要子类实现)
     * 
     * 3,通过calculateDistanceToFinalSnap 获取x方向和y方向对齐需要移动的距离(这个方法时抽象方法,由子类实现。)
     * 
     * 4,最后通过RecyclerView 的smoothScrollBy 来移动对齐
     * 
     */
    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        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]);
        }
    }

3.2、Filing 操作时对齐

SnapHelper继承了 RecyclerView.OnFlingListener,实现了onFling方法。

/**
 * fling 回调方法,方法中调用了snapFromFling,真正的对齐逻辑在snapFromFling里
 */
@Override
    public boolean onFling(int velocityX, int velocityY) {
        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);
    }
 /**
  *snapFromFling 方法被fling 触发,用来帮助实现fling 时View对齐
  *
  */
 private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
       // 首先需要判断LayoutManager 实现了ScrollVectorProvider 接口没有,
      //如果没有实现 ,则直接返回。此时需要手动去实现onFling方法进行处理。
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
      // 创建一个SmoothScroller 用来做滑动到指定位置
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        // 根据x 和 y 方向的速度来获取需要对齐的View的位置,需要子类实现。
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
       // 最终通过 SmoothScroller 来滑动到指定位置
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

其实通过上面的3个方法就实现了SnapHelper的对齐,只是有几个抽象方法是没有实现的,具体的对齐规则交给子类去实现。

4、LinearSnapHelper如何实现居中对齐的

接下来看一下LinearSnapHelper 是怎么实现居中对齐的:主要是实现了上面提到的三个抽象方法,findTargetSnapPosition、calculateDistanceToFinalSnap和findSnapView

4.1、calculateDistanceToFinalSnap

该方法计算最终对齐要移动的距离,返回一个长度为2的int 数组out,out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。

 @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];
       // 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为0
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToCenter(layoutManager, targetView,
                    getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }
 // 如果是竖直方向滚动的,则计算竖直方向需要移动的距离,否则竖直方向的移动距离为0
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToCenter(layoutManager, targetView,
                    getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }
        return out;
    }

   // 计算水平或者竖直方向需要移动的距离
    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        final int childCenter = helper.getDecoratedStart(targetView) +
                (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        return childCenter - containerCenter;
    }

4.2、findSnapView: 找到要对齐的View

// 找到要对齐的目标View, 最终的逻辑在findCenterView 方法里
// 规则是:循环LayoutManager的所有子元素,计算每个 childView的
//中点距离Parent 的中点,找到距离最近的一个,就是需要居中对齐的目标View
 @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;
    }

4.3、findTargetSnapPosition

findTargetSnapPosition :找到需要对齐的目标View的的Position。

@Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
...
// 前面代码省略
        int vDeltaJump, hDeltaJump;
       // 如果是水平方向滚动的列表,估算出水平方向SnapHelper响应fling 
       //对齐要滑动的position和当前position的差,否则,水平方向滚动的差值为0.
        if (layoutManager.canScrollHorizontally()) {
            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getHorizontalHelper(layoutManager), velocityX, 0);
            if (vectorForEnd.x < 0) {
                hDeltaJump = -hDeltaJump;
            }
        } else {
            hDeltaJump = 0;
        }
      // 如果是竖直方向滚动的列表,估算出竖直方向SnapHelper响应fling 
       //对齐要滑动的position和当前position的差,否则,竖直方向滚动的差值为0.
        if (layoutManager.canScrollVertically()) {
            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
                    getVerticalHelper(layoutManager), 0, velocityY);
            if (vectorForEnd.y < 0) {
                vDeltaJump = -vDeltaJump;
            }
        } else {
            vDeltaJump = 0;
        }

 // 最终要滑动的position 就是当前的Position 加上上面算出来的差值。
   
//后面代码省略
...
}

以上就分析了LinearSnapHelper 实现滑动的时候居中对齐和fling时居中对齐的源码。整个流程还是比较简单清晰的,就是涉及到比较多的位置计算比较麻烦。熟悉了它的实现原理,从上面我们知道,SnapHelper里面实现了对齐的流程,但是怎么对齐的规则就交给子类去处理了,比如LinearSnapHelper 实现了居中对齐,PagerSnapHelper 实现了居中对齐,并且限制只能一次滑动一页。那么我们也可以继承它来实现我们自己的SnapHelper,接下来看一下自己实现一个SnapHelper。

5、自定义 SnapHelper

上面分析了SnapHelper 的流程,那么这节我们来自定义一个SnapHelper , LinearSnapHelper 实现了居中对齐,那么我们来试着实现Target View 开始对齐。 当然了,我们不用去继承SnapHelper,既然LinearSnapHelper 实现了居中对齐,那么我们只要更改一下对齐的规则就行,更改为开始对齐(计算目标View到Parent start 要滑动的距离),其他的逻辑和LinearSnapHelper 是一样的。因此我们选择继承LinearSnapHelper,具体代码如下:

/**
 * Created by zhouwei on 17/3/30.
 */

public class StartSnapHelper extends LinearSnapHelper {

    private OrientationHelper mHorizontalHelper, mVerticalHelper;

    @Nullable
    @Override
    public int[] calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager, View targetView) {
        int[] out = new int[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 int distanceToStart(View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }

    @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof LinearLayoutManager) {

            if (layoutManager.canScrollHorizontally()) {
                return findStartView(layoutManager, getHorizontalHelper(layoutManager));
            } else {
                return findStartView(layoutManager, getVerticalHelper(layoutManager));
            }
        }

        return super.findSnapView(layoutManager);
    }



    private View findStartView(RecyclerView.LayoutManager layoutManager,
                              OrientationHelper helper) {
        if (layoutManager instanceof LinearLayoutManager) {
            int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            //需要判断是否是最后一个Item,如果是最后一个则不让对齐,以免出现最后一个显示不完全。
            boolean isLastItem = ((LinearLayoutManager) layoutManager)
                    .findLastCompletelyVisibleItemPosition()
                    == layoutManager.getItemCount() - 1;

            if (firstChild == RecyclerView.NO_POSITION || isLastItem) {
                return null;
            }

            View child = layoutManager.findViewByPosition(firstChild);

            if (helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2
                    && helper.getDecoratedEnd(child) > 0) {
                return child;
            } else {
                if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()
                        == layoutManager.getItemCount() - 1) {
                    return null;
                } else {
                    return layoutManager.findViewByPosition(firstChild + 1);
                }
            }
        }

        return super.findSnapView(layoutManager);
    }


    private OrientationHelper getHorizontalHelper(
            @NonNull RecyclerView.LayoutManager layoutManager) {
        if (mHorizontalHelper == null) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return mHorizontalHelper;
    }

    private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        if (mVerticalHelper == null) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mVerticalHelper;

    }
}

使用的时候,更改为使用StartSnapHelper,代码如下:

StartSnapHelper snapHelper = new StartSnapHelper();
snapHelper.attachToRecyclerView(mRecyclerView);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容