RecyclerView 的 FastScroller 绘制的源码分析

RecyclerView 以前一直被人诟病没有 FastScroller 的功能,然后网上出现了几种解决方法

  1. 继承 RecyclerView,重写 draw() 方法,绘制 FastScroller
  2. 单独自定义一个 View,然后传入 RecyclerView 作为参数。

第一种方法沿用了 ListView 的思维,把 FastScrollerRecyclerView 绘制在一起,耦合度过高,如果代码写的不好,容易出问题。

第二种方法,虽然解决了耦合度高的问题,但是没有充分发挥 RecyclerView 的优势。

那么 Google 看不下去了,自己加入了 FastScroller 功能,既解耦,又充分利用了 RecyclerView 优势,它的实现方式是继承 ItemDecoration

class FastScroller extends ItemDecoration

不过个人认为这个功能并不那么好用,主要有一下几点

  1. ListViewAdapter 如果实现了 SectionIndexer 接口,那么 ListView 会在 ScrollBar 的左侧展示一个气泡形状的 Index, 而 RecyclerViewFastScroller 并没有完善这个功能。
  2. 使用起来复杂
  3. 没有处理 ViewHolder.itemView 高度不一致的情况
  4. 使用效果并不好。

带着这些问题,让我们一起从源码解读这个 FastScroller。 不过在分析源码之前,要说明一点,本文的侧重点是分析垂直方向的 FastScroller

那么,在分析源码之前,看下 FastScroller 的效果。

这里写图片描述

如果你看得比较仔细,你应该会发现,FastScroller 并不能让内容区域滚动到底,为什么?看后面分析。

从前一篇文章可知,FastScroller 是在 RecyclerView 的构造方法中调用的

    public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // ...
        if (attrs != null) {
            // ...
            if (mEnableFastScroller) {
                StateListDrawable verticalThumbDrawable = (StateListDrawable) a
                        .getDrawable(R.styleable.RecyclerView_fastScrollVerticalThumbDrawable);
                Drawable verticalTrackDrawable = a
                        .getDrawable(R.styleable.RecyclerView_fastScrollVerticalTrackDrawable);
                StateListDrawable horizontalThumbDrawable = (StateListDrawable) a
                        .getDrawable(R.styleable.RecyclerView_fastScrollHorizontalThumbDrawable);
                Drawable horizontalTrackDrawable = a
                        .getDrawable(R.styleable.RecyclerView_fastScrollHorizontalTrackDrawable);
                initFastScroller(verticalThumbDrawable, verticalTrackDrawable,
                        horizontalThumbDrawable, horizontalTrackDrawable);
            }

            // ...
        } else {
            // ...
        }
        // ...
    }

    void initFastScroller(StateListDrawable verticalThumbDrawable,
            Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
            Drawable horizontalTrackDrawable) {
        if (verticalThumbDrawable == null || verticalTrackDrawable == null
                || horizontalThumbDrawable == null || horizontalTrackDrawable == null) {
            throw new IllegalArgumentException(
                "Trying to set fast scroller without both required drawables." + exceptionLabel());
        }

        Resources resources = getContext().getResources();
        new FastScroller(this, verticalThumbDrawable, verticalTrackDrawable,
                horizontalThumbDrawable, horizontalTrackDrawable,
                resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
                resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
                resources.getDimensionPixelOffset(R.dimen.fastscroll_margin));
    }    

RecyclerView 的构造函数中可以看出,一定要为 RecyclerView 设置 app:fastScrollEnabled="true"

initFastScroller() 方法可以看出,一定要设置四个属性,否则异常! 而且 android:fastScrollVerticalThumbDrawableandroid:fastScrollHorizontalThumbDrawable 要为 StateListDrawable 类型,android:fastScrollVerticalTrackDrawableandroid:fastScrollHorizontalTrackDrawable 要为 Drawable 类型。

看到这里,我想大家心里跟我一样会有一个疑问,那就是如果我只需要绘制垂直方向的 FastScroller,那么为何还要设置水平方向的 FastScroller 属性呢? 而且设置属性的时候对 Drawable 类型还有特殊要求!这就是我之前说过的 FastScroller 使用起来复杂的问题。

