18.SnapHelper源码分析-RecyclerView实现ViewPager效果

1.简介

SnapHelper是Android 24.2.0 的support 包中添加的一个类,用于辅助RecyclerView扩展滑动效果。比如,通过这个类可以实现RecyclerView像ViewPager一样的滑动,甚至在此基础上再次扩展。越发的体现了RecyclerView的强大之处。今天从源码的角度看看SnapHelper的实现原理。

SnapHelper是一个抽象类,官方提供了两个子类供我们实现特定功能,LinearSnapHelper和PagerSnapHelper,基本上这两个类已经能满足我们日常百分之八十的需要,我们也可以通过继承SnapHelper实现特有功能。

既然是个抽象类,我们先看看他提供出来哪些接口供子类去实现,这些接口又完成了什么样的功能?

new SnapHelper() {
            @Nullable
            @Override
            public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
                return new int[0];
            }

            @Nullable
            @Override
            public View findSnapView(RecyclerView.LayoutManager layoutManager) {
                return null;
            }

            @Override
            public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
                return 0;
            }
        };
calculateDistanceToFinalSnap

这个方法用于计算滑动targetView到指定为止所需要在x轴或者y轴上移动的距离,返回的结果是一个int数组,两个元素分别是xy方向上需要移动的距离

findSnapView

用于找到当前情况下需要移动的view,也就是上边的targetView,一般是先找到这个view,然后计算需要移动的距离,然后开始移动

findTargetSnapPosition

提供一个targetView的position

2.源码分析

接下来我们开始分析SnapHelper的源码,从attachToRecyclerView开始入手,我们知道要使用SnapHelper,只需要attach一下即可,那么这个过程中他做了什么

   /**
     * Attaches the {@link SnapHelper} to the provided RecyclerView, by calling
     * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
     * You can call this method with {@code null} to detach it from the current RecyclerView.
     *
     * @param recyclerView The RecyclerView instance to which you want to add this helper or
     *                     {@code null} if you want to remove SnapHelper from the current
     *                     RecyclerView.
     *
     * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
     * attached to the provided {@link RecyclerView}.
     *
     */
    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
        //判断是否是同一个RecyclerView,如果是直接返回,防止重复attach
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        //如果是一个新的RecyclerView,先销毁所有的回调接口,这里指的是
        //RecyclerView的scroll监听和fling监听
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        //保存这个RecyclerView
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            //重新设置监听
            setupCallbacks();
            //初始化一个Scoller,用于滚动RecylerView
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            //这个方法开始才是进入了主题
            snapToTargetExistingView();
        }
    }

这个方法会在第一次attach的时候和RecyclerView滑动状态改变的时候调用,用于将RecyclerView移动到指定的位置,移动的距离根据当前距离RecyclerView中心点最近的那个itemView获取,获取到这个距离后,调用smoothScrollBy方法移动RecyclerView

   /**
     * Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
     * method is used to snap the view when the {@link RecyclerView} is first attached; when
     * snapping was triggered by a scroll and when the fling is at its final stages.
     */
    void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        //获取到当前需要移动的view,这个View获取的是距离当前RecyclerView
        //中心最近的一个View,可以看PagerSnapHelper和LinearSnapHelper这两
        //个官方提供的类,二者获取的规则是相同的,这个方法是抽象方法,用于
        //提供给调用者,由他指定获取此view的规则
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        //得到这个view之后,计算这个view在x或者y方向上需要移动的距离
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        //滑动RecyclerView到计算的位置
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }

接下来以PagerSnapHelper类的findSnapView和calculateDistanceToFinalSnap方法为例,来说明如何获取view和计算滑动距离的。分两种情况考虑,水平滑动还是竖直滑动,不同滑动方向使用不同的OrientationHelper去处理,这个类的功能我们后面会谈

