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的高度必须要大于这个值才能绘制FastScrollerR.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中RecyclerView FastScroller的工作原理,包括其构造、显示逻辑、触摸事件处理及与RecyclerView滑动的联动机制。
830

被折叠的 条评论
为什么被折叠?