initFastScroller() 方法的最后,new 了一个 FastScroller(),其中注意下它的最后三个参数

  1. R.dimen.fastscroll_default_thicknessFastScroller 默认宽度
  2. R.dimen.fastscroll_minimum_rangeRecyclerView 的高度必须要大于这个值才能绘制 FastScroller
  3. R.dimen.fastscroll_margin 为手指在 FastScroller 滑动范围的 topMarginbottomMargin

现在进入 FastScroller 构造函数

    private final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);

    FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable,
            Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
            Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange,
            int margin) {
        mVerticalThumbDrawable = verticalThumbDrawable;
        mVerticalTrackDrawable = verticalTrackDrawable;
        mHorizontalThumbDrawable = horizontalThumbDrawable;
        mHorizontalTrackDrawable = horizontalTrackDrawable;
        mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth());
        mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth());
        mHorizontalThumbHeight = Math
            .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth());
        mHorizontalTrackHeight = Math
            .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth());
        mScrollbarMinimumRange = scrollbarMinimumRange;
        mMargin = margin;
        mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE); // SCROLLBAR_FULL_OPAQUE = 255
        mVerticalTrackDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);

        mShowHideAnimator.addListener(new AnimatorListener());
        mShowHideAnimator.addUpdateListener(new AnimatorUpdater());

        attachToRecyclerView(recyclerView);
    }

本文只分析垂直方向上的 FastScroller,而其中需要关注的是 mVerticalThumbWidthmVerticalTrackWidth 的值,取的是默认值和 Drawable 的实际宽度的最大值。一般取系统的默认宽度即可,而如果需要,就要自己设置 Drawbale 的宽度。

接着把 mVerticalThumbDrawablemVerticalTrackDrawable 的透明度设置为 255。 为何只单单设置垂直方向的 Drawable 的透明度?不得而知,继续往后看吧。

还为 mShowHideAnimator 设置了两个 Listener,在执行隐藏和显示 FastScroller 的动画的时候会用到。

最后调用 attachToRecyclerView(recyclerView)

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            setupCallbacks();
        }
    }

    private void setupCallbacks() {
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(this);
        mRecyclerView.addOnScrollListener(mOnScrollListener);
    }

setupCallbacks() 方法做了三件事

  1. 把当前的 ItemDecoration 也就是 FastScroller, 添加到 RecyclerView
  2. RecyclerView 添加 onItemTouchListener,用于在触摸 FastScroller 的时候,截断并处理 MotionEvent
  3. RecyclerView 添加 onScrollListener,用于检测 RecyclerView 的滑动,决定是否显示 FastScroller

构造方法分析完了,那么首先要分析的情况就是界面刚显示的时候,这个时候 RecyclerView 会绘制 ItemDecoration,而 FastScroller 也就理所当然要绘制。而FastScroller 只复写了 ItemDecorationonDrawOver() 方法

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        if (mRecyclerViewWidth != mRecyclerView.getWidth()
                || mRecyclerViewHeight != mRecyclerView.getHeight()) {
            mRecyclerViewWidth = mRecyclerView.getWidth();
            mRecyclerViewHeight = mRecyclerView.getHeight();
            // This is due to the different events ordering when keyboard is opened or
            // retracted vs rotate. Hence to avoid corner cases we just disable the
            // scroller when size changed, and wait until the scroll position is recomputed
            // before showing it back.
            setState(STATE_HIDDEN);
            return;
        }

        if (mAnimationState != ANIMATION_STATE_OUT) {
            if (mNeedVerticalScrollbar) {
                drawVerticalScrollbar(canvas);
            }
            if (mNeedHorizontalScrollbar) {
                drawHorizontalScrollbar(canvas);
            }
        }
    }

首先,mRecyclerViewWidthmRecyclerViewHeight 初始化都为 0,所以后来分别被赋值为 mRecyclerView.getWidth()mRecyclerView.getHeight()。然后调用了 setState() 方法,最后就 return 了。setState()方法如下

    private void setState(@State int state) {
        if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
            mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
            cancelHide();
        }

        if (state == STATE_HIDDEN) {
            requestRedraw();
        } else {
            show();
        }

        if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
            mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
            resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
        } else if (state == STATE_VISIBLE) {
            resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
        }
        mState = state;
    }

