前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >这RecyclerView的特效,看了直呼牛批

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

原创
作者头像
用户9239674
发布2021-12-08 11:53:26
4800
发布2021-12-08 11:53:26
举报
文章被收录于专栏:用户9239674的专栏

/ 前言 /

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

图片

在写这个效果之前,需要熟悉Rv的回收复用机制,因为实现这个效果,需要自定义LayoutManager()…

众所周知,RecyclerView 是一个可滑动的View,那么他的回收/复用入口一定是在onTouchEvent()事件中

滑动过程中响应的是MotionEvent.ACTION_MOVE事件,所以直接来这里找找看!!

/ 缓存机制 /

onTouchEvent()入口

代码语言:javascript
复制
#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)方法

代码语言:javascript
复制
#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() 方法

代码语言:javascript
复制
#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()->

代码语言:javascript
复制
#LinearLayoutManager.java

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

接着找到fill()方法

代码语言:javascript
复制
#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); 四级复用

有人可能会问,这里为什么是四级?不是说的三级嘛?

其实三级和四级都无所谓,知识点是不会变的,只是层级越多,理解就越深刻,越细罢了

直接进入到缓存的代码:

代码语言:javascript
复制
#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() 随便点开一个看看,里面代码都差不多一样.

代码语言:javascript
复制
#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()方法

代码语言:javascript
复制
#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()方法

代码语言:javascript
复制
#RecyclerView.java

        // 关键代码24
        public void removeAndRecycleViewAt(int index, Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            // 关键代码25
            recycler.recycleView(view);
        } 

继续往下执行

代码语言:javascript
复制
#RecyclerView.java

 public void recycleView(View view) {
            .......
            ViewHolder holder = getChildViewHolderInt(view);
            // 缓存
            recycleViewHolderInternal(holder);
        } 

接着继续执行recycleViewHolderInternal()

代码语言:javascript
复制
#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)的细节!

代码语言:javascript
复制
#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获取出来,添加到缓存池中,并删除

代码语言:javascript
复制
#RecyclerView.java

void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
            .....
            // 向缓存池中 保存ViewHolder 关键代码28
            getRecycledViewPool().putRecycledView(holder);
        } 

点进来看看putRecycledView()方法

代码语言:javascript
复制
#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()入口

代码语言:javascript
复制
#RecyclerView.java
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 入口
        dispatchLayout();
    } 

接着执行dispatchLayout()

代码语言:javascript
复制
#RecyclerView.java

void dispatchLayout() {
       .....
       dispatchLayoutStep2();
       ......
} 

接着执行dispatchLayoutStep2()

代码语言:javascript
复制
#RecyclerView.java

private void dispatchLayoutStep2() {
          ......

        // 在这里先缓存
        mLayout.onLayoutChildren(mRecycler, mState);
        .....
}

接着走到LinearLayoutManager.onLayoutChildren()方法

代码语言:javascript
复制
#LinearLayoutManager.java

 @Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        ....
        //会执行到: RecyclerView.detachAndScrapAttachedViews()
         detachAndScrapAttachedViews(recycler);
        ......
} 

这里会走到RecyclerView.detachAndScrapAttachedViews(),这行代码非常关键,可以说是缓存屏幕内的ViewHolder的起点,后面完成”探探“效果也需要用到!!

代码语言:javascript
复制
#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()

代码语言:javascript
复制
#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);的细节

代码语言:javascript
复制
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搜索一下,上边说过

代码语言:javascript
复制
#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);方法

代码语言:javascript
复制
#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);

代码语言:javascript
复制
#LinearLayoutManager.java

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

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

代码语言:javascript
复制
#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(),所有的复用代码都在这里了!

代码语言:javascript
复制
#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的具体绑定细节:

代码语言:javascript
复制
private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition,
                                                    int position, long deadlineNs) {
           ....
           // 最终绑定位置
            mAdapter.bindViewHolder(holder, offsetPosition);
           ...
} 

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

/ 探探效果实战 /

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

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

自定义LayoutManager

代码语言:javascript
复制
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()中编写

代码语言:javascript
复制
#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摆放时候有缩放层级:

代码语言:javascript
复制
#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

代码语言:javascript
复制
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

代码语言:javascript
复制
#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()处理:

代码语言:javascript
复制
#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,然后在添加到最后一个,造成循环滑动的效果!来看看效果:

现在看来,还是有点生硬,添加一些滑动系数缩放:这里直接贴出完整代码:看图说话:

代码语言:javascript
复制
#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

代码语言:javascript
复制
// 创建拖拽
        val slideCardStackCallBack = SlideCardStackCallBack2(cardStackAdapter)
        val itemTouchHelper = ItemTouchHelper(slideCardStackCallBack)

        // 绑定拖拽
        itemTouchHelper.attachToRecyclerView(rootRecyclerView) 

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

还有两个比较好玩的参数

代码语言:javascript
复制
// 设置回弹距离
    @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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • / 前言 /
  • / 缓存机制 /
  • onTouchEvent()入口
  • onLayout()入口
  • / 复用机制 /
  • / 探探效果实战 /
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档