目录介绍

  • 01.SnapHelper简单介绍

    • 1.1 SnapHelper作用
    • 1.2 SnapHelper类分析
    • 1.3 LinearSnapHelper类分析
    • 1.4 PagerSnapHelper类分析
  • 02.SnapHelper源码分析
    • 2.1 attachToRecyclerView入口方法
    • 2.2 SnapHelper的抽象方法
    • 2.3 onFling方法源码分析
  • 03.LinearSnapHelper源码分析
    • 3.1 LinearSnapHelper实现功能
    • 3.2 calculateDistanceToFinalSnap()方法源码
    • 3.3 findSnapView()方法源码
    • 3.4 findTargetSnapPosition()方法源码
    • 3.5 支持哪些LayoutManager
    • 3.6 OrientationHelper类
    • 3.7 estimateNextPositionDiffForFling计算偏移量
  • 04.自定义SnapHelper类
    • 4.1 业务需求
    • 4.2 自定义helper类

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.SnapHelper简单介绍

1.1 SnapHelper作用

  • 在某些场景下,卡片列表滑动浏览[有的叫轮播图],希望当滑动停止时可以将当前卡片停留在屏幕某个位置,比如停在左边,以吸引用户的焦点。那么可以使用RecyclerView + Snaphelper来实现,SnapHelper旨在支持RecyclerView的对齐方式,也就是通过计算对齐RecyclerView中TargetView 的指定点或者容器中的任何像素点。

1.2 SnapHelper类分析

  • 查阅可知,SnapHelper继承自RecyclerView.OnFlingListener,并且重写了onFling方法,这个类代码并不多,下面会对重要方法一一解析。

    • 支持SnapHelper的RecyclerView.LayoutManager必须实现的方式:

      • RecyclerView.SmoothScroller.ScrollVectorProvider接口
      • 或者自己实现onFling(int,int)方法手动处理逻辑。
  • SnapHelper类重要的方法
    • 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。
  • 什么是Fling操作
    • 手指在屏幕上滑动 RecyclerView然后松手,RecyclerView中的内容会顺着惯性继续往手指滑动的方向继续滚动直到停止,这个过程叫做 Fling 。 Fling 操作从手指离开屏幕瞬间被触发,在滚动停止时结束。

1.3 LinearSnapHelper类分析

  • LinearSnapHelper 使当前Item居中显示,常用场景是横向的RecyclerView,类似ViewPager效果,但是又可以快速滑动(滑动多页)。
  • 最简单的使用就是,如下代码
    • 几行代码就可以用RecyclerView实现一个类似ViewPager的效果,并且效果还不错。可以快速滑动多页,当前页剧中显示,并且显示前一页和后一页的部分。
    private void initRecyclerView() {
    LinearLayoutManager manager = new LinearLayoutManager(this);
    manager.setOrientation(LinearLayoutManager.HORIZONTAL);
    mRecyclerView.setLayoutManager(manager);
    LinearSnapHelper snapHelper = new LinearSnapHelper();
    snapHelper.attachToRecyclerView(mRecyclerView);
    SnapAdapter adapter = new SnapAdapter(this);
    mRecyclerView.setAdapter(adapter);
    adapter.addAll(getData());
    }

1.4 PagerSnapHelper类分析

  • PagerSnapHelper看名字可能就能猜到,使RecyclerView像ViewPager一样的效果,每次只能滑动一页(LinearSnapHelper支持快速滑动), PagerSnapHelper也是Item居中对齐。
  • 最简单的使用就是,如下代码
    private void initRecyclerView() {
    LinearLayoutManager manager = new LinearLayoutManager(this);
    manager.setOrientation(LinearLayoutManager.HORIZONTAL);
    mRecyclerView.setLayoutManager(manager);
    PagerSnapHelper snapHelper = new PagerSnapHelper();
    snapHelper.attachToRecyclerView(mRecyclerView);
    SnapAdapter adapter = new SnapAdapter(this);
    mRecyclerView.setAdapter(adapter);
    adapter.addAll(getData());
    }

02.SnapHelper源码分析

