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
的位置一样。