从RecyclerView、NestedScrollView源码分析嵌套滑动异常
来源:互联网 发布:七天网络查分平台登陆 编辑:程序博客网 时间:2024/04/29 11:42
一、显示不全、自动滚动异常
NestedScrollView嵌套RecyclerView时,有2个问题:
1、RecyclerView数据加载完成后,会自动滚动到第一个itemView的位置上,导致RecyclerView上面的布局不显示;
2、当RecyclerView的高度发生改变时,也会自动滚动到第一个itemView的位置上;
两个问题的原因其实都一样,就是NestedScrollView的子控件布局发生改变,导致NestedScrollView的高度发生改变,然后会自动滚动到拥有焦点的子view上。
NestedScrollView源码:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 获取当前拥有焦点的子view View currentFocused = findFocus(); if (null == currentFocused || this == currentFocused) { return; } // 源码官方注释,大意就是: // 如果height改变前,“焦点view”显示在屏幕上,那么height改变后,也应该滚动屏幕,让其仍然显示在屏幕上 // If the currently-focused view was visible on the screen when the // screen was at the old height, then scroll the screen to make that // view visible with the new screen height. if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { currentFocused.getDrawingRect(mTempRect); offsetDescendantRectToMyCoords(currentFocused, mTempRect); // 这个方法是计算需要滚动的距离 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); // 执行滚动 doScrollY(scrollDelta); } }
问题出在computeScrollDeltaToGetChildRectOnScreen()方法上,它的功能是根据“焦点view”的宽高计算需要滚动的距离,计算一般view都是OK的,但是计算RecyclerView/ListView时会出问题,得到的结果总是要滑到第一个item处。具体的计算逻辑就不在这里分析了。
所以解决方案有两种:
1、让“焦点view”不是RecyclerView/ListView,而是一个其它不影响滚动效果的view。
比较简单的做法就是让NestedScrollView的一级子view获取焦点,成为“焦点view”。一个比较简单的方法是,在xml布局时,设置focusable、focusableInTouchMode属性为true,如下:
<android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:focusable="true" android:focusableInTouchMode="true" android:orientation="vertical"> <android.support.v7.widget.RecyclerView android:id="@+id/rv_many_item" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> </LinearLayout> </android.support.v4.widget.NestedScrollView>
2、重写computeScrollDeltaToGetChildRectOnScreen()方法,让其返回正确的值。
有时候布局比较复杂、上面两个属性不生效,就重写该方法,判断如果“焦点view”是RecyclerView/ListView,就返回0。
二、惯性滑动,即fling失效
网上有很多解决惯性滑动失效的方案,主要是以下两种:
// 方案一:mRecyclerView.setNestedScrollingEnabled(false);// 方案二:mRecyclerView.setLayoutManager(new LinearLayoutManager(this) { @Override public boolean canScrollVertically() { return false; }});// 这两种方案的本质都是一样:禁止RecyclerView的滑动事件,让NestedScrollView来管理滑动。
但是却找不到一篇分析原因的文章(难道大家都是乱试出来的?),下面就从源码入手,分析一下原因所在。惯性滑动肯定是在dispatchTouchEvent或onTouchEvent的ACTION_UP中,我们直接进去找。
RecyclerView的onTouchEvent()方法:
case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); eventAddedToVelocityTracker = true; mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); final float xvel = canScrollHorizontally ? -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0; final float yvel = canScrollVertically ? -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0; // 前面经过一大堆判断之后,终于执行了fling()方法 if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { setScrollState(SCROLL_STATE_IDLE); } resetTouch(); } break;
RecyclerView的fling()方法:
public boolean fling(int velocityX, int velocityY) { if (mLayout == null) { Log.e(TAG, "Cannot fling without a LayoutManager set. " + "Call setLayoutManager with a non-null argument."); return false; } if (mLayoutFrozen) { return false; } final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); // 判断竖直方向上是否能滑动。这个mLayout就是LayoutManager final boolean canScrollVertical = mLayout.canScrollVertically(); if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { velocityX = 0; } if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { velocityY = 0; } if (velocityX == 0 && velocityY == 0) { // If we don't have any velocity, return false return false; } if (!dispatchNestedPreFling(velocityX, velocityY)) { final boolean canScroll = canScrollHorizontal || canScrollVertical; // 通知父view(即NestedScrollView)调用onNestedFling()方法,参数canScroll告诉父view自己是否消费; // 如果不消费,父view就会自己处理fling;如果消费了,父view就不会处理了。这就是上面的解决方案能生效的原因 dispatchNestedFling(velocityX, velocityY, canScroll); if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { return true; } if (canScroll) { velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); // 如果能滑动,就启动fling了。mViewFlinger是RecyclerView的内部类,专门处理fling的,不是很懂的可以参考之前那篇讲Scroller的博客 mViewFlinger.fling(velocityX, velocityY); return true; } } return false; }
ViewFlinger类的fling()方法:
public void fling(int velocityX, int velocityY) { setScrollState(SCROLL_STATE_SETTLING); mLastFlingX = mLastFlingY = 0; // 调用Scroller的fling()方法,计算目标点的坐标,并记录下来 mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); // 启动“动画” postOnAnimation(); }void postOnAnimation() { if (mEatRunOnAnimationRequest) { mReSchedulePostAnimationCallback = true; } else { removeCallbacks(this); // 参数this是Runable,因为ViewFlinger实现了Runable接口。真正的滑动操作在run()方法里 ViewCompat.postOnAnimation(RecyclerView.this, this); } }
ViewFlinger类的run()方法:
// 方法很长,只贴出部分,可以看到里面调用了类似于scrollBy()的方法来进行view的移动public void run() {.................. final int x = scroller.getCurrX(); final int y = scroller.getCurrY(); final int dx = x - mLastFlingX; final int dy = y - mLastFlingY; int hresult = 0; int vresult = 0; mLastFlingX = x; mLastFlingY = y; int overscrollX = 0, overscrollY = 0; if (mAdapter != null) { eatRequestLayout(); onEnterLayoutOrScroll(); TraceCompat.beginSection(TRACE_SCROLL_TAG); if (dx != 0) { hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); overscrollX = dx - hresult; } if (dy != 0) { // 所有的滑动都是由N次scrollBy()一点一点移动的。mLayout就是LayoutManager,可见,真正的滑动是在LayoutManager里面实现的 // scrollVerticallyBy()返回的就是滑动的距离 vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState); overscrollY = dy - vresult; } TraceCompat.endSection(); repositionShadowingViews(); }..................}
由于RecyclerView、NestedScrollView的方法基本都是缺省、私有的,所以从外部很难跟踪问题,最终在NestedScrollView的onNestedScroll()方法中跟踪到“RecyclerView消费的距离总是0”,所以才导致滑动异常。
现在的研究方向就是:为什么RecyclerView消费的距离总是0?
最终自己写了一个类,把LinearLayoutManager类的代码完全copy过来,修复各种报错信息,然后通过打印log找到了原因。
LinearLayoutManager的scrollVerticallyBy()方法:
// 这个方法返回的值就是RecyclerView消费的距离,总是返回0 @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { // LinearLayoutManager的方向默认是VERTICAL if (mOrientation == HORIZONTAL) { return 0; } // 所以是scrollBy()返回了0 return scrollBy(dy, recycler, state); }int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { // 打印出来dy肯定不为0,childCount也不为0 if (getChildCount() == 0 || dy == 0) { return 0; } mLayoutState.mRecycle = true; ensureLayoutState(); final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(dy); updateLayoutState(layoutDirection, absDy, true, state); // consumed就是消费的距离,fill()方法的作用就是根据一列参数,计算出应该消费的距离 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; } // 返回值scrolled最终是在这里赋值,由于consumed为0,所以scrolled也为0 final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); } mLayoutState.mLastScrollDelta = scrolled; // 最终返回值也是0 return scrolled; }
再看fill()方法:
private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { // 记录滑动前的位置start final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } int remainingSpace = layoutState.mAvailable + layoutState.mExtra; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; // 如果是向下滑,layoutState.hasMore(state)就是获取“下面是否还有itemView未加载”;上滑则反之 // layoutState.hasMore(state)总是返回false,while循环进不去 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); layoutChunk(recycler, state, layoutState, layoutChunkResult); if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) { // 通过一些列计算,得到消费距离,用当前位置减去消费的距离,得到滑动后的位置 layoutState.mAvailable -= layoutChunkResult.mConsumed; remainingSpace -= layoutChunkResult.mConsumed; } 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; } } // 滑动开始前的位置减去滑动后的位置,就是滑动的距离 // 由于while循环进不去,所以layoutState.mAvailable的值不会改变,所以结果总是为0 return start - layoutState.mAvailable; }// 判断上/下面是否还有itemView未显示,如果还有,就允许滑动,否则就禁止滑动。由此来保证不会滑出界@Overrideboolean hasMore(RecyclerView.State state) { // 通过打印发现,下滑时,mCurrentPosition的值总是等于itemCount;上滑时,总是等于-1。所以总是返回false return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();}
那mCurrentPosition为什么会等于itemCount或-1呢?
因为NestedScrollView加载RecyclerView时,无法确定其高度,所以RecyclerView总是把所有item一次性加载完(可以通过打印onCreateViewHolder()发现)。对于屏幕显示来说,只显示了部分item,但对于RecyclerView来说,所有item都处于“显示状态”,所以hasMore()肯定就返回false。
一点警惕:如果需要加载大量item,最好不要用NestedScrollView嵌套RecyclerView,一次性new出大量itemView,可能会导致OOM。
- 从RecyclerView、NestedScrollView源码分析嵌套滑动异常
- NestedScrollView嵌套RecyclerView滑动冲突
- ScrollView/NestedScrollView嵌套RecyclerView滑动不流畅
- NestedScrollview 嵌套 RecyclerView出现滑动冲突解决方法
- CoordinatorLayout与NestedScrollView嵌套RecyclerView滑动问题
- NestedScrollView嵌套RecyclerView滑动卡顿解决方案
- 解决nestedScrollview 嵌套 recyclerview出现的异常
- 解决NestedScrollView 嵌套 RecyclerView出现的滑动冲突问题
- Android之NestedScrollView 嵌套 RecyclerView 滑动冲突的问题
- NestedScrollView嵌套RecyclerView时滑动不流畅问题的解决办法
- NestedScrollView嵌套RecyclerView时滑动不流畅问题的解决办法
- NestedScrollView嵌套RecyclerView时滑动不流畅问题的解决办法
- NestedScrollView嵌套RecyclerView时滑动不流畅问题的解决办法
- 解决使用NestedScrollView嵌套RecyclerView滑动不流畅
- NestedScrollView嵌套RecyclerView滑动到底部事件冲突问题
- 解决NestedScrollView中嵌套RecyclerView滑动冲突问题
- NestedScrollView嵌套RecyclerView时滑动不流畅问题的解决办法
- NestedScrollView 嵌套 RecyclerView
- 《第一行代码》学习笔记
- Kth Largest Element in an Array(数组中第K大元素)
- fdisk 命令与parted 命令的区别
- Linux学习之——/etc/sysconfig目录
- jquery 获取多个具有相同name的checkbox的选中的值
- 从RecyclerView、NestedScrollView源码分析嵌套滑动异常
- input标签内发生变化进行监听
- Struts2总结---OGNL表达式的基本语法和用法 (8)
- 【学术篇】网络流24题--飞行员配对方案问题
- Nginx的优点
- MMORPG游戏的人工智能(AI)和行为树设计
- Spark API Java编程使用方法
- LeetCode 78. Subsets
- which命令的用法