/   今日科技快讯   /

近日苹果公司宣布推出自助维修计划,表示将于2022年初开始销售设备零部件、维修工具和维修手册,让用户能够自行维修iPhone和其他苹果设备。

/   作者简介   /

明天就是周六啦,提前祝大家周末愉快!

本篇文章来自android超级兵的投稿,详细地分析了RecyclerView回收复用机制,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

android超级兵的博客地址:

https://blog.csdn.net/weixin_44819566

/   前言   /

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

在写这个效果之前,需要熟悉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];// 关键代码1if (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@Overridepublic int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,RecyclerView.State state) {if (mOrientation == HORIZONTAL) {return 0;}// 关键代码3return scrollBy(dy, recycler, state);}

scrollBy()->

#LinearLayoutManager.javaint 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 缓存ViewHolderrecycleByLayoutState(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.javaprivate 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.javaprivate void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {if (mShouldReverseLayout) {for (int i = childCount - 1; i >= 0; i--) {...// 关键代码22recycleChildren(recycler, childCount - 1, i);return;}} else {for (int i = 0; i < childCount; i++) {...// 关键代码23recycleChildren(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// 关键代码24public void removeAndRecycleViewAt(int index, Recycler recycler) {final View view = getChildAt(index);removeViewAt(index);// 关键代码25recycler.recycleView(view);}

继续往下执行

#RecyclerView.javapublic 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 = 2if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {// 如果viewholder存满2个则移除第0个位置 // 保证mCachedViews 最多能缓存2个ViewHolderrecycleCachedViewAt(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 关键代码28getRecycledViewPool().putRecycledView(holder);}

点进来看看putRecycledView()方法

#RecyclerView.java// SparseArray 类似与 HashMap<int,ScrapData>
// 特点: key相同会保留最后一个,
//      会根据key的int值排序(从小到大)
SparseArray<ScrapData> mScrap = new SparseArray<>();public void putRecycledView(ViewHolder scrap) {// 获取ViewHolder布局类型final int viewType = scrap.getItemViewType();// 根据布局类型来获取ViewHolderfinal ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;// 判断缓存池的大小// mScrap.get(viewType).mMaxScrap 默认为 5// 同一种ViewType 只保存5个ViewHolderif (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {return;}// 清空ViewHolder记录scrap.resetInternal();//addscrapHeap.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 获取ViewHolderprivate ScrapData getScrapDataForType(int viewType) {ScrapData scrapData = mScrap.get(viewType);if (scrapData == null) {scrapData = new ScrapData();mScrap.put(viewType, scrapData);}return scrapData;}

可以看出,缓存池,中最多保存5个同一类型的ViewHolder,并且ViewHolder是空的ViewHolder,

而且缓存池中保存的都是mCachedViews移除的数据!

  • 小结

mCachedViews 保存即将离开屏幕外的2个ViewHolder

mRecyclerPool 缓存池中:同一种ItemViewType类型能够默认最多保存5个空数据的ViewHolder.

带入实战看看效果:

这里以单布局(ItemViewType = 0)为例

我的layoutManger为`GridLayoutManager(content,7)`,所以每次划出屏幕的时候,就直接会划走7个ViewHolder

可以看出,划出去的一刹那,前5个不会执行onCreateViewHolder(),后2个会执行onCreateViewHolder()

⚠️:onCreateViewHolder() 是用来创建ViewHolder的,后面复用的时候会说!

走到这里,只是分析了RecyclerView从onTouchEvent()–>MOVE事件滑动事件

最终会把ViewHolder保存mCachedViews, mCachedViews只能保存2个ViewHolder

如果第三个ViewHolder来临的时候,就保存到缓存池(mRecyclerPool)中

缓存池(mRecyclerPool)最多保存5个空的ViewHolder…

这只是一种缓存的入口,缓存还有另一种入口,在RecyclerView 的 onLayout()的时候

mAttachedScrap和mChangedScrap 会缓存屏幕内可见的ViewHolder

onLayout()入口

#RecyclerView.java@Overrideprotected 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);// 回收机制关键代码1scrapOrRecycleView(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);// 缓存机制关键代码3recycler.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);// 一级缓存位置点1mAttachedScrap.add(holder);} else {if (mChangedScrap == null) {mChangedScrap = new ArrayList<ViewHolder>();}holder.setScrapContainer(this, true);// 一级缓存位置点2mChangedScrap.add(holder);}
}