2.1 attachToRecyclerView入口方法

  • 通过attachToRecyclerView方法将SnapHelper attach 到RecyclerView,看一下这个方法的源代码

    • 如果SnapHelper之前已经附着到此RecyclerView上,则不用进行任何操作
    • 如果SnapHelper之前附着的RecyclerView和现在的不一致,就将原来设置的回调全部remove或者设置为null
    • 然后更新RecyclerView对象引用,Attach的RecyclerView不为null,设置回调Callback,主要包括滑动的回调和Fling操作的回调,初始化一个Scroller 用于后面做滑动处理,然后调用snapToTargetExistingView
    • 大概流程就是:在attachToRecyclerView()方法中会清掉SnapHelper之前保存的RecyclerView对象的回调(如果有的话),对新设置进来的RecyclerView对象设置回调,然后初始化一个Scroller对象,最后调用snapToTargetExistingView()方法对SnapView进行对齐调整。
    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();
    }
    }
  • 接着看看setupCallbacks()源码
    • 上面已经说了,滑动的回调和Fling操作的回调
    private void setupCallbacks() throws IllegalStateException {
    if (mRecyclerView.getOnFlingListener() != null) {
    throw new IllegalStateException("An instance of OnFlingListener already set.");
    }
    mRecyclerView.addOnScrollListener(mScrollListener);
    mRecyclerView.setOnFlingListener(this);
    }
  • 接着看看snapToTargetExistingView()方法
    • 这个方法用于第一次Attach到RecyclerView时对齐TargetView,或者当Scroll被触发的时候和fling操作的时候对齐TargetView 。
    • 判断RecyclerView 和LayoutManager是否为null,接着调用findSnapView 方法来获取需要对齐的目标View,注意:这是个抽象方法,需要子类实现
    • 通过calculateDistanceToFinalSnap 获取x方向和y方向对齐需要移动的距离
    • 最后如果需要滚动的距离不是为0,就调用smoothScrollBy方法使RecyclerView滚动相应的距离
    • 注意:RecyclerView.smoothScrollBy()这个方法的作用就是根据参数平滑滚动RecyclerView的中的ItemView相应的距离。
    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]);
    }
    }
  • 然后来看一下mScrollListener监听里面做了什么
    • 该滚动监听器的实现很简单,只是在正常滚动停止的时候调用了snapToTargetExistingView()方法对targetView进行滚动调整,以确保停止的位置是在对应的坐标上,这就是RecyclerView添加该OnScrollListener的目的。
    • mScrolled为true表示之前进行过滚动,newState为SCROLL_STATE_IDLE状态表示滚动结束停下来
    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;
    }
    }
    };

2.2 SnapHelper的抽象方法

  • calculateDistanceToFinalSnap抽象方法

    • 计算最终对齐要移动的距离

      • 计算二个参数对应的 ItemView 当前的坐标与需要对齐的坐标之间的距离。该方法返回一个大小为 2 的 int 数组,分别对应out[0] 为 x 方向移动的距离,out[1] 为 y 方向移动的距离。
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
    @NonNull View targetView);
  • findSnapView抽象方法
    • 找到要对齐的View

      • 该方法会找到当前 layoutManager 上最接近对齐位置的那个 view ,该 view 称为 SanpView ,对应的 position 称为 SnapPosition 。如果返回 null ,就表示没有需要对齐的 View ,也就不会做滚动对齐调整。
    @SuppressWarnings("WeakerAccess")
    @Nullable
    public abstract View findSnapView(LayoutManager layoutManager);
  • findTargetSnapPosition抽象方法
    • 找到需要对齐的目标View的的Position。

      • 更加详细一点说就是该方法会根据触发 Fling 操作的速率(参数 velocityX 和参数 velocityY )来找到 RecyclerView 需要滚动到哪个位置,该位置对应的 ItemView 就是那个需要进行对齐的列表项。我们把这个位置称为 targetSnapPosition ,对应的 View 称为 targetSnapView 。如果找不到 targetSnapPosition ,就返回RecyclerView.NO_POSITION 。
    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
    int velocityY);

