这RecyclerView的特效,看了直呼牛批

这RecyclerView的特效,看了直呼牛批
文章图片

/前言/ 还是老套路,先来看看实现的效果!

在写这个效果之前,需要熟悉Rv的回收复用机制,因为实现这个效果,需要自定义LayoutManager()…
众所周知,RecyclerView 是一个可滑动的View,那么他的回收/复用入口一定是在onTouchEvent()事件中
滑动过程中响应的是MotionEvent.ACTION_MOVE事件,所以直接来这里找找看!!
/缓存机制/ onTouchEvent()入口

#RecyclerView.java @Override public boolean onTouchEvent(MotionEvent e) {final int action = e.getActionMasked(); switch (action) { ........................................ ........只展示代码思路,细节请自行查看........ ........................................case MotionEvent.ACTION_MOVE: { if (mScrollState == SCROLL_STATE_DRAGGING) { mLastTouchX = x - mScrollOffset[0]; mLastTouchY = y - mScrollOffset[1]; // 关键代码1 if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } if (mGapWorker != null && (dx != 0 || dy != 0)) { mGapWorker.postFromTraversal(this, dx, dy); } } } break; } }

接着找scrollByInternal(int x, int y, MotionEvent ev)方法
#RecyclerView.javaboolean scrollByInternal(int x, int y, MotionEvent ev) { if (mAdapter != null) { ........................................ ........只展示代码思路,细节请自行查看........ ........................................ if (x != 0) { // 关键代码2 去到 LinearLayoutManager 执行fill方法 consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); unconsumedX = x - consumedX; } if (y != 0) { // 关键代码2 去到LinearLayoutManager 执行fill方法 consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); unconsumedY = y - consumedY; } } .... }

现在走到了mLayout.scrollHorizontallyBy(x, mRecycler, mState);
接着去LinearLayoutManager() 中去找scrollHorizontallyBy() 方法
#LinearLayoutManager.java@Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (mOrientation == HORIZONTAL) { return 0; } // 关键代码3 return scrollBy(dy, recycler, state); }

scrollBy()->
#LinearLayoutManager.java int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { ........................................ ........只展示代码思路,细节请自行查看........ ........................................ final int consumed = mLayoutState.mScrollingOffset // 关键代码4 + fill(recycler, mLayoutState, state, false); }

接着找到fill()方法
#LinearLayoutManager.javaint fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {// 关键代码19 缓存ViewHolder recycleByLayoutState(recycler, layoutState); }// 循环调用 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// 关键代码5 [用来4级复用] layoutChunk(recycler, state, layoutState, layoutChunkResult); ........................................ ........只展示代码思路,细节请自行查看........ ........................................ }}

看到这里只需要记住以下两点即可:
  • recycleByLayoutState(recycler, layoutState); 缓存ViewHolder
  • layoutChunk(recycler, state, layoutState, layoutChunkResult); 四级复用
有人可能会问,这里为什么是四级?不是说的三级嘛?
其实三级和四级都无所谓,知识点是不会变的,只是层级越多,理解就越深刻,越细罢了
直接进入到缓存的代码:
#LinearLayoutManager.java private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { // 关键代码21 缓存底部 recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); } else { // 关键代码20 缓存头部 recycleViewsFromStart(recycler, layoutState.mScrollingOffset); } }

这里如果是向下滑动,就会缓存头部那么就会执行到
recycleViewsFromStart()
如果是向上滑动,就会缓存尾部那么就会执行到recycleViewsFromEnd()
recycleViewsFromStart() 和 recycleViewsFromEnd() 随便点开一个看看,里面代码都差不多一样.
#LinearLayoutManager.java private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {if (mShouldReverseLayout) { for (int i = childCount - 1; i >= 0; i--) { ... // 关键代码22 recycleChildren(recycler, childCount - 1, i); return; } } else { for (int i = 0; i < childCount; i++) { ... // 关键代码23 recycleChildren(recycler, 0, i); return; } } }

