RecyclerView 的 FastScroller 绘制的源码分析
来源:互联网 发布:matlab画数据分布图 编辑:程序博客网 时间:2024/06/03 19:40
RecyclerView
以前一直被人诟病没有 FastScroller
的功能,然后网上出现了几种解决方法
- 继承
RecyclerView
,重写draw()
方法,绘制FastScroller
- 单独自定义一个
View
,然后传入RecyclerView
作为参数。
第一种方法沿用了 ListView
的思维,把 FastScroller
和 RecyclerView
绘制在一起,耦合度过高,如果代码写的不好,容易出问题。
第二种方法,虽然解决了耦合度高的问题,但是没有充分发挥 RecyclerView
的优势。
那么 Google 看不下去了,自己加入了 FastScroller
功能,既解耦,又充分利用了 RecyclerView
优势,它的实现方式是继承 ItemDecoration
class FastScroller extends ItemDecoration
不过个人认为这个功能并不那么好用,主要有一下几点
ListView
的Adapter
如果实现了SectionIndexer
接口,那么ListView
会在ScrollBar
的左侧展示一个气泡形状的Index
, 而RecyclerView
的FastScroller
并没有完善这个功能。- 使用起来复杂
- 没有处理
ViewHolder.itemView
高度不一致的情况 - 使用效果并不好。
带着这些问题,让我们一起从源码解读这个 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:fastScrollVerticalThumbDrawable
和 android:fastScrollHorizontalThumbDrawable
要为 StateListDrawable
类型,android:fastScrollVerticalTrackDrawable
和 android:fastScrollHorizontalTrackDrawable
要为 Drawable
类型。
看到这里,我想大家心里跟我一样会有一个疑问,那就是如果我只需要绘制垂直方向的 FastScroller
,那么为何还要设置水平方向的 FastScroller
属性呢? 而且设置属性的时候对 Drawable
类型还有特殊要求!这就是我之前说过的 FastScroller
使用起来复杂的问题。
initFastScroller()
方法的最后,new
了一个 FastScroller()
,其中注意下它的最后三个参数
R.dimen.fastscroll_default_thickness
为FastScroller
默认宽度R.dimen.fastscroll_minimum_range
:RecyclerView
的高度必须要大于这个值才能绘制FastScroller
R.dimen.fastscroll_margin
为手指在FastScroller
滑动范围的topMargin
和bottomMargin
。
现在进入 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
,而其中需要关注的是 mVerticalThumbWidth
和 mVerticalTrackWidth
的值,取的是默认值和 Drawable
的实际宽度的最大值。一般取系统的默认宽度即可,而如果需要,就要自己设置 Drawbale
的宽度。
接着把 mVerticalThumbDrawable
和 mVerticalTrackDrawable
的透明度设置为 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()
方法做了三件事
- 把当前的
ItemDecoration
也就是FastScroller
, 添加到RecyclerView
中 - 为
RecyclerView
添加onItemTouchListener
,用于在触摸FastScroller
的时候,截断并处理MotionEvent
- 为
RecyclerView
添加onScrollListener
,用于检测RecyclerView
的滑动,决定是否显示FastScroller
构造方法分析完了,那么首先要分析的情况就是界面刚显示的时候,这个时候 RecyclerView
会绘制 ItemDecoration
,而 FastScroller
也就理所当然要绘制。而FastScroller
只复写了 ItemDecoration
的 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(); // 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); } } }
首先,mRecyclerViewWidth
和 mRecyclerViewHeight
初始化都为 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()
只做了两件事
- 调用了
requestRedraw()
让RecyclerView
进行重新绘制 - 设置
mState
为STATE_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()
方法,其中两个参数分别 RecyclerView
在 X
和 Y
方向的偏移量,因为现在只关心垂直的 FastScroller
,X
方向偏移量为 0,所以直接看 recyclerView.computeVerticalScrollOffset()
方法是如何计算垂直的偏移量
public int computeVerticalScrollOffset() { if (mLayout == null) { return 0; } return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0; }
从 return
那一行代码可以看出,如果 LayoutManager
可以垂直滑动,也就是 mLayout.canScrollVertically()
返回 true
,那么就用 LayoutManager
的 computeVerticalScrollOffset()
方法来计算垂直方向滑动的偏移量,这里以 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); }
最终是调用了 ScrollbarHelper
的 computeScrollOffset()
方法来计算的,不过在看这个方法之前,首先需要知道它的几个参数。
通过上一篇文章的分析,可以知道参数 state
,mOrientationHelper
的一些状态值,以及 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
造成的 padding
和 Child
本身的 bottomMargin
)减去第一个显示的 Child
的顶部坐标(包括 ItemDecoration
造成的 padding
和 Child
本身的 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
值是 0
,offsetY
就是刚才计算出来的。
变量 verticalContentLength
是代表 ReyclerView
实际需要显示所有 View
的高度,调用的是 ReyclerView
的 computeVerticalScrollRange()
方法
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
,需要两个条件
verticalContentLength - verticalVisibleLength > 0
,也就是说需要绘制内容的区域要大于RecyclerView
的高度。mRecyclerViewHeight >= mScrollbarMinimumRange
,mScrollbarMinimumRange
是系统提供的值,而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 范围,然后有意思的事情是这里只动态设置了 mVerticalThumbDrawable
和 mVerticalTrackDrawable
的透明度,然后让 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()
,可以看到,其实没做啥事情,除了设置 mCanceled
为 true
。那么什么时候会取消,当然是这 mShowHideAnimator
又重新 start()
了,实际中的情况就是,当 FastScroller
正在透明度正在变为 0 的时候,也就是执行隐藏动画的时候,你又滑动了 RecyclerView
或者拖拽了 FastScroller
。
而如果正常结束了,就需要通过 mShowHideAnimator.getAnimatedValue()
获取结束后的值来进行不同的动作
- 如果等于0,代表隐藏了
FastScroller
,那么mAnimationState
设置为ANIMATION_STATE_OUT
,然后调用setState()
重置mState
的状态并且重新绘制 - 如果不等于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()
用来判断是否截断 RecyclerView
的 Item 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; }
分为了四步
- 为
mVerticalThumbDrawable
设置了pressed
状态。 因为mVerticalThumbDrawable
是StateListDrawable
类型,因此可以根据这个状态显示不同的Drawable
- 调用
cancelHide()
,这是为了防止FastScroller
将要执行隐藏的动画的Runnable
,需要提前取消。 - 调用
show()
显示FastScroller
- 重置
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; } }
这段代码,我们来好好品味下。
- 首先,在之前,把触碰点的
Y
坐标限制在mVerticalRange[0]
和mVerticalRange[1]
之间。本身设计很人性化,不过接着往下看。 scrollbarLength
虽然名字叫scrollbar length
,但是实际指的是手指在Y
轴滑动的最大距离。percentage
为滑动的百分比,没什么问题。totalPossibleOffset
为内容区域总共需要滑动的最大偏移量,没问题- 根据
percentage
计算出了scrollingBy
,也就是RecyclerView
内容区域需要滑动的偏移量。but,pay attenation! 这里做了强制转换,这就可能丢失精度,也就会导致内容区域无法滑动到底部。 - 根据
scrollingBy
和传入进来的scrollOffset
参数计算出来了absoluteOffset
。不过又需要注意参数scrollOffset
是进行过四舍五入的。 所以这个计算出来的absoluteOffset
并不精确。 既然并不精确,那么后面用absoluteOffset
做判断是不是有失水准? 这就可能导致内容区域无法滑动到底部问题。
计算出来了需要位移的距离 scrollingBy
后,verticalScrollTo()
方法就调用了 mRecyclerView.scrollBy(0, scrollingBy)
,在这个方法里面有如下这段代码的调用
if (!mItemDecorations.isEmpty()) { invalidate(); }
这样就会导致 ItemDecoration
被重新绘制,那么 Scrollbar
的位置就会得到相应的更新,原理与监测 RecyclerView
滚动来更新 FastScroller
的位置一样。
- RecyclerView 的 FastScroller 绘制的源码分析
- 源码分析android的UI绘制流程
- RecyclerView 的源码浅析
- Android RecyclerView下拉刷新的实现和源码分析
- 谈谈RecyclerView的LayoutManager-LinearLayoutManager源码分析
- 研究ListView里的FastScroller的一点心得
- recyclerview的应用分析
- Android 中View的绘制机制源码分析 三
- Android 中View的绘制机制源码分析 一
- Android 中View的绘制机制源码分析 二
- Android 中View的绘制机制源码分析 四
- 源码分析Android中View的绘制流程
- View的源码分析(绘制流程以及刷新机制)
- Android 中View的绘制机制源码分析
- RecyclerView源码分析
- RecyclerView源码分析
- RecyclerView滑动源码分析
- Android RecyclerView源码分析
- 『MySQL』索引类型 normal, unique, full text
- python中的实例方法、静态方法、类方法、类变量和实例变量浅析
- 【Scikit-Learn 中文文档】多类和多标签算法
- 计算机网络第一章概述(1)
- MyEclipse使用总结——MyEclipse10安装SVN插件
- RecyclerView 的 FastScroller 绘制的源码分析
- angular4表单—模板式表单
- Win10 安装Ant运行build.xml遇到的问题以及解决
- 【Scikit-Learn 中文文档】特征选择
- Storyboard References
- RadioButton和CheckBox
- 如何优雅的打破NSTimer与控制器的循环引用
- 受限玻尔兹曼机
- oracle wm_concat函数的使用