既然要研究滑动那就要看Recycleview
的onTouchEvent
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public boolean onTouchEvent (MotionEvent e) { switch (action) { case MotionEvent.ACTION_MOVE: { if (scrollByInternal( canScrollHorizontally ? dx : 0 , canScrollVertically ? dy : 0 , e)) { getParent().requestDisallowInterceptTouchEvent(true ); } if (mGapWorker != null && (dx != 0 || dy != 0 )) { mGapWorker.postFromTraversal(this , dx, dy); } } } break ; case MotionEvent.ACTION_UP: { mVelocityTracker.addMovement(vtev); eventAddedToVelocityTracker = true ; mVelocityTracker.computeCurrentVelocity(1000 , mMaxFlingVelocity); 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 ; } return true ; }
看方法scrollByInternal
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 boolean scrollByInternal (int x, int y, MotionEvent ev) { if (mAdapter != null ) { scrollStep(x, y, mReusableIntPair); } if (!mItemDecorations.isEmpty()) { invalidate(); } if (!awakenScrollBars()) { invalidate(); } return consumedNestedScroll || consumedX != 0 || consumedY != 0 ; } void scrollStep (int dx, int dy, @Nullable int [] consumed) { int consumedX = 0 ; int consumedY = 0 ; if (dx != 0 ) { consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); } if (dy != 0 ) { consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); } if (consumed != null ) { consumed[0 ] = consumedX; consumed[1 ] = consumedY; } }
可以看到滑动事件交给了LayoutManager
的scrollVerticallyBy
h和scrollHorizontallyBy
所以自定义LayoutManager
的话如果支持滑动那么就需要重写这两个方法。现在来看下scrollVertically
方法。
1 2 3 4 5 6 7 8 @Override public int scrollVerticallyBy (int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (mOrientation == HORIZONTAL) { return 0 ; } return scrollBy(dy, recycler, state); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int scrollBy (int delta, RecyclerView.Recycler recycler, RecyclerView.State state) { mLayoutState.mRecycle = true ; final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDelta = Math.abs(delta); updateLayoutState(layoutDirection, absDelta, true , state); final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false ); if (consumed < 0 ) { return 0 ; } final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta; mOrientationHelper.offsetChildren(-scrolled); mLayoutState.mLastScrollDelta = scrolled; return scrolled; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 private void updateLayoutState (int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) { mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mLayoutDirection = layoutDirection; mReusableIntPair[0 ] = 0 ; mReusableIntPair[1 ] = 0 ; calculateExtraLayoutSpace(state, mReusableIntPair); int extraForStart = Math.max(0 , mReusableIntPair[0 ]); int extraForEnd = Math.max(0 , mReusableIntPair[1 ]); boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END; mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart; mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd; int scrollingOffset; if (layoutToEnd) { mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding(); final View child = getChildClosestToEnd(); mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); } else { final View child = getChildClosestToStart(); mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding(); mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); scrollingOffset = -mOrientationHelper.getDecoratedStart(child) + mOrientationHelper.getStartAfterPadding(); } mLayoutState.mAvailable = requiredSpace; if (canUseExistingSpace) { mLayoutState.mAvailable -= scrollingOffset; } mLayoutState.mScrollingOffset = scrollingOffset; }
进入fill
方法来看下:
滑动的时候mScrollingOffset = 最后一个view全部展示出来的距离
mAvailable = 滑动距离 - mScrollingOffset
这个mAvailable > 0 代表这次的滑动需要去增加新的页面。
mAvailable < 0 代表这次的滑动不需要去增加新的页面。
如果小于0,他里面执行了 mScrollingOffset = mScrollingOffset - mAvailable 也就等于这次的滑动距离。然后根据这次的滑动距离,去判断需不需要对不可见的view进行回收。4
fill方法不仅执行了添加视图的操作也执行了回收的操作。回收的操作我放在另外一篇博客来讲。《Recycleview回收原理分析》
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int fill (RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { 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.mExtraFillSpace; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; while ((layoutState.mInfinite || remainingSpace > 0 ) && layoutState.hasMore(state)) { layoutChunk(recycler, state, layoutState, layoutChunkResult); if (layoutChunkResult.mFinished) { break ; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; if (!layoutChunkResult.mIgnoreConsumed || layoutState.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 ; } } return start - layoutState.mAvailable; }
现在关于onTouchevent
里面的scrollByInternal
方法讲解完了,现在看onTouchevent
下的mGapWorker.postFromTraversal
:
1 2 3 4 5 6 7 8 9 10 11 12 void postFromTraversal (RecyclerView recyclerView, int prefetchDx, int prefetchDy) { if (recyclerView.isAttachedToWindow()) { if (mPostTimeNs == 0 ) { mPostTimeNs = recyclerView.getNanoTime(); recyclerView.post(this ); } } recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Override public void run () { try { if (mRecyclerViews.isEmpty()) { return ; } prefetch(nextFrameNs); } finally { mPostTimeNs = 0 ; TraceCompat.endSection(); } } void prefetch (long deadlineNs) { buildTaskList(); flushTasksWithDeadline(deadlineNs); } private void flushTasksWithDeadline (long deadlineNs) { for (int i = 0 ; i < mTasks.size(); i++) { final Task task = mTasks.get(i); if (task.view == null ) { break ; } flushTaskWithDeadline(task, deadlineNs); task.clear(); } } private void flushTaskWithDeadline (Task task, long deadlineNs) { long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs; RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view, task.position, taskDeadlineNs); if (holder != null && holder.mNestedRecyclerView != null && holder.isBound() && !holder.isInvalid()) { prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs); } } private RecyclerView.ViewHolder prefetchPositionWithDeadline (RecyclerView view, int position, long deadlineNs) { if (isPrefetchPositionAttached(view, position)) { return null ; } RecyclerView.Recycler recycler = view.mRecycler; RecyclerView.ViewHolder holder; try { view.onEnterLayoutOrScroll(); holder = recycler.tryGetViewHolderForPositionByDeadline( if (holder != null ) { if (holder.isBound() && !holder.isInvalid()) { recycler.recycleView(holder.itemView); } else { recycler.addViewHolderToRecycledViewPool(holder, false ); } } } finally { view.onExitLayoutOrScroll(false ); } return holder; }
看下prefetchPositionWithDeadline
方法,里面判断viewHolder是否调用过onBinderViewHolder方法,如果调用过了,就把这个viewHolder添加到mCachedViews数组里面,如果没有添加过的话就添加到mRecycleviewPool里面。并且mRecycleviewPool里面的这种类型的数组还没有超过数组上线默认一种类型的数组大小是5:
1 2 3 4 5 6 7 8 9 10 11 12 if (holder != null ) { if (holder.isBound() && !holder.isInvalid()) { recycler.recycleView(holder.itemView); } else { recycler.addViewHolderToRecycledViewPool(holder, false ); } }
关于滑动的时候的预取操作讲完了。
在来看下在onTouchEvent里面的scrollByInternal
方法。是怎么实现滑动 的。
当手指从下往上的时候mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END
这个mLayoutState.mLayoutDirection
的值是根据每次滑动的值来判断的。滑动改变值>0等于LAYOUT_END ;<0 等于LAYOUT_START。mLayoutState.mItemDirection
是我们初始化的时候就已经设置好的值。不会变。
监听viewHolder移除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void removeAndRecycleViewAt (int index, @NonNull Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); recycler.recycleView(view); } void dispatchChildDetached (View child) { final ViewHolder viewHolder = getChildViewHolderInt(child); onChildDetachedFromWindow(child); if (mAdapter != null && viewHolder != null ) { mAdapter.onViewDetachedFromWindow(viewHolder); } if (mOnChildAttachStateListeners != null ) { final int cnt = mOnChildAttachStateListeners.size(); for (int i = cnt - 1 ; i >= 0 ; i--) { mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); } } }
方式一:
调用Recycleview.setRecyclerListener
注册回收监听,这个回调里面可以拿到对应的viewHolder
调用Recycleveiw.addOnChildAttachStateChangeListener
注册监听。这个方法只能拿到view.
重写Recycleview.onChildDetachedFromWindow
,这个方法只能拿到view.
方式二:
重写Adapter.onViewRecycled
可以监听到视图的回收,这个回调里面可以拿到对应的viewHolder
重写Adapter.onViewDetachedFromWindow
监听viewHolder添加
Recycleview:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void dispatchChildAttached (View child) { final ViewHolder viewHolder = getChildViewHolderInt(child); onChildAttachedToWindow(child); if (mAdapter != null && viewHolder != null ) { mAdapter.onViewAttachedToWindow(viewHolder); } if (mOnChildAttachStateListeners != null ) { final int cnt = mOnChildAttachStateListeners.size(); for (int i = cnt - 1 ; i >= 0 ; i--) { mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); } } }
方式一:
重写Adapter.onViewAttachedToWindow
>方法。
方式二:
调用Recycleveiw.addOnChildAttachStateChangeListener
注册监听。
可以发现addOnChildAttachStateChangeListener方法可以监听移除和添加
1 2 3 4 5 6 public interface OnChildAttachStateChangeListener { void onChildViewAttachedToWindow (@NonNull View view) ; void onChildViewDetachedFromWindow (@NonNull View view) ; }
如果要实现像抖音那样选中的视频要进行播放的话,该怎么做。
要实现一页一页滑动的话,当然用的是PagerSnapHelper工具类。
可以重写Recycleview
的onScrollStateChanged
方法监听滑动状态,当状态为SCROLL_STATE_IDLE
的时候表示滑动停止。这样我们就可以在这个状态下获取当前展示在屏幕上的view。
让Recycleview滚动到指定位置
在看PagerSnapHelper的源码发现。
在实现惯性滑动的时候,recycleview的底层是通过这样的方法让recycleview滚动起来的。:
首先我们需要new一个Recycleview.SmoothScroller对象
给Recycleview.SmoothScroller对象设置一个目标滚动位置
调用layoutManager的startSmoothScroll(RecycleView.SmaoothScroller)方法
1 2 3 4 5 6 7 8 9 10 11 RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager); if (smoothScroller == null ) { return false ; } int targetPosition = 10 smoothScroller.setTargetPosition(targetPosition); layoutManager.startSmoothScroll(smoothScroller);
Recycleview的缓存过程
首先先从mAttachedScrap里面获取,他代表的是展示在手机屏幕上面的viewHolder。mAttachesScrop在我们第一次布局页面的时候,虽然调用了他的add操作。但是那个时候recycleview是空的,所以mAttachedScrap数组也是空的。在我看来,当旋转屏幕的时候,系统会自动触发requestLayout。这个时候,我们的mAttachedScrop的数组就不在是空的。
在当mAttachedScrap为空的时候,就从mHiddenViews数组里面获取。
如果mHiddenViews为空,就从mCachedViews里面获取。这个mCachedViews的数组默认大小为2。意味着最大缓存为2,当超过2的时候,mCachedViews会移除超出的数据把他添加到RecycledViewPool里面。RecyclePool里面的数组是根据viewHolder的类型来保存的。每个类型的最大缓存数量为5。
mCachedViews为空的话就去RecycledViewPool里找,再为空就调用onCreateViewHolder去创建。
那mCachedViews里面的数据是在什么时候添加进去的呢?是在移动Recycleview的时候,然触发滑动的时候,如果有的页面超过了我们的限定线limit的时候,就会执行移除操作被添加到mCachedViews数组里面。
那RecycleredViewPool里的数据什么时候加的呢?再手势滑动的时候,执行move操作会调用recycleview的预取操作。预取操作里面判断想获取的那个view有没有执行过onBindViewHolder操作。执行过的那么就把预取得viewHolder加到mCachedViews里面。没有执行过onBindViewHolder操作得就放到RecycledViewPool里面。
通过这里可以知道,从mCachedViews里面获取得viewHolder可以直接拿来展示,而从RecycledViewPool里面获取得viewHolder需要再次执行onBindViewHodler操作。
Recycleview缓存 过程知道了,那么他Recycleview
从缓存里面取的条件是什么呢?
条件是:先遍历mCachedViews数组通过position找到和mCachedViews里面对应的那个viewHolder,mCachedViews里面保存的是最近移除屏幕的view,所以可以通过position找到那个最近移除的viewholder然后直接就可以展示出来,不需要走onBindViewHolder操作。如果在mCachedViews里面找不到就去RecycleredViewPool里面找,遍历RecycleredViewPool是不根据position来找的,而是根据viewHolder的type值来找,找到的话就把RecycleredViewPool数组里面的这个viewHolder删掉,然后在把删掉的这个返回回去。然后则个viewHolder是需要执行onBindViewHolder操作的。然后才能进行展示。
RecycleView更新
需要跟新得时候,会把数据加到:mPendingUpdates里面。
mAdapterUpdateDuringMeasure = true