这里无论走哪一个if() 都会走到recycleChildren()方法
#LinearLayoutManager.javaprivate void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { if (startIndex == endIndex) { return; } if (endIndex > startIndex) { for (int i = endIndex - 1; i >= startIndex; i--) { // 移除View关键代码23 [执行到RecyclerView.removeAndRecycleViewAt()] removeAndRecycleViewAt(i, recycler); } } else { for (int i = startIndex; i > endIndex; i--) { removeAndRecycleViewAt(i, recycler); } } }

接着这里会执行到RecyclerView的removeAndRecycleViewAt()方法
#RecyclerView.java// 关键代码24 public void removeAndRecycleViewAt(int index, Recycler recycler) { final View view = getChildAt(index); removeViewAt(index); // 关键代码25 recycler.recycleView(view); }

继续往下执行
#RecyclerView.java public void recycleView(View view) { ....... ViewHolder holder = getChildViewHolderInt(view); // 缓存 recycleViewHolderInternal(holder); }

接着继续执行recycleViewHolderInternal()
#RecyclerView.javavoid recycleViewHolderInternal(ViewHolder holder) { ........................................ ........只展示代码思路,细节请自行查看........ ........................................ boolean cached = false; if (forceRecycle || holder.isRecyclable()) { // mViewCacheMax = 缓存的最大值 // mViewCacheMax = 2 // 如果viewHolder是无效、未被移除、未被标记的 if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { int cachedViewSize = mCachedViews.size(); // 关键代码24 // mViewCacheMax = 2 if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { // 如果viewholder存满2个则移除第0个位置 // 保证mCachedViews 最多能缓存2个ViewHolder recycleCachedViewAt(0); cachedViewSize--; } .... // 保存ViewHolder数据 [mCachedViews数据不会超过2个] mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { // 当ViewHolder不改变时候(只有一个ViewHolder) 就会直接存到缓存池中 addViewHolderToRecycledViewPool(holder, true); recycled = true; } ........................................ ........只展示代码思路,细节请自行查看........ ........................................ }

通过 关键代码24 可知,mCachedViews 最多能保存2个ViewHolder
如果第三个ViewHolder来临的时候,就会先删除掉第0个,然后在 mCachedViews.add(targetCacheIndex, holder);
然后再来看看 recycleCachedViewAt(0)的细节!
#RecyclerView.javavoid recycleCachedViewAt(int cachedViewIndex) { ... ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); // 关键代码25 // 添加到ViewPool到缓存里面取 addViewHolderToRecycledViewPool(viewHolder, true); // 将第0个ViewHolder移除 mCachedViews.remove(cachedViewIndex); }

继续执行到 addViewHolderToRecycledViewPool()方法
将mCachedViews.get(0)中的ViewHolder获取出来,添加到缓存池中,并删除
#RecyclerView.javavoid addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) { ..... // 向缓存池中 保存ViewHolder 关键代码28 getRecycledViewPool().putRecycledView(holder); }

点进来看看putRecycledView()方法
#RecyclerView.java// SparseArray 类似与 HashMap // 特点: key相同会保留最后一个, //会根据key的int值排序(从小到大) SparseArray mScrap = new SparseArray<>(); public void putRecycledView(ViewHolder scrap) { // 获取ViewHolder布局类型 final int viewType = scrap.getItemViewType(); // 根据布局类型来获取ViewHolder final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; // 判断缓存池的大小 // mScrap.get(viewType).mMaxScrap 默认为 5 // 同一种ViewType 只保存5个ViewHolder if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; }// 清空ViewHolder记录 scrap.resetInternal(); //add scrapHeap.add(scrap); } // 清空ViewHolder记录 void resetInternal() { mFlags = 0; mPosition = NO_POSITION; mOldPosition = NO_POSITION; mItemId = NO_ID; mPreLayoutPosition = NO_POSITION; mIsRecyclableCount = 0; mShadowedHolder = null; mShadowingHolder = null; clearPayload(); mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; clearNestedRecyclerViewIfNotNested(this); }// 根据不同viewType 获取ViewHolder private ScrapData getScrapDataForType(int viewType) { ScrapData scrapData = https://www.it610.com/article/mScrap.get(viewType); if (scrapData == null) { scrapData = new ScrapData(); mScrap.put(viewType, scrapData); } return scrapData; }