2.3 onFling方法源码分析

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

    • 获取RecyclerView要进行fling操作需要的最小速率,为啥呢?因为只有超过该速率,ItemView才会有足够的动力在手指离开屏幕时继续滚动下去。
    @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方法源代码,就是通过该方法实现平滑滚动并使得在滚动停止时itemView对齐到目的坐标位置

    • 首先layoutManager必须实现ScrollVectorProvider接口才能继续往下操作
    • 然后通过createSnapScroller方法创建一个SmoothScroller,这个东西是一个平滑滚动器,用于对ItemView进行平滑滚动操作
    • 根据x和y方向的速度来获取需要对齐的View的位置,需要子类实现
    • 最终通过 SmoothScroller 来滑动到指定位置
    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
    int velocityY) {
    if (!(layoutManager instanceof ScrollVectorProvider)) {
    return false;
    } RecyclerView.SmoothScroller smoothScroller = createSnapScroller(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;
    }
    • 总结一下可知:snapFromFling()方法会先判断layoutManager是否实现了ScrollVectorProvider接口,如果没有实现该接口就不允许通过该方法做滚动操作。接下来就去创建平滑滚动器SmoothScroller的一个实例,layoutManager可以通过该平滑滚动器来进行滚动操作。SmoothScroller需要设置一个滚动的目标位置,将通过findTargetSnapPosition()方法来计算得到的targetSnapPosition给它,告诉滚动器要滚到这个位置,然后就启动SmoothScroller进行滚动操作。
  • 接着看下createSnapScroller这个方法源码

    • 先判断layoutManager是否实现了ScrollVectorProvider这个接口,没有实现该接口就不创建SmoothScroller
    • 这里创建一个LinearSmoothScroller对象,然后返回给调用函数,也就是说,最终创建出来的平滑滚动器就是这个LinearSmoothScroller
    • 在创建该LinearSmoothScroller的时候主要考虑两个方面:
      • 第一个是滚动速率,由calculateSpeedPerPixel()方法决定;
      • 第二个是在滚动过程中,targetView即将要进入到视野时,将匀速滚动变换为减速滚动,然后一直滚动目的坐标位置,使滚动效果更真实,这是由onTargetFound()方法决定。
    @Nullable
    protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
    if (!(layoutManager instanceof ScrollVectorProvider)) {
    return null;
    }
    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;
    }
    };
    }

03.LinearSnapHelper源码分析

3.1 LinearSnapHelper实现功能

  • LinearSnapHelper实现了SnapHelper,并且实现SnapHelper的三个抽象方法,从而让ItemView滚动居中对齐。那么具体怎么做到呢?

3.2 calculateDistanceToFinalSnap()方法源码

  • calculateDistanceToFinalSnap源码如下所示

    • 如果是水平方向滚动的,则计算水平方向需要移动的距离,否则水平方向的移动距离为0
    • 如果是竖直方向滚动的,则计算竖直方向需要移动的距离,否则竖直方向的移动距离为0
    • distanceToCenter方法主要作用是:计算水平或者竖直方向需要移动的距离
    @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;
    }
  • 接着看看distanceToCenter方法
    • 计算对应的view的中心坐标到RecyclerView中心坐标之间的距离
    • 首先是找到targetView的中心坐标
    • 接着也就是找到容器【RecyclerView】的中心坐标
    • 两个中心坐标的差值就是targetView需要滚动的距离
    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;
    }

3.3 findSnapView()方法源码

  • 也就是找到要对齐的View

    • 根据layoutManager的布局方式(水平布局方式或者竖向布局方式)区分计算,但最终都是通过findCenterView()方法来找snapView的。
    @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;
    }
  • 接着看看findCenterView方法源代码
    • 查询当前是否支持垂直滚动还是横向滚动
    • 循环LayoutManager的所有子元素,计算每个 childView的中点距离Parent 的中点,找到距离最近的一个,就是需要居中对齐的目标View
    @Nullable
    private View findCenterView(RecyclerView.LayoutManager layoutManager,
    OrientationHelper helper) {
    int childCount = layoutManager.getChildCount();
    if (childCount == 0) {
    return null;
    } View closestChild = null;
    final int center;
    if (layoutManager.getClipToPadding()) {
    center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
    } else {
    center = helper.getEnd() / 2;
    }
    int absClosest = Integer.MAX_VALUE; for (int i = 0; i < childCount; i++) {
    final View child = layoutManager.getChildAt(i);
    int childCenter = helper.getDecoratedStart(child)
    + (helper.getDecoratedMeasurement(child) / 2);
    int 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;
    }

3.4 findTargetSnapPosition()方法源码

  • LinearSnapHelper实现了SnapHelper,来看一下在findTargetSnapPosition操作了什么

    • 如果是水平方向滚动的列表,估算出水平方向SnapHelper响应fling,对齐要滑动的position和当前position的差,否则,水平方向滚动的差值为0
    • 如果是竖直方向滚动的列表,估算出竖直方向SnapHelper响应fling,对齐要滑动的position和当前position的差,否则,竖直方向滚动的差值为0
    • 这个方法在计算targetPosition的时候把布局方式和布局方向都考虑进去了。布局方式可以通过layoutManager.canScrollHorizontally()/layoutManager.canScrollVertically()来判断,布局方向就通过RecyclerView.SmoothScroller.ScrollVectorProvider这个接口中的computeScrollVectorForPosition()方法来判断。
    @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;
    }

