深入理解 RecyclerView 的绘制流程和滑动原理,android 应用开发教程答案
在onLayout()
方法中, 直接调用dispatchLayout()
方法布局:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout(); //直接调用 dispatchLayout()方法布局
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
dispatchLayout()
是layoutChildren()
的包装器,它处理由布局引起的动态变化:
void dispatchLayout() {
······
mState.mIsMeasuring = false;//设置 RecyclerView 布局完成状态,前面已经设置预布局完成了。
if (mState.mLayoutStep == State.STEP_START) {//如果没在 OnMeasure 阶段提前测量子 ItemView
dispatchLayoutStep1();//布局第一步:适配器更新、动画运行、保存当前视图的信息、运行预测布局
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {//前两步完成测量,但是因为大小改变不得不再次运行下面的代码
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();//布局第二步:最终实际的布局视图,如果有必要会多次运行
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();//布局第三步:最后一步的布局,保存视图动画、触发动画和不必要的清理。
}
可以看到dispatchLayout()
和onMeasure()
阶段中一样选择性地进行测量布局的三个步骤:
1、如果没在 onMeasure 阶段提前测量子 ItemView,即 RecyclerView 宽高为
match_parent
或者精确值时,调用dispatchLayoutStep1()
和dispatchLayoutStep2()
测量 itemView 宽高;2、如果在 onMeasure 阶段提前测量子 ItemView,但是子视图发生了改变或者期望宽高和实际宽高不一致,则会调用
dispatchLayoutStep2()
重新测量;3、最后都会执行
dispatchLayoutStep3()
方法。
(1)我们来看看 dispatchLayoutStep1、2、3 分发布局的三个步骤:dispatchLayoutStep1()
主要是进行预布局,适配器更新、动画运行、保存当前视图的信息等工作;
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
startInterceptRequestLayout();//拦截布局请求
mViewInfoStore.clear();//itemView 信息清除
onEnterLayoutOrScroll();
//测量和分派布局时,更新适配器和计算那种类型要运行的动画
processAdapterUpdatesAndSetAnimationFlags();
saveFocusInfo();//保存焦点信息
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);//找到可绘制 itemView 最小最大 position
if (mState.mRunSimpleAnimations) {
//获得界面上可以显示的个数
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
//动画信息
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
//保存 holder 和动画信息到预布局中
mViewInfoStore.addToPreLayout(holder, animationInfo);
}
}
//运行与布局,将会使用旧的 item 的 position,布局管理器布局所有
if (mState.mRunPredictiveAnimations) {
//保存旧的管理器可以运行的逻辑
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
//布局 itemView
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
}
stopInterceptRequestLayout(false);//回复绘制锁定
mState.mLayoutStep = State.STEP_LAYOUT;
}
(2)dispatchLayoutStep2()
表示对最终状态的视图进行实际布局:
private void dispatchLayoutStep2() {
startInterceptRequestLayout();//拦截请求布局
onEnterLayoutOrScroll();
//设置布局状态和动画状态
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
//预布局完成,开始布局 itemView
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
······
stopInterceptRequestLayout(false);//停止拦截布局请求
}
(3)dispatchLayoutStep3()
是布局的最后一步,保存 view 的动画信息,执行动画,和一些必要的清理工作:
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();//开始拦截布局请求
mState.mLayoutStep = State.STEP_START;//布局开始状态
if (mState.mRunSimpleAnimations) {
//步骤 3:找出事情现在的位置,并处理更改动画。
//反向遍历列表,因为我们可能会在循环中调用 animateChange,这可能会删除目标视图持有者。
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
//运行一个变更动画。如果一个项目被更改,但是更新后的版本正在消失,则会产生冲突的情况。
//由于标记为正在消失的视图可能会超出界限,所以我们运行一个 change 动画。两个视图都将在动画完成后自动清除。
//另一方面,如果是相同的视图持有者实例,我们将运行一个正在消失的动画,因为我们不会重新绑定更新的 VH,除非它是由布局管理器强制执行的。
//运行消失动画而不是改变
mViewInfoStore.addToPostLayout(holder, animationInfo);
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder);
//我们添加和删除,这样任何的布置信息都是合并的
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
//处理视图信息列表和触发动画
mViewInfoStore.process(mViewInfoProcessCallback);
}
//回收废弃的视图
mLayout.removeAndRecycleScrapInt(mRecycler);
//重置状态
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
//清除 mChangedScrap 中的数据
mRecycler.mChangedScrap.clear();
mRecycler.updateViewCacheSize();//更新缓存大小
mLayout.onLayoutCompleted(mState);//布局完成状态
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);//停止拦截布局请求
mViewInfoStore.clear();//itemView 信息清除
recoverFocusFromState();//回复焦点
resetFocusInfo();//重置焦点信息
}
归纳分发布局的三个步骤:
dispatchLayoutStep1():??表示进行预布局,适配器更新、动画运行、保存当前视图的信息等工作;
dispatchLayoutStep2():??表示对最终状态的视图进行实际布局,有必要时会多次执行;
dispatchLayoutStep3():??表示布局最后一步,保存和触发有关动画的信息,相关清理等工作。
[](
)1.3 onDraw()
来到最后一步的绘制onDraw()
方法中,如果不需要一些特殊的效果,在 TextView、ImageView 控件中已经绘制完了。
@Override
public void onDraw(Canvas c) {
super.onDraw(c);//所有 itemView 先绘制
//分别绘制 ItemDecoration
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
[](
)1.4 绘制流程总结:
1、RecyclerView 的 itemView 可能会被测量多次,如果 RecyclerView 的宽高是固定值或者 match_parent,那么在onMeasure()
阶段是不会提前测量 ItemView 布局,如果 RecyclerView 的宽高是 wrap_content,由于还没有知道 RecyclerView 的实际宽高,那么会提前在onMeasure()
阶段遍历测量 itemView 布局确定内容显示区域的宽高值来确定 RecyclerView 的实际宽高;
2、dispatchLayoutStep1()
、 dispatchLayoutStep2()
、 dispatchLayoutStep3()
这三个方法一定会执行,在 RecyclerView 的实际宽高不确定时,会提前多次执行dispatchLayoutStep1()
、 dispatchLayoutStep2()
方法,最后在onLayout()
阶段执行 dispatchLayoutStep3()
,如果有 itemView 发生改变会再次执行dispatchLayoutStep2()
;
3、正在的测量和布局 itemView 实际在dispatchLayoutStep2()
方法中。
RecyclerView 的绘制三个步骤流程图:
[](
)2、LinearLayoutManager 填充、测量、布局过程
RecyclerView 的绘制经过 measure、layout、draw 三个步骤,但是 itemView 的真正布局时委托给各个的 LayoutManager 中处理,上面 LinearLayoutManager 可以知道dispatchLayoutStep2()
是实际布局视图步骤,通过 LayoutManager 调用onLayoutChildren()
方法进行布局 itemView,它是绘制 itemView 的核心方法,表示从给定的适配器中列出所有相关的子视图。
[](
)2.1 onLayoutChildren()布局 itemView
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1) 检查子类和其他变量找到描点坐标和描点位置
// 2) 从开始填补,从底部堆积
// 3) 从底部填补,从顶部堆积
// 4) 从底部堆积来满足需求
// 创建布局状态
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);//移除所有子 View
return;
}
}
ensureLayoutState();
mLayoutState.mRecycle = false;//禁止回收
//颠倒绘制布局
resolveShouldLayoutReverse();
final View focused = getFocusedChild();//获取目前持有焦点的 child
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();//重置锚点信息
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
//1. 计算更新描点位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
·······
//计算第一布局的方向
int startOffset;
int endOffset;
final int firstLayoutDirection;
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);//暂时分离已经附加的 view,即将所有 child detach 并通过 Scrap 回收
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
//2.开始填充,从底部开始堆叠;
if (mAnchorInfo.mLayoutFromEnd) {
//描点位置从 start 位置开始填充 ItemView 布局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有 itemView
//描点位置从 end 位置开始填充 ItemView 布局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);//填充所有 itemView
endOffset = mLayoutState.mOffset;
}else { //3.向底填充,从上往下堆放;
//描点位置从 end 位置开始填充 ItemView 布局
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
//描点位置从 start 位置开始填充 ItemView 布局
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
//4.计算滚动偏移量,如果有必要会在调用 fill 方法去填充新的 ItemView
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
}
首先是状态判断和一些准备工作,对描点信息选择和更新, detachAndScrapAttachedViews(recycler)
暂时将已经附加的 view 分离,缓存 Scrap 中,下次重新填充时直接拿出来复用。然后计算是从哪个方向开始布局。布局算法如下:
1.通过检查子元素和其他变量,找到一个锚点坐标和一个锚点项的位置;
2.开始填充,从底部开始堆叠;
3.向底填充,从上往下堆放;
4.滚动以满足要求,如堆栈从底部。
[](
)2.2 fill()开始填充 itemView
填充布局交给了fill()
方法,表示填充由 layoutState 定义的给定布局。为什么要 fill 两次呢?我们来看看fill()
方法:
//填充方法,返回的是填充 itemView 的像素,方便后续滚动时使用
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
recycleByLayoutState(recycler, layoutState);//回收滑出屏幕的 view
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//核心 == while()循环 ==
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循环,知道没有数据
layoutChunkResult.resetInternal();
//填充 itemView 的核心方法
layoutChunk(recycler, state, layoutState, layoutChunkResult);
······
if (layoutChunkResult.mFinished) {//布局结束,退出循环
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;//根据添加的 child 高度偏移计算
}
······
return start - layoutState.mAvailable;//返回这次填充的区域大小
}
fill()
核心就是一个 while()循环,循环执行layoutChunk()
填充一个 itemView 到屏幕,同时返回这次填充的区域大小。首先根据屏幕还有多少剩余空间 remainingSpace,根据这个数值减去子 View 所占的空间大小,小于 0 时布局子 View 结束,如果当前所有子 View 还没有超过 remainingSpace 时,调用layoutChunk()
安排 View 的位置。
[](
)2.3 layoutChunk()对 itemView 创建、填充、测量、布局
layoutChunk()
作为最终填充布局 itemView 的方法,对 itemView 创建、填充、测量、布局,主要有以下几个步骤:
1.
layoutState.next(recycler)
从缓存中获取 itemView,如果没有则创建 itemView;2.根据实际情况来添加 itemView 到 RecyclerView 中,最终调用的还是 ViewGroup 的
addView()
方法;3.
measureChildWithMargins()
测量 itemView 大小包括父视图的填充、项目装饰和子视图的边距;4.根据计算好的 left, top, right, bottom 通过
layoutDecoratedWithMargins()
使用坐标在 RecyclerView 中布局给定的 itemView。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//1.从缓存中获取或者创建 itemView
View view = layoutState.next(recycler);//获取当前 postion 需要展示的 View
······
//2.根据实际情况来添加 itemView 到 RecyclerView 中,最终调用的还是 ViewGroup 的 addView()方法
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
//3.测量子 View 大小包括父视图的填充、项目装饰和子视图的边距
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//计算一个 ItemView 的 left, top, right, bottom 坐标值
int left, top, right, bottom;
······
//4.使用坐标在 RecyclerView 中布局给定的 itemView
//计算正确的布局位置,减去 margin,计算所有视图的边界框(包括 margin 和装饰)
layoutDecoratedWithMargins(view, left, top, right, bottom);//调用 child.layout 进行布局
}
通过layoutState.next()
从缓存中获取 itemView 如果没有就创建一个新的 itemView,然后addView()
根据实际情况来添加 itemView 到 RecyclerView 中,最终调用的还是 ViewGroup 的addView()
方法,接着通过 measureChildWithMargins()
测量子 View 大小包括父视图的填充、项目装饰和子视图的边距;最后getDecoratedMeasuredWidth()
通过计算好的 left, top, right, bottom 值在 RecyclerView 坐标中布局给定的 itemView,注意这里的宽度是 item+decoration 的总宽度。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
获取 itemView,并且如果mScrapList
中有缓存的 View 则使用缓存的 view,如果没有 mScrapList 就创建 view,并添加到 mScrapList 中。接下来getViewForPosition()
方法主要是 RecyclerView 的缓存机制,后续的文章会讲解到。
[](
)2.4 LinearLayoutManager 填充、测量、布局过程总结:
**onLayoutChildren()
表示从给定的适配器中列出所有相关的子视图,填充布局交给了fill()
方法,填充由 layoutState 定义的给定布局,while()循环执行layoutChunk()
填充一个 itemView 到屏幕,作为最终填充布局 itemView 的方法,layoutState.next(recycler)
从缓存中获取或者创建 itemView,通过 addView()添加 itemView 到 RecyclerView 中,其实最终调用的还是 ViewGroup 的addView()
方法,`me
asureChildWithMargins()测量itemView大小包括父视图的填充、项目装饰和子视图的边距,最后
layoutDecoratedWithMargins()`根据计算好的 left, top, right, bottom 通过使用坐标在 RecyclerView 中布局给定的 itemView。**
流程图如下:
[](
)二、滑动原理
====================================================================
RecyclerView 作为一个列表控件,自带滑动功能,实际开发中经常用到,它的滑动原理也是我们需要掌握的,正所谓“知其然更要知其之所然”。RecyclerView 的滑动事件处理依然是通过onTouchEvent()
触控事件响应的,不同的是 RecyclerView 采用嵌套滑动机制,会把滑动事件通知给支持嵌套滑动的父 View 先做决定。本文在介绍普通滑动的过程中可能会涉及到嵌套滑动的知识(下篇文章会分析嵌套滑动),先来看看普通滑动的效果图:
[](
)1、onTouchEvent()
RecyclerView 的事件处理依然是通过onTouchEvent()
触控事件响应的,这里补充一点onTouchEvent()
的知识,熟悉的可以略过。
boolean onTouchEvent(MotionEvent event): ?实现此方法来处理触摸屏运动事件,返回值 true 表示处理事件,false 表示不处理事件;
MotionEvent.ACTION_DOWN:?? 手指按下,一个按下的手势已经开始,该动作包括初始的起始位置;
MotionEvent.ACTION_MOVE:?? 手指移动,在按下手势时(在 down 和 up 之间)发生了改变,该运动包含最近的点,以及自上次向下或移动事件以来的任何中间点;
MotionEvent.ACTION_UP:?? ? 手指离开,一个按下的手势已经完成,该动作包含一个最终的发布位置以及自上一个向下或移动事件以来的任何中间点;
MotionEvent.ACTION_CANCEL:?手势取消,当前手势已经终止,你不会得到更多的坐标点,可以将此视为 up 事件,但不执行任何你通常会执行的操作;
MotionEvent.ACTION_POINTER_DOWN:? 多个手指按下,一个非主触摸点在下降;
MotionEvent.ACTION_POINTER_UP:??? 多个手指离开,一个非主触摸点上升;
MotionEvent.ACTION_OUTSIDE:?????手指触碰超出了正常边界,移动发生在 UI 元素的正常范围之外。这并不是提供一个完整的手势,但只是提供了运动触摸的初始位置;注意,因为任何事件的位置都在视图层次结构的边界之外,所以默认情况它不会被分配给 ViewGroup 的任何子元素;
MotionEvent.ACTION_SCROLL:?????非触摸滑动,运动事件包含相对的垂直/水平滚动偏移量,这个动作不是触摸事件。
先来看看 RecyclerView 的onTouchEvent()
方法:
@Override
public boolean onTouchEvent(MotionEvent e) {
//将滑动事件分派给 OnItemTouchListener 或为 OnItemTouchListeners 提供拦截机会,触摸事件被拦截处理则返回 true
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
//根据布局方向来决定滑动的方向
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();//能否支持水平方向滑动
final boolean canScrollVertically = mLayout.canScrollVertically();//能否支持垂直方向滑动
//获取一个新的 VelocityTracker 对象来观察滑动的速度
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(e);
//返回正在执行的操作,不包含触摸点索引信息。即事件类型,如 MotionEvent.ACTION_DOWN
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();//Action 的索引
//复制事件信息创建一个新的事件
final MotionEvent vtev = MotionEvent.obtain(e);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
case MotionEvent.ACTION_DOWN: {//手指按下
mScrollPointerId = e.getPointerId(0);//特定触摸点相关联的触摸点 id,获取第一个触摸点的 id
//记录 down 事件的 X、Y 坐标
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;//指示沿水平轴滑动
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;//指示沿纵轴滑动
}
//开启一个新的嵌套滚动,如果找到一个协作的父 View,并开始嵌套滑动
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
case MotionEvent.ACTION_POINTER_DOWN: {//多个手指按下
//更新 mScrollPointerId,表示只会响应最近按下的手势事件
mScrollPointerId = e.getPointerId(actionIndex);
//更新最近的手势坐标
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
case MotionEvent.ACTION_MOVE: {//手指移动
//根据 mScrollPointerId 获取触摸点下标
final int index = e.findPointerIndex(mScrollPointerId);
//根据 move 事件产生的 x,y 来计算偏移量 dx,dy
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (mScrollState != SCROLL_STATE_DRAGGING) {//不是被触摸移动状态
boolean startScroll = false;
if (canScrollHorizontally) {//水平滑动的方向
if (dx > 0) {
dx = Math.max(0, dx - mTouchSlop);
} else {
dx = Math.min(0, dx + mTouchSlop);
}
if (dx != 0) {
startScroll = true;
}
}
if (canScrollVertically) {//垂直滑动的方向
if (dy > 0) {
dy = Math.max(0, dy - mTouchSlop);
} else {
dy = Math.min(0, dy + mTouchSlop);
}
if (dy != 0) {
startScroll = true;
}
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
//被触摸移动状态,真正处理滑动的地方
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;//mReusableIntPair 父 view 消耗的滑动距离
mReusableIntPair[1] = 0;
//mScrollOffset 表示 RecyclerView 的滚动位置
//将嵌套的预滑动操作的一个步骤分派给当前嵌套的滑动父 View,如果为 true 表示父 View 优先处理滑动事件。
//如果消耗,dx dx 会分别减去父 View 消耗的那一部分距离
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];//减去父 View 消耗的那一部分距离
dx -= mReusableIntPair[1];
//更新嵌套的偏移量
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
//滑动已经开始,防止父 View 被拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//最终实现的滑动效果
if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
//从缓存中预取一个 ViewHolder
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
case MotionEvent.ACTION_POINTER_UP: {//多个手指离开
//选择一个新的触摸点来处理结局,重新处理坐标
onPointerUp(e);
} break;
case MotionEvent.ACTION_UP: {//手指离开,滑动事件结束
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
//计算滑动速度
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
//最后一次 X/Y 轴的滑动速度
final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
//处理惯性滑动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);//设置滑动状态
}
resetScroll();//重置滑动
} break;
case MotionEvent.ACTION_CANCEL: {//手势取消,释放各种资源
cancelScroll();//退出滑动
} break;
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();//回收滑动事件,方便重用,调用此方法你不能再接触事件
return true;//返回 true 表示由 RecyclerView 来处理事件
}
上面就是 RecyclerView 的onTouchEvent()
方法,其中ACTION_DOWN
,ACTION_MOVE
,ACTION_UP
,ACTION_CANCEL
这几个事件是 View 的基本事件,ACTION_POINTER_DOWN
,ACTION_POINTER_UP
这个两个事件跟多指滑动有关。
这里主要做了三件事,
一是将滑动事件分派给 OnItemTouchListener 或为 OnItemTouchListeners 提供拦截机会,被拦截处理则返回 true,即消费掉事件;
二是初始化手势坐标,滑动方向,事件信息等数据;
三是 OnItemTouchListener 或 OnItemTouchListeners 不消费当前事件,那么走正常的事件分发流程。
这里面有很多细节,我们逐个事件来详细分析:
[](
)1.1 Down 事件
case MotionEvent.ACTION_DOWN:{//手指按下
mScrollPointerId = e.getPointerId(0);//特定触摸点相关联的触摸点 id,获取第一个触摸点的 id
//1.记录 down 事件的 X、Y 坐标
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;//指示沿水平轴滑动
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;//指示沿纵轴滑动
}
//2.开启一个新的嵌套滑动,如果找到一个协作的父 View,并开始嵌套滚动
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
} break;
Down 事件首先获取第一个触摸点 id,一个 Pointer 就是一个触摸点,down 是一系列事件的开始,这里主要做了两件事:
1.记录 down 事件的 X,Y 坐标;
2.调用
startNestedScroll()
启一个新的嵌套滑动,如果找到嵌套的父 View 则会启动嵌套滑动,即处理事件。
[](
)1.2 Move 事件
case MotionEvent.ACTION_MOVE:{//手指移动
//根据 mScrollPointerId 获取触摸点下标
final int index = e.findPointerIndex(mScrollPointerId);
//1.根据 move 事件产生的 x,y 来计算偏移量 dx,dy
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
if (mScrollState != SCROLL_STATE_DRAGGING) {//不是被触摸移动状态
boolean startScroll = false;
if (canScrollHorizontally) {//水平滑动的方向
······
}
if (canScrollVertically) {//垂直滑动的方向
······
}
//设置滑动状态,SCROLL_STATE_DRAGGING 表示正在滑动中
if (startScroll) setScrollState(SCROLL_STATE_DRAGGING);
}
//被触摸移动状态,真正处理滑动的地方
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;//mReusableIntPair 父 view 消耗的滑动距离
mReusableIntPair[1] = 0;
//2.将嵌套的预滑动操作的一个步骤分派给当前嵌套的滚动父 View,如果为 true 表示父 View 优先处理滑动事件。
//如果消耗,dx dy 会分别减去父 View 消耗的那一部分距离,mScrollOffset 表示 RecyclerView 的滚动位置
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];//减去父 View 消耗的那一部分距离
dy -= mReusableIntPair[1];
//更新嵌套的偏移量
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
//开始滑动,防止父 View 被拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//3.最终实现的滚动效果
if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
//4.从缓存中预取一个 ViewHolder
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
Move 事件是处理滑动事件的核心,代码比较长,但是结构简单,主要分为如下几步:
1.根据 move 事件产生的 x、y 计算偏移量 dx,dy;
2.
dispatchNestedPreScroll()
分派一个步骤询问父 View 是否需要先处理滑动事件,如果处理则 dx,dy 会分别减去父 View 消耗的那一部分距离;3.判断滑动方向,调用
scrollByInternal()
最终实现滚动效果;4.调用
mGapWorker.postFromTraversal()
从 RecyclerView 缓存中预取一个 ViewHolder。
scrollByInternal()
是最终实现滑动效果,后面会详细分析,GapWorker 预取 ViewHolder 是通过添加 Runnable 到 RecyclerView 任务队列中,最终调用 RecyclerView.Recycler 的tryGetViewHolderForPositionByDeadline()
获取 ViewHolder,它是整个 RecyclerView 回收复用缓存机制的核心方法。这里就不详细分析了,《[RecyclerView 的回收复用缓存机制详解](
)》希望能给你提供帮助。
[](
)1.3 Up 事件
case MotionEvent.ACTION_UP: {//手指离开,滑动事件结束
mVelocityTracker.addMovement(vtev);
eventAddedToVelocityTracker = true;
//1.根据过去的点计算现在的滑动速度
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
//最后一次 X/Y 轴的滑动速度
final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
//处理惯性滑动
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);//设置滑动状态
}
resetScroll();//2.重置滑动
} break;
Up 事件在手指离开后,滑动事件结束。主要做了两件事:
1.通过
computeCurrentVelocity()
计算滑动的速度以及计算 X,Y 轴的最后滑动速度,fling()
是处理惯性滑动;2.惯性滑动结束后设置滑动状态,重置滑动信息。
先通过computeCurrentVelocity()
计算滑动的速度以及计算 X,Y 轴最后的滑动速度后,如果抬起的时候最后速度大于系统的给定值,就保持惯性再滑动一段距离,最后通知嵌套滑动的 View 滑动结束,重置数据。fling()
是处理惯性滑动的核心方法,下面会分析到。
[](
)1.4 Cancel 事件
case MotionEvent.ACTION_CANCEL:{//手势取消,释放各种资源
cancelScroll();//退出滑动
} break;
private void cancelScroll() {
//1.重置滑动,是否资源
resetScroll();
//2.设置滑动状态为没有滑动状态
setScrollState(SCROLL_STATE_IDLE);
}
Cancel 事件表示手势事件被取消了,重置滑动状态等信息。主要做了两件事:
1.
resetScroll()
停止正在进行的嵌套滑动,释放资源;2.设置滑动状态为
SCROLL_STATE_IDLE
没有滑动。
当事件中途被父 View 消费时会响应 cancel 事件,比如在 RecyclerView 接收到 down 事件,但是后续被父 View 拦截,RecyclerView 就会响应 cancel 事件。
[](
)1.5 Pointer_Down 事件
case MotionEvent.ACTION_POINTER_DOWN:{//多个手指按下
//更新 mScrollPointerId,表示只会响应最近按下的手势事件
mScrollPointerId = e.getPointerId(actionIndex);
//更新最近的手势坐标
mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
} break;
Pointer_Down 事件主要是在多个手指按下时,立即更新mScrollPointerId
和按下的坐标。响应新的手势,不再响应旧的手势,一切事件和坐标以新的事件和坐标为准。
注意:这里多指滑动的意思不是 RecyclerView 响应多个手指滑动,而是当旧的一个手指没有释放时,此时另一个新的手指按下,那么 RecyclerView 就不响应旧手指的手势,而是响应最新手指的手势。
[](
)6 Pointer_Up 事件
case MotionEvent.ACTION_POINTER_UP:{//多个手指离开
//选择一个最新的坐标点来处理结局,重新处理坐标
onPointerUp(e);
} break;
private void onPointerUp(MotionEvent e) {
final int actionIndex = e.getActionIndex();
if (e.getPointerId(actionIndex) == mScrollPointerId) {
final int newIndex = actionIndex == 0 ? 1 : 0;
mScrollPointerId = e.getPointerId(newIndex);
评论