可以看出,缓存池,中最多保存5个同一类型的ViewHolder,并且ViewHolder是空的ViewHolder,
而且缓存池中保存的都是mCachedViews移除的数据!!
[](https://upload-images.jianshu...)
  • 小结
mCachedViews 保存即将离开屏幕外的2个ViewHolder
这RecyclerView的特效,看了直呼牛批
文章图片

mRecyclerPool 缓存池中:同一种ItemViewType类型能够默认最多保存5个空数据的ViewHolder.
这RecyclerView的特效,看了直呼牛批
文章图片

带入实战看看效果:
这里以单布局(ItemViewType = 0)为例
我的layoutManger为GridLayoutManager(content,7),所以每次划出屏幕的时候,就直接会划走7个ViewHolder
可以看出,划出去的一刹那,前5个不会执行onCreateViewHolder(),后2个会执行onCreateViewHolder()
??:onCreateViewHolder() 是用来创建ViewHolder的,后面复用的时候会说!
这RecyclerView的特效,看了直呼牛批
文章图片

走到这里,只是分析了RecyclerView从onTouchEvent()–>MOVE事件滑动事件
最终会把ViewHolder保存mCachedViews, mCachedViews只能保存2个ViewHolder
如果第三个ViewHolder来临的时候,就保存到缓存池(mRecyclerPool)中
缓存池(mRecyclerPool)最多保存5个空的ViewHolder…
这只是一种缓存的入口,缓存还有另一种入口,在RecyclerView 的 onLayout()的时候
mAttachedScrap和mChangedScrap 会缓存屏幕内可见的ViewHolder
onLayout()入口
#RecyclerView.java @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 入口 dispatchLayout(); }

接着执行dispatchLayout()
#RecyclerView.javavoid dispatchLayout() { ..... dispatchLayoutStep2(); ...... }

接着执行dispatchLayoutStep2()
#RecyclerView.javaprivate void dispatchLayoutStep2() { ......// 在这里先缓存 mLayout.onLayoutChildren(mRecycler, mState); ..... }

接着走到LinearLayoutManager.onLayoutChildren()方法
#LinearLayoutManager.java @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { .... //会执行到: RecyclerView.detachAndScrapAttachedViews() detachAndScrapAttachedViews(recycler); ...... }

这里会走到RecyclerView.detachAndScrapAttachedViews(),这行代码非常关键,可以说是缓存屏幕内的ViewHolder的起点,后面完成”探探“效果也需要用到!!
#RecyclerView.javapublic void detachAndScrapAttachedViews(Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); // 回收机制关键代码1 scrapOrRecycleView(recycler, i, v); } }

继续走scrapOrRecycleView()
#RecyclerView.javaprivate void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); ... if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); // 缓存机制关键代码2 主要用来处理 cacheView ,RecyclerViewPool的缓存 recycler.recycleViewHolderInternal(viewHolder); } else { detachViewAt(index); // 缓存机制关键代码3 recycler.scrapView(view); } }

这里有两个非常关键的点
  • 缓存机制关键代码2 主要用来处理 cacheView ,RecyclerViewPool的缓存recycler.recycleViewHolderInternal(viewHolder); // 这个关键点上面已经分析过了!!,忘记的ctrl+F搜索看看看一看
  • recycler.scrapView(view); // 缓存屏幕内的ViewHolder
这里直接看看recycler.scrapView(view); 的细节
void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); // 如果标记没有移除,或者失效等清空 就会缓存 if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { holder.setScrapContainer(this, false); // 一级缓存位置点1 mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList(); } holder.setScrapContainer(this, true); // 一级缓存位置点2 mChangedScrap.add(holder); } }

走到这里4级缓存就结束了
总结一下:
这RecyclerView的特效,看了直呼牛批
文章图片

参考深入理解Android RecyclerView的缓存机制:
https://segmentfault.com/a/11...
/复用机制/ 回到fill()方法。ctrl + F搜索一下,上边说过
#LinearLayoutManager.javaint fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { ..... // 关键代码19 [用来4级缓存] recycleByLayoutState(recycler, layoutState); } ....// 循环调用 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {// 关键代码5 [用来4级复用] layoutChunk(recycler, state, layoutState, layoutChunkResult); ........................................ ........只展示代码思路,细节请自行查看........ ........................................ } }