因为参数 state 的值为 STATE_HIDDEN,这里的 setState()只做了两件事

  1. 调用了 requestRedraw()RecyclerView 进行重新绘制
  2. 设置 mStateSTATE_HIDDEN

到此,我们发现 onDrawOver() 方法并没有去绘制 FastScroller,那么什么时候绘制的呢? 如果你使用过 FastScroller,你就会发现,只有在 RecyclerView 滑动的时候才会去绘制。 而在 setupCallbacks() 方法中,为 RecyclerView 设置过 onScrollListener,也就是mOnScrollListener 变量

    private final OnScrollListener mOnScrollListener = new OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            updateScrollPosition(recyclerView.computeHorizontalScrollOffset(),
                    recyclerView.computeVerticalScrollOffset());
        }
    };

RecyclerView 滑动的时候,调用了 updateScrollPosition() 方法,其中两个参数分别 RecyclerViewXY 方向的偏移量,因为现在只关心垂直的 FastScrollerX 方向偏移量为 0,所以直接看 recyclerView.computeVerticalScrollOffset() 方法是如何计算垂直的偏移量

    public int computeVerticalScrollOffset() {
        if (mLayout == null) {
            return 0;
        }
        return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
    }

return 那一行代码可以看出,如果 LayoutManager 可以垂直滑动,也就是 mLayout.canScrollVertically() 返回 true,那么就用 LayoutManagercomputeVerticalScrollOffset() 方法来计算垂直方向滑动的偏移量,这里以 LinearLayoutManager 的方法为例

    @Override
    public int computeVerticalScrollOffset(RecyclerView.State state) {
        return computeScrollOffset(state);
    }

    private int computeScrollOffset(RecyclerView.State state) {
        if (getChildCount() == 0) {
            return 0;
        }
        ensureLayoutState();
        return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper,
                findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
                findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
                this, mSmoothScrollbarEnabled, mShouldReverseLayout);
    }    

最终是调用了 ScrollbarHelpercomputeScrollOffset() 方法来计算的,不过在看这个方法之前,首先需要知道它的几个参数。

通过上一篇文章的分析,可以知道参数 statemOrientationHelper 的一些状态值,以及 mShouldReverseLayout = false

参数 mSmoothScrollbarEnabled 默认就是为 true

参数 findFirstVisibleChildClosestToStart()findFirstVisibleChildClosestToEnd() 是为了找到 RecyclerView 中第一个显示的 Child 最后一个显示的 Child。这两个方法和 findFirstVisibleItemPosition()findLastVisibleItemPosition() 的原理是一样的,具体代码就不分析了。

那么现在,直接进入到 ScrollbarHelper.computeScrollOffset() 方法

    static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
            View startChild, View endChild, RecyclerView.LayoutManager lm,
            boolean smoothScrollbarEnabled, boolean reverseLayout) {
        if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
                || endChild == null) {
            return 0;
        }
        final int minPosition = Math.min(lm.getPosition(startChild),
                lm.getPosition(endChild));
        final int maxPosition = Math.max(lm.getPosition(startChild),
                lm.getPosition(endChild));
        final int itemsBefore = reverseLayout
                ? Math.max(0, state.getItemCount() - maxPosition - 1)
                : Math.max(0, minPosition);
        if (!smoothScrollbarEnabled) {
            return itemsBefore;
        }
        final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
                - orientation.getDecoratedStart(startChild));
        final int itemRange = Math.abs(lm.getPosition(startChild)
                - lm.getPosition(endChild)) + 1;
        final float avgSizePerRow = (float) laidOutArea / itemRange;

        return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
                - orientation.getDecoratedStart(startChild)));
    }

laidOutArea 为最后一个显示的 Child 的底部坐标(包括 ItemDecoration 造成的 paddingChild 本身的 bottomMargin)减去第一个显示的 Child 的顶部坐标(包括 ItemDecoration 造成的 paddingChild 本身的 topMargin)。这个意思就比较明显了,就是界面上能看到的所有View 整体高度。

如果这里的函数不能理解,请参考我前一篇文章的分析。

itemRange 为界面显示 Children 的个数。

avgSizePerRow 为界面上显示的每个 Child 的平均高度。