走到这里4级缓存就结束了

总结一下:

参考深入理解Android RecyclerView的缓存机制:https://segmentfault.com/a/1190000040421118

/   复用机制   /

回到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// 关键代码6View view = layoutState.next(recycler);// 测量ViewmeasureChildWithMargins(view, 0, 0);.....
}

接着执行到recycler.getViewForPosition(mCurrentPosition);

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

然后继续执行到getViewForPosition()–> getViewForPosition()

#RecyclerView.javapublic View getViewForPosition(int position) {// 关键代码8return 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()) {// 关键代码11holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 一级复用 [mAttachedScrap]if (holder == null) {// 通过位置// 关键代码12holder = 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 从缓存池获取viewHolderholder = getRecycledViewPool().getRecycledView(type);}// 最终,如果走到这里,holder == 0,表示没有缓存,那么则创建ViewHolderif (holder == null) {// 如果四级缓存都是 null, 那么就由适配器创建 ViewHolderholder = 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);...
}

复用机制比缓存机制简单很多,因为复用入口就一个。看看流程图一目了然!

/   探探效果实战   /

⚠️:为了全局性考虑,实战采用java,底部附 java/kotlin 源码

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

自定义LayoutManager

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

需要重写generateDefaultLayoutParams()方法,咋们是仿造着 LinearLayoutManager()来写,所以直接参考 LinearLayoutManager()就可以

注意:这里的 onLayoutChildren() 需要手动重写!

主要功能都在onLayoutChildren()中编写

#CardStack2LayoutManager.java// 最开始显示个数public static final int MAX_SHOW_COUNT = 4;@Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {super.onLayoutChildren(recycler, state);// 调用RecyclerView的缓存机制 缓存 ViewHolderdetachAndScrapAttachedViews(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,然后全部布局到屏幕中心

先来看看当前的效果:

detachAndScrapAttachedViews()上面提到过,是缓存的入口,会直接调用到RecyclerView.detachAndScrapAttachedViews()方法

测量布局,摆放的代码参考自 LinearLayoutManager(),思路就是吧当前View添加到RecyclerView中,然后在测量View,最后在摆放(布局)View