缓存是进入的recycleByLayoutState(recycler, layoutState); 方法
复用是进入的layoutChunk()方法
执行到layoutState.next(recycler); 方法
#LinearLayoutManager.javavoid layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { // 获取当前view // 关键代码6 View view = layoutState.next(recycler); // 测量View measureChildWithMargins(view, 0, 0); ..... }

接着执行到recycler.getViewForPosition(mCurrentPosition);
#LinearLayoutManager.javaView next(RecyclerView.Recycler recycler) { ..... // 关键代码7 [复用机制入口] final View view = recycler.getViewForPosition(mCurrentPosition); return view; }

然后继续执行到getViewForPosition()–> getViewForPosition()
#RecyclerView.java public View getViewForPosition(int position) { // 关键代码8 return getViewForPosition(position, false); }View getViewForPosition(int position, boolean dryRun) { // 关键代码10 所有的复用都在这里return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; }

最终会执行到tryGetViewHolderForPositionByDeadline(),所有的复用代码都在这里了!
#RecyclerView.javaViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ViewHolder holder = null; // 一级别复用 [mChangedScrap] if (mState.isPreLayout()) { // 关键代码11 holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; }// 一级复用 [mAttachedScrap] if (holder == null) { // 通过位置 // 关键代码12 holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); }// 二级复用 [mCachedViews] if (holder == null) { // 获取布局类型 final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap/cache via stable ids, if exists // 2) 通过稳定ID从废料/缓存中查找(如果存在) if (mAdapter.hasStableIds()) { // 关键代码13 根据Id来复用 holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } }// 三级复用 【自定义复用】 if (holder == null && mViewCacheExtension != null) { // 关键代码14 // 自定义复用 final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); } }// 四级复用 [mRecyclerPool(缓存池复用)] if (holder == null) { // 关键代码15 从缓存池获取viewHolder holder = getRecycledViewPool().getRecycledView(type); }// 最终,如果走到这里,holder == 0,表示没有缓存,那么则创建ViewHolder if (holder == null) { // 如果四级缓存都是 null, 那么就由适配器创建 ViewHolder holder = mAdapter.createViewHolder(RecyclerView.this, type); }// 走到这了的时候,ViewHolder != null // 绑定布局 if (mState.isPreLayout() && holder.isBound()) { ..... } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { ...... // 关键代码17 // 在这里调 onBindViewHolder() 绑定数据 bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); ...... } ...... }

看一下tryBindViewHolderByDeadline(),绑定ViewHolder的具体绑定细节:
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition, int position, long deadlineNs) { .... // 最终绑定位置 mAdapter.bindViewHolder(holder, offsetPosition); ... }

复用机制比缓存机制简单很多,因为复用入口就一个。看看流程图一目了然!
这RecyclerView的特效,看了直呼牛批
文章图片

/探探效果实战/ ??:为了全局性考虑,实战采用java,底部附 java/kotlin 源码
这RecyclerView的特效,看了直呼牛批
文章图片

要想实战,那就得先实现最普通的效果,这段代码没啥营养,直接看效果!

自定义LayoutManager
public class CardStack3LayoutManager extends RecyclerView.LayoutManager { @Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); }// 必须重写 在 RecyclerView->OnLayout()时候调用,用来摆放 Item位置 @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); } }

需要重写generateDefaultLayoutParams()方法,咋们是仿造着 LinearLayoutManager()来写,所以直接参考 LinearLayoutManager()就可以
注意:这里的 onLayoutChildren() 需要手动重写!
这RecyclerView的特效,看了直呼牛批
文章图片

主要功能都在onLayoutChildren()中编写
#CardStack2LayoutManager.java // 最开始显示个数 public static final int MAX_SHOW_COUNT = 4; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); // 调用RecyclerView的缓存机制 缓存 ViewHolder detachAndScrapAttachedViews(recycler); // 最下面图片下标 int bottomPosition = 0; // 获取所有图片 int itemCount = getItemCount(); if (itemCount > MAX_SHOW_COUNT) { // 获取到从第几张开始 bottomPosition = itemCount - MAX_SHOW_COUNT; }for (int i = bottomPosition; i < itemCount; i++) { // 获取当前view宽高 View view = recycler.getViewForPosition(i); addView(view); // 测量 measureChildWithMargins(view, 0, 0); //getWidth() RecyclerView 宽 //getDecoratedMeasuredWidth() View的宽 int widthSpace = getWidth() - getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getDecoratedMeasuredHeight(view); // LinearLayoutManager#layoutChunk#layoutDecoratedWithMargins // 绘制布局 layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)); } }