最后通过 itemsBefore * avgSizePerRow 来估算了没有绘制出来的前面的所有 Child 的高度。 注意,我这里用了“估算” 这个词,因为这里是用 avgSizePerRow 这个平均值去乘以 startChild 之前没有显示 Child 的个数。而我们经常会遇到一种情况,不同的类型的 Child ,它的高度是不一样的。所以,这就是我在文章开头提到,它没有考虑 ViewHolder.itemView 高度不一致的问题。

那么,现在直接看 mOnScrollListener 中调用的 updateScrollPosition(recyclerView.computeHorizontalScrollOffset(),recyclerView.computeVerticalScrollOffset())方法

    void updateScrollPosition(int offsetX, int offsetY) {
        int verticalContentLength = mRecyclerView.computeVerticalScrollRange();
        int verticalVisibleLength = mRecyclerViewHeight;
        mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0
            && mRecyclerViewHeight >= mScrollbarMinimumRange;

        int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange();
        int horizontalVisibleLength = mRecyclerViewWidth;
        mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0
            && mRecyclerViewWidth >= mScrollbarMinimumRange;

        if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) {
            if (mState != STATE_HIDDEN) {
                setState(STATE_HIDDEN);
            }
            return;
        }

        if (mNeedVerticalScrollbar) {
            float middleScreenPos = offsetY + verticalVisibleLength / 2.0f;
            mVerticalThumbCenterY =
                (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength);
            mVerticalThumbHeight = Math.min(verticalVisibleLength,
                (verticalVisibleLength * verticalVisibleLength) / verticalContentLength);
        }

        if (mNeedHorizontalScrollbar) {
            float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f;
            mHorizontalThumbCenterX =
                (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength);
            mHorizontalThumbWidth = Math.min(horizontalVisibleLength,
                (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength);
        }

        if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) {
            setState(STATE_VISIBLE);
        }
    }

参数 offsetX 值是 0offsetY 就是刚才计算出来的。

变量 verticalContentLength 是代表 ReyclerView 实际需要显示所有 View 的高度,调用的是 ReyclerViewcomputeVerticalScrollRange() 方法

    public int computeVerticalScrollRange() {
        if (mLayout == null) {
            return 0;
        }
        return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0;
    }

LinearLayoutManager 为例,看下 computeVerticalScrollRange() 方法

    @Override
    public int computeVerticalScrollRange(RecyclerView.State state) {
        return computeScrollRange(state);
    }

    private int computeScrollRange(RecyclerView.State state) {
        if (getChildCount() == 0) {
            return 0;
        }
        ensureLayoutState();
        return ScrollbarHelper.computeScrollRange(state, mOrientationHelper,
                findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true),
                findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true),
                this, mSmoothScrollbarEnabled);
    }    

最终是调用 ScrollbarHelper.computeScrollRange() 方法,几个参数前面已经解释过,这里直接看这个方法

    static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
            View startChild, View endChild, RecyclerView.LayoutManager lm,
            boolean smoothScrollbarEnabled) {
        if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
                || endChild == null) {
            return 0;
        }
        if (!smoothScrollbarEnabled) {
            return state.getItemCount();
        }
        // smooth scrollbar enabled. try to estimate better.
        final int laidOutArea = orientation.getDecoratedEnd(endChild)
                - orientation.getDecoratedStart(startChild);
        final int laidOutRange = Math.abs(lm.getPosition(startChild)
                - lm.getPosition(endChild))
                + 1;
        // estimate a size for full list.
        return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
    }

直接看最后一行,(float) laidOutArea / laidOutRange 计算的是界面显示的 Children 的平均高度。state.getItemCount() 的值为 mAdapter.getItemCount()。 那么返回值就不言而喻了,返回的是所有需要绘制的 View 的高度。然而,如果 Children 的高度并不是一样的,这个算法是不是有点欠妥?

现在回到 updateScrollPosition() 方法的第三行代码,mRecyclerViewHeight 在第一次绘制 ItemDecoration 的时候就赋值了,为 mRecyclerView.getHeight()

