作者:maxcion
现在RV
已经初始化好了,那当我们进行滑动交互时代码又是如何执行的呢?
RV
优秀就优秀在他是动态布局的,与ScrollView
不同在于:ScrollView
是初始化时将所有child都inflate
并add
而RV
是只inflate
屏幕展示得下的child.
如果我们有100个child:
-
ScrollView
便会在初始化时就inflate
并add
100个child,这样滑动的时候ScrollView
不用执行过多的逻辑. -
RV
由于只会inflate
部分child,所以当滑动的时候就会涉及动态inflate
、add
、remove
等逻辑.
RV
再强大,他终究还是View,那他也逃离不了android触摸事件传递的限制,所以如果RV
想要响应滑动事件,那一切的开始必然在onTouchEvent()
中.
一切的开始
RV.onTouchEvent()
public boolean onTouchEvent(MotionEvent e) {
if (mLayoutSuppressed || mIgnoreMotionEventTillDown) {
return false;
}
if (dispatchToOnItemTouchListeners(e)) {
cancelScroll();
return true;
}
if (mLayout == null) {
return false;
}
//判断滑动方向,这里我们选择以垂直方向滑动为例
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
//这个就是创建一个 速度跟踪器
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
boolean eventAddedToVelocityTracker = false;
final int action = e.getActionMasked();
final int actionIndex = e.getActionIndex();
if (action == MotionEvent.ACTION_DOWN) {
mNestedOffsets[0] = mNestedOffsets[1] = 0;
}
final MotionEvent vtev = MotionEvent.obtain(e);
vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
switch (action) {
//DOWN 事件主要和嵌套滑动相关,所以这里可以跳过
case MotionEvent.ACTION_DOWN: {
mScrollPointerId = e.getPointerId(0);
mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
if (canScrollHorizontally) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
}
if (canScrollVertically) {
nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
}
startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
}
break;
case MotionEvent.ACTION_POINTER_DOWN: {
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: {
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
//这里主要计算手指滑动距离
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;
//mScrollState的默认值是SCROLL_STATE_IDLE,
//从名字上能看出来跟滑动状态相关,现在还没开始滑动,所以是默认值
//通过查看mScrollState所有赋值的地方都是在滑动的逻辑
//所以这个if条件成立
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);
}
}
//上面已经把mScrollState设置为SCROLL_STATE_DRAGGING
//判断条件成立
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dx -= releaseHorizontalGlow(dx, e.getY());
dy -= releaseVerticalGlow(dy, e.getX());
//这里和嵌套滑动相关,可以跳过
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
//① 这里是重点了
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
}
break;
...
...
}
if (!eventAddedToVelocityTracker) {
mVelocityTracker.addMovement(vtev);
}
vtev.recycle();
return true;
}
RV.scrollByInternal()
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
int unconsumedX = 0;
int unconsumedY = 0;
int consumedX = 0;
int consumedY = 0;
//这个函数主要是为了处理当在滑动的时候,adapter数据更新的情况
//滑动的时候默认数据是不变的,但是adapter实际修改了数据,就会有crash
consumePendingUpdateOperations();
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
//① 这里又是重点了,滑动时的layout逻辑都在这里
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
//后面都是和滑动嵌套相关的逻辑,可以直接跳过
if (!mItemDecorations.isEmpty()) {
invalidate();
}
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
type, mReusableIntPair);
unconsumedX -= mReusableIntPair[0];
unconsumedY -= mReusableIntPair[1];
boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;
// Update the last touch co-ords, taking any scroll offset into account
mLastTouchX -= mScrollOffset[0];
mLastTouchY -= mScrollOffset[1];
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
// For rotary encoders, we release stretch EdgeEffects after they are pulled, to
// avoid the effects being stuck pulled.
if (Build.VERSION.SDK_INT >= 31
&& MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_ROTARY_ENCODER)) {
releaseGlows();
}
}
considerReleasingGlowsOnScroll(x, y);
}
if (consumedX != 0 || consumedY != 0) {
dispatchOnScrolled(consumedX, consumedY);
}
if (!awakenScrollBars()) {
invalidate();
}
return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}
其实这里面大部分逻辑都是处理嵌套滑动的,有layout相关的逻辑都在scrollStep()
LLM.scrollStep()
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
TraceCompat.beginSection(TRACE_SCROLL_TAG);
fillRemainingScrollValues(mState);
int consumedX = 0;
int consumedY = 0;
//这里就是根据滑动方向进行滑动处理
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
TraceCompat.endSection();
repositionShadowingViews();
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
if (consumed != null) {
consumed[0] = consumedX;
consumed[1] = consumedY;
}
}
scrollStep()
主要处理两件事,将滑动委托给LayoutManager
和其他…
LLM.scrollVerticallyBy
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
//交给scrollBy()处理
return scrollBy(dy, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || delta == 0) {
return 0;
}
ensureLayoutState();
mLayoutState.mRecycle = true;
//detail > 0 代表往上滑或者往左滑
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
//通过滑动方向来更新layoutState中的各种参数
updateLayoutState(layoutDirection, absDelta, true, state);
//开始填充child
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
这里的重点逻辑又进入到fill()
这里的fill()
和静态布局时的逻辑差别不大,不同的是这里设计Viewholder
的回收,这里的回收逻辑都在recycleByLayoutState()
中
LLM.fill()
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
//①这里的逻辑就是先判断哪些child会被划出屏幕,然后把他们进行回收
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
//计算总共有多少空间可以用来摆放child
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
//一个用来保存每次布局一个child的结果类,比如一个child消费了多少空间
//是否应该真实的计算这个child消费的空间(预布局的时候有些child虽然消费了空间,
// 但是不应该不参与真正的空间剩余空间的计算)
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//只要还有空间和item就进行布局layoutchunk
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//重置上一次布局child的结果
layoutChunkResult.resetInternal();
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//这里是真正layout child的逻辑
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (RecyclerView.VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
//layoutState.mLayoutDirection的值是 1或者-1 所以这里是 乘法
//如果是从顶部往底部填充,当前填充的是第三个child 且每个高度是10dp,那么layoutState.mOffset的值
//就是上次填充时的偏移量 + 这次填充child的高度
//如果是从底部往顶部填充,那就是次填充时的偏移量 - 这次填充child的高度
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
//判断是否要真正的消费当前child参与布局所消费的高度
//从判断条件中可以看到预布局和这个有关,不过预布局等后面几章会详细说的
//这里就是同步目前还剩多少空间可以用来布局
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
//在这个判断内执行滑出去的child进行回收
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//执行回收相关逻辑
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}
这里的逻辑几乎与 Recyclerview源码分析:一、静态时如何布局 一致,但是他在执行逻辑的开始进行了回收逻辑,并且在每次布局完一个child也会再次判断哪些child需要回收.
LLM.recycleByLayoutState()
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
int scrollingOffset = layoutState.mScrollingOffset;
int noRecycleSpace = layoutState.mNoRecycleSpace;
//这里我们还是以垂直布局手指向上滑动场景为例
//因为手指向上滑动,就需要在底部填充child,所以layoutState.mLayoutDirection != LayoutState.LAYOUT_START
//就会走到else逻辑中
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
//①
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
if (scrollingOffset < 0) {
if (DEBUG) {
Log.d(TAG, "Called recycle from start with a negative value. This might happen"
+ " during layout changes but may be sign of a bug");
}
return;
}
// ignore padding, ViewGroup may not clip children.
//举个栗子:屏幕高度100dp,每个child高度为15dp,此时屏幕会显示
//7个child,但是第7个child没有全部显示,底部还有5dp的内容在屏幕
//下方,这时候scrollingOffset=5dp
final int limit = scrollingOffset - noRecycleSpace;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
} else {
//从顶部第一个child开始找,找到第一个child的bottom>scrollingOffset(5dp)的child
//那么这个child之前的所有child在这次滑动中都会划出屏幕
//所以要把他们都回收掉
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, 0, i);
return;
}
}
}
}
总结一下就是:RV
在滑动的时候会把滑动的距离交给LayoutManager
处理和消费,LayoutManager
会遍历当前所有的child并根据他们的位置加上这次滑动距离,判断哪些child会划出屏幕,然后就把他们给回收掉.文章来源:https://www.toymoban.com/news/detail-422742.html
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap
文章来源地址https://www.toymoban.com/news/detail-422742.html
到了这里,关于RecyclerView 滑动布局源码分析:带你深入掌握列表滑动机制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!