这RecyclerView的特效,让你直呼666
/ 今日科技快讯 /
近日苹果公司宣布推出自助维修计划,表示将于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相关推荐
- Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666
Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666 作为初级程序员,这样的问题在面试中,也被问到过,随着越来越了解,发现以前自己答的真水. 一般的回答 先来说说我以 ...
- Python仅用3行代码就能输出花式字符串图集,同事直呼666!
Python用3行代码输出花式字符串图集,同事直呼666! 高逼格的日志 springboot 相信Java程序员看到上面的图,一定不会陌生.没错,springboot的启动日志.不知道其他人怎么想, ...
- 让你直呼666的仿Excel表格效果
今日科技快讯 据CNBC报道,网络安全记者发布报告声称,Facebook在未加密的情况下存储了多达6亿个用户账户密码,并以明文形式存储,公司数万名员工可以访问.在Facebook的27亿用户中,6亿用 ...
- 这样规范写代码,同事直呼“666”
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://i7q.cn/5iDTto 一.MyBatis ...
- 这样设计订单系统,同事直呼 666!
来源:人人都是产品经理 | http://dwz.win/Wkh 本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考 ...
- Spring 事务失效的 8 大场景,面试官直呼666...
前几天发了一篇文章里面有一个关于事务失效的问题: 用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景? 其中有个热心粉丝留言分享了下,我觉得总结得有点经验,给置顶了: ...
- 4种分布式Session的实现方式!老大直呼666...
前言 公司有一个 Web 管理系统,使用 Tomcat 进行部署.由于是后台管理系统,所有的网页都需要登录授权之后才能进行相应的操作. 起初这个系统的用的人也不多,为了节省资源,这个系统仅仅只是单机部 ...
- 这样写Java,同事直呼666
作者:涛姐涛哥 来源:cnblogs.com/taojietaoge/p/11575376.html 一.MyBatis 不要写 1=1 当遇到多个查询条件,使用where 1=1 可以很方便的解决我 ...
- 7张图讲透Java垃圾回收算法!学妹直呼666!!!
JVM 在垃圾回收的时候: ① 到底使用了哪些垃圾回收算法? ② 分别在什么场景下使用? ③ 各自的优缺点? 下面就来正式的介绍下垃圾回收算法 标记-清除 标记清除是最简单和干脆的一种垃圾回收算法,他 ...
最新文章
- html调用asp边疆,[求助]怎么实现ASP在HTML中调用
- IEEE发布2022年科技趋势全球调研:人工智能和机器学习、云计算及5G将成为下一年最重要的技术...
- python 是什么类型的语言-python是一种什么类型的语言
- python修改html的td_python3修改HTMLTestRunner,生成有截图的测试报告,并发送测试邮件(一)...
- 5年前面试题引发的“血案”(番外篇)(总结和乱侃)
- 13、mysql中视图的应用
- Elasticsearch:用于内容丰富的文本分析
- canvas 绘制圆形进度条
- mysql 多个库一起导出_MYSQL 导出多个库
- 学习笔记:GoogLeNet
- 基于Android幼儿园管理系统,幼儿园管理系统
- python怎么用pip安装numpy_python如何安装numpy
- kali2020出现中文乱码解决
- 应用计算机测定线性电阻伏安特性实验结论,线性电阻和非线性电阻伏安特性曲线测定实验报告(共8篇).docx...
- 手游多开怎么设置不同的IP登陆
- “钱三篇”后续之物价为什么上涨?
- iis 程序池设置及详解
- 酷狗及一些播放软件收费歌曲下载方法
- leetcode No5 最长回文子串
- 微信小程序---倒计时