updateScrollPosition() 方法第四行,变量 mNeedVerticalScrollbar 决定是否需要绘制 FastScroller,需要两个条件

  1. verticalContentLength - verticalVisibleLength > 0,也就是说需要绘制内容的区域要大于 RecyclerView 的高度。
  2. mRecyclerViewHeight >= mScrollbarMinimumRangemScrollbarMinimumRange 是系统提供的值,而RecyclerView 的高度要大于这个值。所以说,RecyclerView 的高度不要设置太小了,不然就不会出现 FastScroller

所以,如果不满足这其中一个条件,是绘制不出来 FastScroller 的。

然后再看 updateScrollPosition() 方法第十八行的 if 结构体

        if (mNeedVerticalScrollbar) {
            float middleScreenPos = offsetY + verticalVisibleLength / 2.0f;
            mVerticalThumbCenterY =
                (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength);
            mVerticalThumbHeight = Math.min(verticalVisibleLength,
                (verticalVisibleLength * verticalVisibleLength) / verticalContentLength);
        }

这里计算了滚动条的中心位置 mVerticalThumbCenterY 和 滚动条的高度 mVerticalThumbHeight。这里为何要这么计算,原理如下图
这里写图片描述

AB 代表 RecyclerView 高度,也就是 verticalVisibleLength

AC 代表所有内容的高度,也就是 verticalContentLength

那么 AD 代表什么呢? 也是 verticalVisibleLength,为什么呢? 因为如果把 verticalContentLength 当做一个整体,那么 verticalVisibleLength 是不是就是它的旁边的滚动条。 那么 D 点的位置会一直移动到 C 点位置。

那么同时,在 AB 上也要取一点,我命名为 X ,那么 AX 就要代表需要绘制的 FastScroller 的高度。

在 D 点到达 C 的时候,X 点也要达到 B 点。那么联想到几何图形的知识,有一个公式就出来了 AD / AC = AX / AB,所以 AX = AD * AB / AC 也就是 (verticalVisibleLength * verticalVisibleLength) / verticalContentLength,这就是 mVerticalThumbHeight 的值了,如下图

这里写图片描述

现在已经找到了 X 点,那么在移动中的 FastScroller 中心点的位置如何确认呢? 如下图

这里写图片描述

E 为 AX 的中点,那么等比地可以在 AD 上找到一个中点 F,这个滑动中的 F 坐标怎么计算呢? 假如现在滑动了一段距离,如下图

这里写图片描述
A2X 代表了 FastScroller 高度, A1D 代表了 verticalVisibleLength。 E,F 分别为 A2X 和 A1D 的中点。

那么 F 点坐标等于 AA1 + A1F,A1F 等于 verticalVisibleLength / 2 ,那么 AA1 代表什么呢?代表的就是 RecyclerView 在 Y 轴滑动的偏移量。也就是代码中的 offsetY

所以根据几何图形学的知识,你应该就能推导出 mVerticalThumbCenterY 吧?

原理理解后,最后看下 updateScrollPosition() 方法的最后几行

        if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) {
            setState(STATE_VISIBLE);
        }

mState 有三个值 STATE_HIDDEN,STATE_VISIBLE,STATE_DRAGGING。 而这里可以看到,隐藏或者可见的状态都调用了 setState(STATE_VISIBLE),也就是说,只要不是拖拽 FastScroller,我在滑动 RecyclerView 的时候,一直调用 setState(STATE_VISIBLE) 执行动画让 FastScroller 透明度一直到 255。

    private void setState(@State int state) {
        if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
            mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
            cancelHide();
        }

        if (state == STATE_HIDDEN) {
            requestRedraw();
        } else {
            show();
        }

        if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
            mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
            resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
        } else if (state == STATE_VISIBLE) {
            resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
        }
        mState = state;
    }

做了三件事
1. 调用 show() 方法来显示 FastScroller
2. 调用 resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS) ,在 1500ms 后执行隐藏动画。
3. mState = state 重新设置 mState 的状态

首先看下 show() 方法

    private final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);
    @AnimationState private int mAnimationState = ANIMATION_STATE_OUT;

    public void show() {
        switch (mAnimationState) {
            case ANIMATION_STATE_FADING_OUT:
                mShowHideAnimator.cancel();
                // fall through
            case ANIMATION_STATE_OUT:
                mAnimationState = ANIMATION_STATE_FADING_IN;
                mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1);
                mShowHideAnimator.setDuration(SHOW_DURATION_MS);
                mShowHideAnimator.setStartDelay(0);
                mShowHideAnimator.start();
                break;
        }
    }