findSnapView

    @Nullable
    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        //判断RecyclerView的滑动方向,这个可以由layoutManager获取
        if (layoutManager.canScrollVertically()) {
            //OrientationHelper是对RecycleView中子View管理的工具类
            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
        }
        return null;
    }

    /**
     * Return the child view that is currently closest to the center of this parent.
     * 如注释所说,这个方法返回的是一个距离当前parent也就是RecyclerView中心位置最近的一个item
     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
     *                      {@link RecyclerView}.
     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
     *
     * @return the child view that is currently closest to the center of this parent.
     */
    @Nullable
    private View findCenterView(RecyclerView.LayoutManager layoutManager,
            OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        final int center;
        //clipToPadding是RecyclerView的一个属性,表示在含有padding值的时候
        //会不会裁剪padding位置的view,如果为true,表示裁剪,那么在padding
        //位置滚动的时候是看不到view的
        if (layoutManager.getClipToPadding()) {
            //getStartAfterPadding  获取RecycleView左侧内边距(paddingLeft)
            //getTotalSpace  Recycleview水平内容区大小(宽度,除去左右内边距)
            //这些方法都会在OrientationHelper中具体说明,如果是Horizontal,
            //getStartAfterPadding  得到的是paddingLeft,如果是vertical,那么得到
            //的是paddingTop,自己想象一下
            //含有padding的时候,RecyclerView的中心位置应该是起始位置的padding
            //值加上内容区的宽度的一半,得到的刚好是RecyclerView显示内容的中心
            //位置的值
            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            //没有padding,则中心位置的值直接是宽度的一半
            center = helper.getEnd() / 2;
        }
        int absClosest = Integer.MAX_VALUE;

        //循环的计算所有childView,得到离上边计算到的中心点值最近的一个
        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            //getDecoratedStart返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
            //通俗地讲:子View左边界点到父View的(0,0)点的水平间距
            ///getDecoratedMeasurement返回view在水平方向上所占位置的大小(包括view的左右外边距)
            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;
    }

clipToPadding官方解释是
the ViewGroup will clip its children and resize (but not clip) any EdgeEffect to its padding, if padding is not zero.
意思是padding非空的时候,viewgroup会根据padding值来裁剪子view。默认情况下,clipToPadding为true,表示会裁剪子view并且resize边界效果。实际上是clipToPadding影响的是绘制的时候是否绘制padding里的子view内容。可以这么理解,clipToPadding对measure和layout无影响,只会影响draw阶段。无论clipToPadding是true还是false,滚的时候,都会滚到padding里去,但是clipToPadding为true,就会clip,这样padding里的部分就看不到了。回头看看刚才的问题,滚动前,布局是考虑padding的,所以padding里没有child元素,然后滚动的时候会把child滚动到padding里去,而由于clipToPadding=false,不裁剪,所以我们能看到padding里的child。如果设置clipToPadding=true,那padding里的child就会被裁剪掉,我们就看不到了

       final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
           if (clipToPadding) {
                clipSaveCount = canvas.save();
                canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
                       mScrollX + mRight - mLeft - mPaddingRight,
                       mScrollY + mBottom - mTop - mPaddingBottom);
        }

经过上边的计算,就能得到距离当前RecyclerView中心位置最近的一个itemView,找到view之后,如何计算滑动距离?

calculateDistanceToFinalSnap
这个方法相对简单,仍然是根据RecyclerView不同的orientation获取不同方向滑动的距离。也许你会觉得下边的计算方式在上边不是做了一次了吗,何必重复操作,其实在上边计算中心view的时候就已经得到这个距离了。但是为了将获取距离的方法暴露给调用者,所以多做了这个方法,而且,下边这个计算方式也只是PagerSnapHelper使用的方式,你还可以使用别的计算方法,所以这里并不算重复代码。
PagerSnapHelper

    @Nullable
    @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;
    }

    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
            @NonNull View targetView, OrientationHelper helper) {
        //getDecoratedStart 返回view左边界点(包含左内边距和左外边距)在父View中的位置(以父View的(0,0)点位坐标系)
            //通俗地讲:子View左边界点到父View的(0,0)点的水平间距
        final int childCenter = helper.getDecoratedStart(targetView)
                + (helper.getDecoratedMeasurement(targetView) / 2);
        final int containerCenter;
        if (layoutManager.getClipToPadding()) {
            //获取RecycleView左侧内边距(paddingLeft)
            //再次计算RecyclerView中心位置
            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
        } else {
            containerCenter = helper.getEnd() / 2;
        }
        //用itemView的中心位置减去RecyclerView的中心位置,得到的就是
        //将要移动的距离
        return childCenter - containerCenter;
    }

PagerSnapHelper的移动规则是每次滑动将距离中心位置最近的item移动到中心位置,如果你需要指定其他效果,就需要你自己实现这个方法,比如移动到左边距,移动到右边距等等。
然后就是调用mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1])移动RecyclerView了,这是所有attachToRecyclerView后做的操作,相当于初始化一下显示位置。

接下来要说的是滑动时的处理

