资讯专栏INFORMATION COLUMN

SnapHelper源码深度解析

ThinkSNS / 2028人阅读

摘要:为表示之前进行过滚动,为状态表示滚动结束停下来的抽象方法抽象方法计算最终对齐要移动的距离计算二个参数对应的当前的坐标与需要对齐的坐标之间的距离。抽象方法找到要对齐的该方法会找到当前上最接近对齐位置的那个,该称为,对应的称为。

目录介绍

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/yangchong2...

如果觉得好,可以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/yangchong2...

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.关于博客汇总链接

1.技术博客汇总

2.开源项目汇总

3.生活博客汇总

4.喜马拉雅音频汇总

5.其他汇总

02.关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn

github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/...

简书:http://www.jianshu.com/u/b7b2...

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo...

开源中国:https://my.oschina.net/zbj161...

泡在网上的日子:http://www.jcodecraeer.com/me...

邮箱:yangchong211@163.com

阿里云博客:https://yq.aliyun.com/users/a... 239.headeruserinfo.3.dT4bcV

segmentfault头条:https://segmentfault.com/u/xi...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72109.html

相关文章

  • RecyclerView问题汇总

    摘要:缺点自动装箱的存在意味着每一次插入都会有额外的对象创建。对象本身是一层额外需要被创建以及被垃圾回收的对象。相较于我们舍弃了和类型的放弃了并依赖于二分法查找。 目录介绍 25.0.0.0 请说一下RecyclerView?adapter的作用是什么,几个方法是做什么用的?如何理解adapter订阅者模式? 25.0.0.1 ViewHolder的作用是什么?如何理解ViewHolder...

    boredream 评论0 收藏0
  • RecyclerView封装库和综合案例【包含25篇博客】

    摘要:支持复杂页面,例如添加自定义头部和底部布局,支持横向滑动,还可以支持粘贴头部类似微信好友分组,支持不规则瀑布流效果,支持侧滑删除功能。支持粘贴头部的需求效果,这种效果类似微信好友分组的那种功能界面。 目录介绍 1.复杂页面库介绍 2.本库优势亮点 2.1 支持多种状态切换管理 2.2 支持添加多个header和footer 2.3 支持侧滑功能和拖拽移动 2.4 其他亮点介绍 ...

    silenceboy 评论0 收藏0
  • Flink 源码解析 —— 深度解析 Flink Checkpoint 机制

    摘要:机制博客从到学习介绍从到学习上搭建环境并构建运行简单程序入门从到学习配置文件详解从到学习介绍从到学习如何自定义从到学习介绍从到学习如何自定义从到学习转换从到学习介绍中的从到学习中的几种详解从到学习读取数据写入到从到学习项目如何运行从 Flink Checkpoint 机制 https://t.zsxq.com/ynQNbeM 博客 1、Flink 从0到1学习 —— Apache Fl...

    0x584a 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<