mAnimationState 的初始值为 ANIMATION_STATE_OUT,这里做了两件事
1. 设置 mAnimationState 状态为 ANIMATION_STATE_FADING_IN 代表正在显示
2. 执行动画 mShowHideAnimator.start();

在构造函数中,为 mShowHideAnimator 设置过两个 Listener,第一个是 AnimatorListener 用来监听动画的结束以及取消。第二个是 AnimatorUpdater 用来监听动画的进度。

那么先看 AnimatorUpdater

    private class AnimatorUpdater implements AnimatorUpdateListener {

        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue()));
            mVerticalThumbDrawable.setAlpha(alpha);
            mVerticalTrackDrawable.setAlpha(alpha);
            requestRedraw();
        }
    }

alpha 的值是 0 ~ 255 范围,然后有意思的事情是这里只动态设置了 mVerticalThumbDrawablemVerticalTrackDrawable 的透明度,然后让 RecyclerView 重新绘制。 我就很纳闷了,水平方向的呢?

ok,当动画结束或者取消的时候,就需要看看 AnimatorListener

    private class AnimatorListener extends AnimatorListenerAdapter {

        private boolean mCanceled = false;

        @Override
        public void onAnimationEnd(Animator animation) {
            // Cancel is always followed by a new directive, so don't update state.
            if (mCanceled) {
                mCanceled = false;
                return;
            }
            if ((float) mShowHideAnimator.getAnimatedValue() == 0) {
                mAnimationState = ANIMATION_STATE_OUT;
                setState(STATE_HIDDEN);
            } else {
                mAnimationState = ANIMATION_STATE_IN;
                requestRedraw();
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            mCanceled = true;
        }
    }

如果取消了,会调用 onAnimationCancel()onAnimationEnd(),可以看到,其实没做啥事情,除了设置 mCanceledtrue。那么什么时候会取消,当然是这 mShowHideAnimator 又重新 start() 了,实际中的情况就是,当 FastScroller 正在透明度正在变为 0 的时候,也就是执行隐藏动画的时候,你又滑动了 RecyclerView 或者拖拽了 FastScroller

而如果正常结束了,就需要通过 mShowHideAnimator.getAnimatedValue() 获取结束后的值来进行不同的动作

  1. 如果等于0,代表隐藏了 FastScroller,那么 mAnimationState 设置为 ANIMATION_STATE_OUT,然后调用 setState() 重置 mState 的状态并且重新绘制
  2. 如果不等于0,那就是1,代表显示,那么把 mAnimationState 设置为 ANIMATION_STATE_IN,然后再进行重新绘制。

搞了这么多事情,其实是为了设置状态,并且为重新绘制做准备,那么,就需要再次进入到 onDrawOver()

    @Override
    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
        if (mRecyclerViewWidth != mRecyclerView.getWidth()
                || mRecyclerViewHeight != mRecyclerView.getHeight()) {
            mRecyclerViewWidth = mRecyclerView.getWidth();
            mRecyclerViewHeight = mRecyclerView.getHeight();
            setState(STATE_HIDDEN);
            return;
        }

        if (mAnimationState != ANIMATION_STATE_OUT) {
            if (mNeedVerticalScrollbar) {
                drawVerticalScrollbar(canvas);
            }
            if (mNeedHorizontalScrollbar) {
                drawHorizontalScrollbar(canvas);
            }
        }
    }

从代码中第二个 if 语句可以看出,mAnimationState 只有在非 ANIMATION_STATE_OUT 状态下才会进行绘制 FastScroller。那么,直接看下 drawVerticalScrollbar() 方法

    private void drawVerticalScrollbar(Canvas canvas) {
        int left = viewWidth - mVerticalThumbWidth;
        int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2;
        mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight);
        mVerticalTrackDrawable
            .setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight);

        if (isLayoutRTL()) {
            // ...
        } else {
            canvas.translate(left, 0);
            mVerticalTrackDrawable.draw(canvas);
            canvas.translate(0, top);
            mVerticalThumbDrawable.draw(canvas);
            canvas.translate(-left, -top);
        }
    }