最后让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;@Overridepublic void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {super.onLayoutChildren(recycler, state);// 缓存 ViewHolderdetachAndScrapAttachedViews(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 = 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());}

当前效果为:

到目前为止,完成了ItemView的叠加摆放,接下来只需要添加上滑动即可!

RecyclerView拖拽滑动需要使用到ItemTouchHelper.SimpleCallback

public class SlideCardStackCallBack2<T> extends ItemTouchHelper.SimpleCallback {private final CardStackAdapter<T> mAdapter;public SlideCardStackCallBack2(CardStackAdapter<T> mAdapter) {super(0, 15);this.mAdapter = mAdapter;}// 拖拽使用,不用管@Overridepublic boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {return false;}// 滑动结束后的处理@Overridepublic 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@Overridepublic void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {// 当前滑动的View下标int layoutPosition = viewHolder.getLayoutPosition();// 删除当前滑动的元素CardStackBean<T> bean = mAdapter.getData().remove(layoutPosition);// 添加到集合第0个位置 造成循环滑动的效果mAdapter.addData(0, bean);mAdapter.notifyDataSetChanged();
}

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

现在看来,还是有点生硬,添加一些滑动系数缩放:

这里直接贴出完整代码:

看图说话:

#SlideCardStackCallBack2.java@Overridepublic 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 = 4if (level < CardStack2LayoutManager.MAX_SHOW_COUNT - 1) {// 缩放比例: CardStack2LayoutManager.SCALE = 0.05float 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

// 创建拖拽val slideCardStackCallBack = SlideCardStackCallBack2(cardStackAdapter)val itemTouchHelper = ItemTouchHelper(slideCardStackCallBack)// 绑定拖拽itemTouchHelper.attachToRecyclerView(rootRecyclerView)

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

还有两个比较好玩的参数

// 设置回弹距离@Overridepublic float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {return 0.3f;}// 设置回弹时间@Overridepublic long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, float animateDx, float animateDy) {return 3000;}

很简单,直接看效果

项目地址:https://gitee.com/lanyangyangzzz/android_ui

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Activity Result API详解

边学边玩,来搞个2048小游戏吧

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

这RecyclerView的特效,让你直呼666相关推荐

  1. Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666

    Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666 作为初级程序员,这样的问题在面试中,也被问到过,随着越来越了解,发现以前自己答的真水. 一般的回答 ​ 先来说说我以 ...

  2. Python仅用3行代码就能输出花式字符串图集,同事直呼666!

    Python用3行代码输出花式字符串图集,同事直呼666! 高逼格的日志 springboot 相信Java程序员看到上面的图,一定不会陌生.没错,springboot的启动日志.不知道其他人怎么想, ...

  3. 让你直呼666的仿Excel表格效果

    今日科技快讯 据CNBC报道,网络安全记者发布报告声称,Facebook在未加密的情况下存储了多达6亿个用户账户密码,并以明文形式存储,公司数万名员工可以访问.在Facebook的27亿用户中,6亿用 ...

  4. 这样规范写代码,同事直呼“666”

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://i7q.cn/5iDTto 一.MyBatis ...

  5. 这样设计订单系统,同事直呼 666!

    来源:人人都是产品经理 | http://dwz.win/Wkh 本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考 ...

  6. Spring 事务失效的 8 大场景,面试官直呼666...

    前几天发了一篇文章里面有一个关于事务失效的问题: 用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景? 其中有个热心粉丝留言分享了下,我觉得总结得有点经验,给置顶了: ...

  7. 4种分布式Session的实现方式!老大直呼666...

    前言 公司有一个 Web 管理系统,使用 Tomcat 进行部署.由于是后台管理系统,所有的网页都需要登录授权之后才能进行相应的操作. 起初这个系统的用的人也不多,为了节省资源,这个系统仅仅只是单机部 ...

  8. 这样写Java,同事直呼666

    作者:涛姐涛哥 来源:cnblogs.com/taojietaoge/p/11575376.html 一.MyBatis 不要写 1=1 当遇到多个查询条件,使用where 1=1 可以很方便的解决我 ...

  9. 7张图讲透Java垃圾回收算法!学妹直呼666!!!

    JVM 在垃圾回收的时候: ① 到底使用了哪些垃圾回收算法? ② 分别在什么场景下使用? ③ 各自的优缺点? 下面就来正式的介绍下垃圾回收算法 标记-清除 标记清除是最简单和干脆的一种垃圾回收算法,他 ...

最新文章

  1. html调用asp边疆,[求助]怎么实现ASP在HTML中调用
  2. IEEE发布2022年科技趋势全球调研:人工智能和机器学习、云计算及5G将成为下一年最重要的技术...
  3. python 是什么类型的语言-python是一种什么类型的语言
  4. python修改html的td_python3修改HTMLTestRunner,生成有截图的测试报告,并发送测试邮件(一)...
  5. 5年前面试题引发的“血案”(番外篇)(总结和乱侃)
  6. 13、mysql中视图的应用
  7. Elasticsearch:用于内容丰富的文本分析
  8. canvas 绘制圆形进度条
  9. mysql 多个库一起导出_MYSQL 导出多个库
  10. 学习笔记:GoogLeNet
  11. 基于Android幼儿园管理系统,幼儿园管理系统
  12. python怎么用pip安装numpy_python如何安装numpy
  13. kali2020出现中文乱码解决
  14. 应用计算机测定线性电阻伏安特性实验结论,线性电阻和非线性电阻伏安特性曲线测定实验报告(共8篇).docx...
  15. 手游多开怎么设置不同的IP登陆
  16. “钱三篇”后续之物价为什么上涨?
  17. iis 程序池设置及详解
  18. 酷狗及一些播放软件收费歌曲下载方法
  19. leetcode No5 最长回文子串
  20. 微信小程序---倒计时

热门文章

  1. 【如何实现一个简单的canvas动态水球图】
  2. 独行温哥华,漫步西雅图(上)
  3. canvas实现画布动画星空网页背景
  4. 双非末流一本面霸,十面阿里,七面头条,4个月斩获六个Offer!
  5. el-select 可选择/可输入,自定义搜索方法
  6. 经济与社会发展研究杂志社经济与社会发展研究编辑部2022年第30期目录
  7. 转行学java好吗_转行学Java开发怎么样?
  8. 腾讯面试题(警察捉小偷)
  9. phantomjs 配置和使用_phantomjs使用笔记
  10. 【效率】Docker:从入门到实战过程全记录