Evil Mouth's Blog

使用SnapHelper帮助RecyclerView滑动停留

February 13, 2017

现在做项目早已抛弃ListView而选择RecyclerView,而且使用RecyclerView起来也已经得心应手,但是RecyclerView有很多隐藏功能比较少用到,比如接下来的SnapHelper

偷偷截下 Google Play 的图 1 如上图所示,上面的 banner 就是中对齐的效果,下面的游戏列表就是左对齐的效果

官网 support 包提供了SnapHelper帮助实现这样的效果

public abstract class SnapHelper extends RecyclerView.OnFlingListener

使用方法很简单,官方已经帮我们实现了一个LinearSnapHelper供我们直接使用,运行后是中对齐的效果

SnapHelper snapHelper = new LinearSnapHelper();
snapHelper.attachToRecyclerView(recyclerview);

官方没有提供左对齐,想要左对齐需要自己动手,继承LinearSnapHelper并重写calculateDistanceToFinalSnapfindSnapView方法

public class FirstItemSnapHelper extends LinearSnapHelper {
    private OrientationHelper mVerticalHelper, mHorizontalHelper;

    /**
     * 计算移动距离
     */
    @Override
    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
        int[] out = new int[2];

        //如果是水平滑动,计算x偏移量
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToStart(targetView, getHorizontalHelper(layoutManager));
        } else {
            out[0] = 0;
        }

        //如果是垂直滑动,计算y偏移量
        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToStart(targetView, getVerticalHelper(layoutManager));
        } else {
            out[1] = 0;
        }

        return out;
    }

    /**
     * 寻找需要移动的item
     */
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof LinearLayoutManager) {
            if (layoutManager.canScrollHorizontally()) {
                return getStartView(layoutManager, getHorizontalHelper(layoutManager));
            } else {
                return getStartView(layoutManager, getVerticalHelper(layoutManager));
            }
        }

        //返回null以不进行偏移移动
        return null;
    }

    private int distanceToStart(View targetView, OrientationHelper helper) {
        return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
    }

    private View getStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
        if (layoutManager instanceof LinearLayoutManager && layoutManager.getItemCount() > 0) {
            //出于对item宽度或高度不够大的考虑,故需要判断是否滑动最后一个item了,否则可能会导致永远会滑不到最后
            boolean isLastItem = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1;
            if (isLastItem) {
                return null;
            }

            //因为是要对齐第一个item,所以这里找到使用findFirstVisibleItemPosition
            int firstChild = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
            View child = layoutManager.findViewByPosition(firstChild);

            //根据该item的右坐标比对该item的一半(宽度或高度)返回最终的SnapView
            if (helper.getDecoratedEnd(child) > 0 && helper.getDecoratedEnd(child) >= helper.getDecoratedMeasurement(child) / 2) {
                return child;
            } else {
                return layoutManager.findViewByPosition(firstChild + 1);
            }
        }

        //返回null以不进行偏移移动
        return null;
    }

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

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

使用方法也是去attachToRecyclerView,最终实现左对齐的效果

使用注意

SnapHelper一般项目需要都是用于水平列表,但其实SnapHelper同样适用于垂直列表,需要注意的一点是,SnapHelper具体原理是根据判断哪个 item 到达指定位置的偏差小而去滑动的,当 item 的宽度或高度不够大的情况下(需要尽可能大于半屏),滑动到最后一个 item 会因为偏差不够前一个 item 大而导致选择了前一个 item 去移动。官方提供的LinearSnapHelper就是这样,如果 item 宽度不够大,会出现的情况是当滑动最后一松手,会判定倒数第二个 item 偏差小而选择倒数第二个 item 为SnapView去滑动到中间,导致最后一个 item 永远无法显示全。而本文提供的FirstItemSnapHelper虽然有相关处理是否到达最后一个 item,当也许不是适合每个需求,具体还得根据需求去修改。

— Evil Mouth