到这里终于看到了绘制的操作了,这个就不用解释了~

FastScroller 已经显示出来,现在要分析的就是 FastScroller 的触摸事件处理。 在构造函数的中,做过如下设置

mRecyclerView.addOnItemTouchListener(this);

FastScroller 实现了 onInterceptTouchEvent(),onTouchEvent(),而 onRequestDisallowInterceptTouchEvent() 实现的是个空方法。

首先看 onInterceptTouchEvent(),这个函数的作用是,当我们触摸点处于 FastScroller 区域的时候,截断事件。

    public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent ev) {
        final boolean handled;
        // 当滚动条处理可见状态
        if (mState == STATE_VISIBLE) {
            boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY());
            boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY());
            if (ev.getAction() == MotionEvent.ACTION_DOWN
                    && (insideVerticalThumb || insideHorizontalThumb)) {
                if (insideHorizontalThumb) {
                    mDragState = DRAG_X;
                    mHorizontalDragX = (int) ev.getX();
                } else if (insideVerticalThumb) {
                    mDragState = DRAG_Y;
                    mVerticalDragY = (int) ev.getY();
                }

                setState(STATE_DRAGGING);
                handled = true;
            } else {
                handled = false;
            }
        } else if (mState == STATE_DRAGGING) {
            handled = true;
        } else {
            handled = false;
        }
        return handled;
    }

onInterceptTouchEvent() 用来判断是否截断 RecyclerViewItem Touch 事件。

从代码中可以看出,当处于拖拽状态,也就是 mState == STATE_DRAGGING , 是就截断。

另外一种情况就是,当 FastScroller 处于可见状态,也就是 mState == STATE_VISIBLE,手指在 FastScroller 上按下的时候,也就是 ev.getAction() == MotionEvent.ACTION_DOWN &&insideVerticalThumb 也是要截断事件的。这种情况下回调用 setState(STATE_DRAGGING) 方法,看下代码

    private void setState(@State int state) {
        if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
            // STEP1:为 drawable 设置了 state_pressed 状态
            mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
            // STEP2:如果正在隐藏就取消
            cancelHide();
        }

        if (state == STATE_HIDDEN) {
            requestRedraw();
        } else {
            // STEP3:再次显示
            show();
        }

        if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
            mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
            resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
        } else if (state == STATE_VISIBLE) {
            resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
        }
        // STEP4: 重置 mState 状态值
        mState = state;
    }

分为了四步

  1. mVerticalThumbDrawable 设置了 pressed 状态。 因为 mVerticalThumbDrawableStateListDrawable 类型,因此可以根据这个状态显示不同的 Drawable
  2. 调用 cancelHide(),这是为了防止 FastScroller 将要执行隐藏的动画的 Runnable,需要提前取消。
  3. 调用 show() 显示 FastScroller
  4. 重置 mState 的状态为 STATE_DRAGGING

截断事件后,就进入到 onToucheEvent() 方法

    @Override
    public void onTouchEvent(RecyclerView recyclerView, MotionEvent me) {
        if (mState == STATE_HIDDEN) {
            return;
        }

        if (me.getAction() == MotionEvent.ACTION_DOWN) {
            boolean insideVerticalThumb = isPointInsideVerticalThumb(me.getX(), me.getY());
            boolean insideHorizontalThumb = isPointInsideHorizontalThumb(me.getX(), me.getY());
            if (insideVerticalThumb || insideHorizontalThumb) {
                if (insideHorizontalThumb) {
                    mDragState = DRAG_X;
                    mHorizontalDragX = (int) me.getX();
                } else if (insideVerticalThumb) {
                    mDragState = DRAG_Y;
                    mVerticalDragY = (int) me.getY();
                }
                setState(STATE_DRAGGING);
            }
        } else if (me.getAction() == MotionEvent.ACTION_UP && mState == STATE_DRAGGING) {
            mVerticalDragY = 0;
            mHorizontalDragX = 0;
            setState(STATE_VISIBLE);
            mDragState = DRAG_NONE;
        } else if (me.getAction() == MotionEvent.ACTION_MOVE && mState == STATE_DRAGGING) {
            show();
            if (mDragState == DRAG_X) {
                horizontalScrollTo(me.getX());
            }
            if (mDragState == DRAG_Y) {
                verticalScrollTo(me.getY());
            }
        }
    }