这段代码就是获取所有的 ItemView,然后全部布局到屏幕中心
先来看看当前的效果:
这RecyclerView的特效,看了直呼牛批
文章图片

detachAndScrapAttachedViews()上面提到过,是缓存的入口,会直接调用到RecyclerView.detachAndScrapAttachedViews()方法
测量布局,摆放的代码参考自 LinearLayoutManager(),思路就是吧当前View添加到RecyclerView中,然后在测量View,最后在摆放(布局)View
这RecyclerView的特效,看了直呼牛批
文章图片

最后让View摆放时候有缩放层级:
#CardStack2LayoutManager.java // 最开始显示个数 public static final int MAX_SHOW_COUNT = 4; // item 平移Y轴距 public static final int TRANSLATION_Y = 20; // 缩放的大小 public static final float SCALE = 0.05f; @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { super.onLayoutChildren(recycler, state); // 缓存 ViewHolder detachAndScrapAttachedViews(recycler); // 最下面图片下标 int bottomPosition = 0; // 获取所有图片 int itemCount = getItemCount(); //如果所有图片 > 显示的图片 if (itemCount > MAX_SHOW_COUNT) { // 获取到从第几张开始 bottomPosition = itemCount - MAX_SHOW_COUNT; }for (int i = bottomPosition; i < itemCount; i++) { // 获取当前view宽高 View view = recycler.getViewForPosition(i); addView(view); // 测量 measureChildWithMargins(view, 0, 0); //getWidth() RecyclerView 宽 //getDecoratedMeasuredWidth() View的宽 int widthSpace = getWidth() - getDecoratedMeasuredWidth(view); int heightSpace = getHeight() - getDecoratedMeasuredHeight(view); // LinearLayoutManager#layoutChunk#layoutDecoratedWithMargins // 绘制布局 layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)); /* * 作者:android 超级兵 * TODO itemCount - 1= 最后一个元素 最后一个元素 - i = 倒数的元素 */ int level = itemCount - 1 - i; if (level > 0) { int value = https://www.it610.com/article/toDip(view.getContext(), TRANSLATION_Y); // 如果不是最后一个才缩放 if (level < MAX_SHOW_COUNT - 1) {// 平移 view.setTranslationY(value * level); // 缩放 view.setScaleX(1 - SCALE * level); view.setScaleY(1 - SCALE * level); } else { // 最下面的View 和前一个View布局一样(level - 1) view.setTranslationY(value * (level - 1)); view.setScaleX(1 - SCALE * (level - 1)); view.setScaleY(1 - SCALE * (level - 1)); } } } }private int toDip(Context context, float value) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, context.getResources().getDisplayMetrics()); }

当前效果为:
这RecyclerView的特效,看了直呼牛批
文章图片