3.5 支持哪些LayoutManager

  • SnapHelper为了适配layoutManager的各种情况,特意要求只有实现了RecyclerView.SmoothScroller.ScrollVectorProvider接口的layoutManager才能使用SnapHelper进行辅助滚动对齐。官方提供的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager都实现了这个接口,所以都支持SnapHelper。

3.6 OrientationHelper类

  • 如何创建OrientationHelper对象呢?如下所示

    • 比如,上面三个抽象方法都使用到了这个类,这个类是干嘛的?
    • 计算位置的时候用的是OrientationHelper这个工具类,它是LayoutManager用于测量child的一个辅助类,可以根据Layoutmanager的布局方式和布局方向来计算得到ItemView的大小位置等信息。
    @NonNull
    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
    if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
    mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
    }
    return mVerticalHelper;
    } @NonNull
    private OrientationHelper getHorizontalHelper(
    @NonNull RecyclerView.LayoutManager layoutManager) {
    if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
    mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
    }
    return mHorizontalHelper;
    }

3.7 estimateNextPositionDiffForFling计算偏移量

  • 如下所示

    • 首先,计算滚动的总距离,这个距离受到触发fling时的速度的影响,得到一个distances数组
    • 然后计算每个ItemView的长度
    • 根据是横向布局还是纵向布局,来取对应布局方向上的滚动距离
    • 总结大概流程就是:用滚动总距离除以itemview的长度,从而估算得到需要滚动的item数量,此数值就是位置偏移量。而滚动距离是通过SnapHelper的calculateScrollDistance()方法得到的,ItemView的长度是通过computeDistancePerChild()方法计算出来。
    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
    OrientationHelper helper, int velocityX, int velocityY) {
    int[] distances = calculateScrollDistance(velocityX, velocityY);
    float distancePerChild = computeDistancePerChild(layoutManager, helper);
    if (distancePerChild <= 0) {
    return 0;
    }
    int distance =
    Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
    return (int) Math.round(distance / distancePerChild);
    }

04.自定义SnapHelper类

4.1 业务需求

  • LinearSnapHelper 实现了居中对齐,那么我们只要更改一下对齐的规则就行,更改为开始对齐(计算目标 View到 Parent start 要滑动的距离),其他的逻辑和 LinearSnapHelper 是一样的。因此我们选择继承 LinearSnapHelper
  • 大概流程
    • 重写calculateDistanceToFinalSnap方法,计算SnapView当前位置与目标位置的距离
    • 写findSnapView方法,找到当前时刻的SnapView
    • 可以发现完成上面两个方法就可以呢,但是感觉滑动效果不太好。滑动比较快时,会滚动很远。在分析了上面的代码可知,滚动速率,由createSnapScroller方法中的calculateSpeedPerPixel()方法决定。那么是不是可以修改一下速率就可以解决问题呢。最后测试真的可以,ok,完成了。
    • 当然还会发现滚动时候,会滑动多个item,如果相对item个数做限制,可以在findTargetSnapPosition()方法中处理。
  • 代码地址:https://github.com/yangchong211/YCBanner

4.2 自定义helper类

  • 重写calculateDistanceToFinalSnap方法

    • 这里需要知道,在LinearSnapHelper中,out[0]和out[1]是通过distanceToCenter获取的。那么既然要设置开始对齐,那么这里需要创建distanceToStart方法
    @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();
    }
  • 写findSnapView方法,找到当前时刻的SnapView
    @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);
    }
  • 修改滚动速率
    @Nullable
    protected LinearSmoothScroller createSnapScroller(final RecyclerView.LayoutManager layoutManager) {
    if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
    return null;
    }
    return new LinearSmoothScroller(mRecyclerView.getContext()) {
    @Override
    protected void onTargetFound(View targetView, RecyclerView.State state, RecyclerView.SmoothScroller.Action action) {
    int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), targetView);
    final int dx;
    final int dy;
    if (snapDistances != null) {
    dx = snapDistances[0];
    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;
    }
    };
    }

关于其他内容介绍

关于其他内容介绍

01.关于博客汇总链接

02.关于我的博客

