RecyclerView 的 FastScroller 绘制的源码分析
2017-11-23 17:13
429 查看
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的位置一样。
相关文章推荐
- 从Android源码分析View绘制
- android应用程序窗口框架学习(2)-view绘制流程源代码解析-setContentView与LayoutInflater加载解析机制源码分析
- 安卓5.1源码解析 : RecyclerView解析从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析
- Android View 绘制流程 与invalidate 和postInvalidate 分析--从源码角度
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- RecyclerView源码分析
- Android应用层View绘制流程与源码分析
- View的绘制流程源码分析(Android开发艺术探索学习笔记)
- Android系统分析之View绘制流程与源码分析
- Android 中View的绘制机制源码分析 三
- RecyclerView 源码分析
- android view绘制源码分析
- RecyclerView系列之二:从源码中分析为RecyclerView添加分割线
- (二十八)RecyclerView ItemTouchHelper 源码分析以及拓展
- View的源码分析(绘制流程以及刷新机制)
- Android recyclerview源码分析(一)
- 从RecyclerView、NestedScrollView源码分析嵌套滑动异常
- Android应用层View绘制流程与源码分析