您的位置:首页 > 其它

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
的位置一样。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: