/ 前言 /
还是老套路,先来看看实现的效果!
在写这个效果之前,需要熟悉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.java
boolean 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.java
int 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.java
private 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.java
void 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.java
void recycleCachedViewAt(int cachedViewIndex) {
...
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
// 关键代码25
// 添加到ViewPool到缓存里面取
addViewHolderToRecycledViewPool(viewHolder, true);
// 将第0个ViewHolder移除
mCachedViews.remove(cachedViewIndex);
}
继续执行到 addViewHolderToRecycledViewPool()方法
将mCachedViews.get(0)中的ViewHolder获取出来,添加到缓存池中,并删除
#RecyclerView.java
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
.....
// 向缓存池中 保存ViewHolder 关键代码28
getRecycledViewPool().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();
// 根据布局类型来获取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 = 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
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 入口
dispatchLayout();
}
接着执行dispatchLayout()
#RecyclerView.java
void dispatchLayout() {
.....
dispatchLayoutStep2();
......
}
接着执行dispatchLayoutStep2()
#RecyclerView.java
private 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.java
public 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.java
private 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<ViewHolder>();
}
holder.setScrapContainer(this, true);
// 一级缓存位置点2
mChangedScrap.add(holder);
}
}
走到这里4级缓存就结束了
总结一下:
参考深入理解Android RecyclerView的缓存机制:
https://segmentfault.com/a/1190000040421118
/ 复用机制 /
回到fill()方法。ctrl + F搜索一下,上边说过
#LinearLayoutManager.java
int 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.java
void 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.java
View 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.java
ViewHolder 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);
...
}
复用机制比缓存机制简单很多,因为复用入口就一个。看看流程图一目了然!
/ 探探效果实战 /
⚠️:为了全局性考虑,实战采用java,底部附 java/kotlin 源码
要想实战,那就得先实现最普通的效果,这段代码没啥营养,直接看效果!
自定义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() 需要手动重写!
主要功能都在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,然后全部布局到屏幕中心
先来看看当前的效果:
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;
@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 = 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;
}
// 拖拽使用,不用管
@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<T> 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
// 创建拖拽
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/lanyangyangzzz/android_ui
文末
您的点赞收藏就是对我最大的鼓励!
欢迎关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!