SnapHelper源码深度解析的更多相关文章

  1. 源码深度解析SpringMvc请求运行机制(转)

    源码深度解析SpringMvc请求运行机制 本文依赖的是springmvc4.0.5.RELEASE,通过源码深度解析了解springMvc的请求运行机制.通过源码我们可以知道从客户端发送一个URL请 ...

  2. SpringMVC 源码深度解析&lt;context:component-scan&gt;(扫描和注冊的注解Bean)

    我们在SpringMVC开发项目中,有的用注解和XML配置Bean,这两种都各有自己的优势,数据源配置比較经经常使用XML配置.控制层依赖的service比較经经常使用注解等(在部署时比較不会改变的) ...

  3. mybatis 3.x源码深度解析与最佳实践(最完整原创)

    mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...

  4. 并发编程(十五)——定时器 ScheduledThreadPoolExecutor 实现原理与源码深度解析

    在上一篇线程池的文章<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中从ThreadPoolExecutor源码分析了其运行机制.限于篇幅,留下了Scheduled ...

  5. 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)

    在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...

  6. VueRouter 源码深度解析

    VueRouter 源码深度解析 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还 ...

  7. Spring源码深度解析系列-----------org.springframework.aop-3.0.6.RELEASE

    Spring源码深度解析系列-----------org.springframework.aop-3.0.6.RELEASE

  8. spring源码深度解析— IOC 之 容器的基本实现

    概述 上一篇我们搭建完Spring源码阅读环境,spring源码深度解析—Spring的整体架构和环境搭建 这篇我们开始真正的阅读Spring的源码,分析spring的源码之前我们先来简单回顾下spr ...

  9. spring源码深度解析— IOC 之 默认标签解析(上)

    概述 接前两篇文章  spring源码深度解析—Spring的整体架构和环境搭建  和  spring源码深度解析— IOC 之 容器的基本实现 本文主要研究Spring标签的解析,Spring的标签 ...

  10. spring源码深度解析— IOC 之 默认标签解析(下)

    在spring源码深度解析— IOC 之 默认标签解析(上)中我们已经完成了从xml配置文件到BeanDefinition的转换,转换后的实例是GenericBeanDefinition的实例.本文主 ...

随机推荐

  1. Typora+PicGo+码云Gitee搭建本地博客环境

    Typora+PicGo+码云Gitee搭建本地博客环境 前期准备: 安装Typora 安装链接:https://typora.io/ 安装PicGo 安装链接:https://github.com/ ...

  2. 【React】排查两小时,修改一个词,记一个因代码书写不规范导致的生命周期BUG

    壹 ❀ 引 因为现在工作主要以修bug为主,日常工作中总是会接触到千奇百怪的前端问题,它可能是代码缺陷导致的程序错误,也可能是方案不合理造成的性能问题,老实说修bug是一件很枯燥的事情,你需要阅读大量 ...

  3. renren-fast-vue@1.2.2 项目编译报错: build `gulp`

    问题呈现: PS D:\Code\Java\ideaWorkspace\renren-fast-vue> npm run build > renren-fast-vue@1.2.2 bui ...

  4. 开源开发者的狂欢,STRK开了一个好头!附领取价值800元的web3空投教程

    这两天在Github和推特上最热闹的事情便是知名ETH(以太坊)二层公链项目STRK给所有gtihub上排名前5000的开源项目的项目贡献者提供了价值800元的代币空投,其中不乏前端程序员.大学生等w ...

  5. C++ 快速加载 Dll 里的 API

    最近项目里要重新编写程序加载器,也就是编译出一个可执行文件,在 Windows 上是 .exe 为什么要程序加载器? 个人理解是,可执行文件大小最好是越小越好,功能都可以由 dll 文件执行 而程序加 ...

  6. js结合canvas画任意多边形

    实现六边形 // html <canvas></canvas> // js const canvas = document.querySelector("canvas ...

  7. Ubuntu常用工具和问题整理

    安装Ubuntu虚拟机时常会遇到的几个问题 1.安装时设置镜像 安装Ubuntu系统时设置国内镜像可以加快安装速度:http://mirrors.aliyun.com/ubuntu/ 参考:ubunt ...

  8. render_to_string快捷函数,渲染模板字符串

    # views.py from django.template.loader import render_to_string from django.http import HttpResponse ...

  9. Java 通过属性名称读取或者设置实体的属性值

    原因 项目实战中有这个需求,数据库中配置对应的实体和属性名称,在代码中通过属性名称获取实体的对应的属性值. 解决方案 工具类,下面这个工具是辅助获取属性值 import com.alibaba.fas ...

  10. 【Azure Developer】Python 获取 Azure 中订阅(subscription)信息,包含ID, Name等

    问题描述 在Azure AD中注册一个Applicaiton后,对其进行授权,能够查看所有订阅的ReadOnly权限,然后,是否可以同通过Python代码,在完成Authorization后(使用Cl ...