我们刚才也看到了,在attachToRecyclerView方法调用时,给RecyclerView设置了两个监听方法,分别是对滑动事件和fling事件的监听,我们看看回调中执行了什么

    /**
     * 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);
    }

onScroll
可以看到,在RecyclerView滑动然后停下的状态会调用snapToTargetExistingView方法,正是上边讲到的逻辑。滑动停止之后,会执行一次摆放view的操作,按照我们设定好的方式移动RecyclerView到指定的位置

    // Handles the snap on scroll case.
    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;
                    }
                }
            };

然后看fling事件

    @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;
        }
        //获取出发RecyclerView fling的最小速度,这是一个定义好的常量值
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        //当滑动速度满足条件时,执行snapFromFling方法
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }

layoutManager必须满足是ScrollVectorProvider的子类,LinearLayoutManager GridLayoutManager和StaggeredGridLayoutManager都实现了这个接口,所以,都是满足的,那为何还要做这个判断,你自己想

    /**
     * Helper method to facilitate for snapping triggered by a fling.
     *
     * @param layoutManager The {@link LayoutManager} associated with the attached
     *                      {@link RecyclerView}.
     * @param velocityX     Fling velocity on the horizontal axis.
     * @param velocityY     Fling velocity on the vertical axis.
     *
     * @return true if it is handled, false otherwise.
     */
    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }
        //创建Scroller用于执行滑动事件
        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }
        //这是第三个抽象方法,下面看在PagerSnapHelper中是如何实现的
        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }
        //找到position后滚动到这个位置        
        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }

LinearSmoothScroller 这个类,在下一篇博客再看吧,调用mRecyclerView.smoothScrollToPosition 时候,RecyclerView实际调用的是LayoutManager中的代码,而layoutManager中也是使用LinearSmoothScoller滑动的。现在只需要知道他是控制RecyclerView滑动速度的即可。

    /**
     * Creates a scroller to be used in the snapping implementation.
     *
     * @param layoutManager     The {@link RecyclerView.LayoutManager} associated with the attached
     *                          {@link RecyclerView}.
     *
     * @return a {@link LinearSmoothScroller} which will handle the scrolling.
     */
    @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;
            }
        };
    }

    /**
     * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
     * DecelerateInterpolator looks smooth.</p>
     *
     * @param dx Distance to scroll
     * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
     * from LinearInterpolation
     */
    protected int calculateTimeForDeceleration(int dx) {
        // we want to cover same area with the linear interpolator for the first 10% of the
        // interpolation. After that, deceleration will take control.
        // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
        // which gives 0.100028 when x = .3356
        // this is why we divide linear scrolling time with .3356
        return  (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
    }

findTargetSnapPosition
以PagerSnapHelper实现为例

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
            int velocityY) {
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        View mStartMostChildView = null;
        if (layoutManager.canScrollVertically()) {
            //findStartView获取的就是距离RecyclerView中心点最近的view,同上
            mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
        } else if (layoutManager.canScrollHorizontally()) {
            mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
        }

        if (mStartMostChildView == null) {
            return RecyclerView.NO_POSITION;
        }

        //获取startView的position                        
        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
        if (centerPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        } 
        //只要速度大于0就是满足的        
        final boolean forwardDirection;
        if (layoutManager.canScrollHorizontally()) {
            forwardDirection = velocityX > 0;
        } else {
            forwardDirection = velocityY > 0;
        }
        boolean reverseLayout = false;
        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
            //这里会调用layoutManager中的computeScrollVectorForPosition,用于判断是否reverseLayout
            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
            if (vectorForEnd != null) {
                reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
            }
        }
        return reverseLayout
                ? (forwardDirection ? centerPosition - 1 : centerPosition)
                : (forwardDirection ? centerPosition + 1 : centerPosition);
    }
    
    //LinearLayoutManager中实现的    
    @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        if (getChildCount() == 0) {
            return null;
        }
        final int firstChildPos = getPosition(getChildAt(0));
        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
        if (mOrientation == HORIZONTAL) {
            return new PointF(direction, 0);
        } else {
            return new PointF(0, direction);
        }
    }
总结一下

总的来说,SnapHelper是通过计算当前距离RecyclerView中心位置最近的view与中心位置的距离来滑动RecyclerView,通过对scroll事件和fling事件的监听,实现需要的效果。这个过程中涉及到两个比较重要的类,OrientationHelper和LinearSmoothScrolloer,一个用于测量RecyclerView和其item,一个用于滑动RecyclerView。不出意外我会在后边的文章中去写这两个类的分析,除非忘了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值