onInterceptTouchEvent() 如果返回 true 就代表了截断事件,所以事件会传到 onTouchEvent() 方法中,其中 ACTION_DOWN 的处理方式大致是一样的,ACTION_UP 也比较简单,重点看的就是 ACTION_MOVE,首先会执行 show() 这个前面已经分析过,然后执行了 verticalScrollTo(me.getY())

    private void verticalScrollTo(float y) {
        final int[] scrollbarRange = getVerticalRange();
        y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y));
        if (Math.abs(mVerticalThumbCenterY - y) < 2) {
            return;
        }
        int scrollingBy = scrollTo(mVerticalDragY, y, scrollbarRange,
                mRecyclerView.computeVerticalScrollRange(),
                mRecyclerView.computeVerticalScrollOffset(), mRecyclerViewHeight);
        if (scrollingBy != 0) {
            mRecyclerView.scrollBy(0, scrollingBy);
        }
        mVerticalDragY = y;
    }

首先调用getVerticalRange()来获取滚动的范围

    /**
     * Gets the (min, max) vertical positions of the vertical scroll bar.
     */
    private int[] getVerticalRange() {
        mVerticalRange[0] = mMargin;
        mVerticalRange[1] = mRecyclerViewHeight - mMargin;
        return mVerticalRange;
    }

从注释中可以看出,数组的2个值分别最大值和最小值。

然后触摸点的 Y 坐标值就被限制在这个范围,也就是 y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y)),这个是比较切实际的算法,因为手指可能并不会极限的触碰到顶部或者底部坐标。

然后出现个 if 语句,判断 Math.abs(mVerticalThumbCenterY - y) < 2,请原谅我真的没看懂~

然后最主要的就是计算 RecyclerView 需要位移的距离,也就是 scrollTo() 方法

    private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange,
            int scrollOffset, int viewLength) {
        int scrollbarLength = scrollbarRange[1] - scrollbarRange[0];
        if (scrollbarLength == 0) {
            return 0;
        }
        float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength);
        int totalPossibleOffset = scrollRange - viewLength;
        int scrollingBy = (int) (percentage * totalPossibleOffset);
        int absoluteOffset = scrollOffset + scrollingBy;
        if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) {
            return scrollingBy;
        } else {
            return 0;
        }
    }

这段代码,我们来好好品味下。

  1. 首先,在之前,把触碰点的 Y 坐标限制在 mVerticalRange[0]mVerticalRange[1] 之间。本身设计很人性化,不过接着往下看。
  2. scrollbarLength 虽然名字叫 scrollbar length,但是实际指的是手指在 Y 轴滑动的最大距离。
  3. percentage 为滑动的百分比,没什么问题。
  4. totalPossibleOffset 为内容区域总共需要滑动的最大偏移量,没问题
  5. 根据 percentage 计算出了 scrollingBy ,也就是 RecyclerView 内容区域需要滑动的偏移量。but,pay attenation! 这里做了强制转换,这就可能丢失精度,也就会导致内容区域无法滑动到底部。
  6. 根据 scrollingBy 和传入进来的 scrollOffset 参数计算出来了 absoluteOffset。不过又需要注意参数 scrollOffset 是进行过四舍五入的。 所以这个计算出来的 absoluteOffset 并不精确。 既然并不精确,那么后面用 absoluteOffset 做判断是不是有失水准? 这就可能导致内容区域无法滑动到底部问题。

计算出来了需要位移的距离 scrollingBy 后,verticalScrollTo() 方法就调用了 mRecyclerView.scrollBy(0, scrollingBy),在这个方法里面有如下这段代码的调用

        if (!mItemDecorations.isEmpty()) {
            invalidate();
        }

这样就会导致 ItemDecoration 被重新绘制,那么 Scrollbar 的位置就会得到相应的更新,原理与监测 RecyclerView 滚动来更新 FastScroller 的位置一样。

发布了44 篇原创文章 · 获赞 30 · 访问量 400万+

猜你喜欢

转载自blog.csdn.net/zwlove5280/article/details/78616886
今日推荐