到目前为止,完成了ItemView的叠加摆放,接下来只需要添加上滑动即可!
RecyclerView拖拽滑动需要使用到ItemTouchHelper.SimpleCallback
public class SlideCardStackCallBack2 extends ItemTouchHelper.SimpleCallback { private final CardStackAdapter mAdapter; public SlideCardStackCallBack2(CardStackAdapter mAdapter) { super(0, 15); this.mAdapter = mAdapter; }// 拖拽使用,不用管 @Override public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; }// 滑动结束后的处理 @Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {} }

这里需要传递两个参数:
  • 参数一:dragDirs 拖拽
  • 参数二:swipeDirs 滑动
这里咋们不用拖拽,直接给0就行,主要说一下滑动swipeDirs
#ItemTouchHelper.java/** * Up direction, used for swipe & drag control. */ public static final int UP = 1; //1/** * Down direction, used for swipe & drag control. */ public static final int DOWN = 1 << 1; //2 /** * Left direction, used for swipe & drag control. */ public static final int LEFT = 1 << 2; //4/** * Right direction, used for swipe & drag control. */ public static final int RIGHT = 1 << 3; //8

滑动主要以这几个位运算组
  • 如果需要上下滑动 那么就是 UP+DOWN = 1+2 = 3
  • 如果是上下左滑动就是 UP + DOWN + LEFT = 1 + 2 + 4 = 7
  • 那么如果是上下左右滑动就是 UP + DOWN + LEFT + RIGHT = 15
所以这里直接填15就表示可以上下左右滑动
onSwiped()处理:
#SlideCardStackCallBack2.java@Override public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { // 当前滑动的View下标 int layoutPosition = viewHolder.getLayoutPosition(); // 删除当前滑动的元素 CardStackBean bean = mAdapter.getData().remove(layoutPosition); // 添加到集合第0个位置 造成循环滑动的效果 mAdapter.addData(0, bean); mAdapter.notifyDataSetChanged(); }

这段代码很好理解,先删除当前滑动的View,然后在添加到最后一个,造成循环滑动的效果!来看看效果:

现在看来,还是有点生硬,添加一些滑动系数缩放:这里直接贴出完整代码:看图说话:
#SlideCardStackCallBack2.java@Override public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); int maxDistance = recyclerView.getWidth() / 2; // dx = 当前滑动x位置 // dy = 当前滑动y位置 //sqrt 开根号 double sqrt = Math.sqrt((dX * dX + dY * dY)); // 放大系数 double scaleRatio = sqrt / maxDistance; // 系数最大为1 if (scaleRatio > 1.0) { scaleRatio = 1.0; }int childCount = recyclerView.getChildCount(); // 循环所有数据 for (int i = 0; i < childCount; i++) { View view = recyclerView.getChildAt(i); int valueDip = toDip(view.getContext(), 20f); /* * 作者:android 超级兵 * TODO *childCount - 1 =itemView总个数 *childCount - 1 - i = itemView总个数 - i = 从最后一个开始 * * 假设 childCount - 1 = 7 *i累加 *那么level = childCount - 1 - 0 = 7 *那么level = childCount - 1 - 1 = 6 *那么level = childCount - 1 - 2 = 5 *那么level = childCount - 1 - 3 = 4 *那么level = childCount - 1 - 4 = 3 *。。。。 */ int level = childCount - 1 - i; if (level > 0) { // 最大显示叠加个数:CardStack2LayoutManager.MAX_SHOW_COUNT = 4 if (level < CardStack2LayoutManager.MAX_SHOW_COUNT - 1) { // 缩放比例: CardStack2LayoutManager.SCALE = 0.05 float scale = CardStack2LayoutManager.SCALE; // valueDip * level= 原始平移距离 // scaleRatio * valueDip = 平移系数 // valueDip * level - scaleRatio * valueDip = 手指滑动过程中的Y轴平移距离 // 因为是Y轴,所以向上平移是 - 号 view.setTranslationY((float) (valueDip * level - scaleRatio * valueDip)); // 1 - scale * level = 原始缩放大小 // scaleRatio * scale = 缩放系数 // 因为是需要放大,所以这里是 + 号 view.setScaleX((float) ((1 - scale * level) + scaleRatio * scale)); view.setScaleY((float) ((1 - scale * level) + scaleRatio * scale)); } } } }private int toDip(Context context, float value) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, context.getResources().getDisplayMetrics()); }

滑动系数图解:
这RecyclerView的特效,看了直呼牛批
文章图片

【这RecyclerView的特效,看了直呼牛批】??:记得绑定 RecyclerView
// 创建拖拽 val slideCardStackCallBack = SlideCardStackCallBack2(cardStackAdapter) val itemTouchHelper = ItemTouchHelper(slideCardStackCallBack)// 绑定拖拽 itemTouchHelper.attachToRecyclerView(rootRecyclerView)

这里的注释比较清晰,来看看最终效果吧~

还有两个比较好玩的参数
// 设置回弹距离 @Override public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) { return 0.3f; }// 设置回弹时间 @Override public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) { return 3000; }

很简单,直接看效果

项目地址:https://gitee.com/lanyangyang...
文末 您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!

    推荐阅读