美文网首页Android技术知识Android开发Android
ItemDecoration深入解析与实战(二)—— 实际运用

ItemDecoration深入解析与实战(二)—— 实际运用

作者: 鸡汤程序员 | 来源:发表于2019-03-13 10:01 被阅读19次

    一 概述

    这是这个系列的第二篇文章,第一篇
    ItemDecoration深入解析与实战(一)——源码分析 是偏原理性的,而这篇是偏应用性的。没看过上一篇文章对阅读此文也基本没多大影响,不过了解原理会加深对本文Demo的理解。

    这篇文章将会实现上篇文章最后说的几个实战点,包括:

    1. (LinearLayoutManager) 最简单的分割线实现
    2. (LinearLayoutManager) 自定义分割线实现
    3. (GridLayoutManager) 网格布局下的均分等距间距(分割线)
    4. (StaggeredLayoutManger) 瀑布流布局下均分等距间距(分割线)
    5. (GridLayoutManager) 网格布局下实现表格式边框
    6. 打造粘性头部

    看完这6点标题,应该会知道这篇文章的篇幅会稍长,不过因为是实战类型的文章,所以也不会特别枯燥。

    建议

    1. 你需要具备怎样的前提知识

    • 阅读本文应该有一定的 RecyclerView 使用基础
    • 对 View 的基础绘制使用有了解(没有影响也不大)

    2. 阅读顺序

    • 从头到尾,这有个难易顺序,读下去会比较顺畅
    • 由于文章较长,可以挑上面6点其中一个感兴趣的进行阅读,拉到下方每个点的第一部分都会有一个实现图,可以观看实际效果决定是否想要阅读

    二 实战

    1. (LinearLayoutManager) 最简单的分割线实现

    (1) 实现效果

    image

    (2) 具体实现

    像这种单一颜色的分割线实现起来很简单,就是一行代码:

    public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
    
        @Override
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                                   @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            outRect.set(0,0,0,5);
        }
    }
    

    这个5对应的就是outRect.bottom,看过这系列的上篇文章就能容易理解,这个跟在 ItemView的布局文件中增加一个 marginBottom是一样的效果的。不过这样默认是没有颜色的,这个分割线的颜色就取决于 RecyclerView的背景颜色。如我们的效果图的实现:

    image
    RecyclerView rvTest = findViewById(R.id.rv_test);
    rvTest.addItemDecoration(new SimpleDividerDecoration());
    

    这种实现很简单,但是缺点也很突出,因为他是依赖于 RecyclerView 的背景的,而如果我们为 RecyclerView 设置一个padding,就会变成这样:

    image

    就是说万一我们的需求是有padding,而且背景颜色要跟分割线颜色不同那就没办法了。如果要解决这一问题,就要看第2点。

    2. (LinearLayoutManager) 自定义分割线实现

    (1) 实现效果

    image

    (2) 使用

    由于 support 包中已经有了一个默认的实现,所以就没有自己写了,这是官方自带的 ItemDecoration实现类,先看下怎么用:

    rvTest.setLayoutManager(new LinearLayoutManager(this));      
    DividerItemDecoration decoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
    decoration.setDrawable(getResources().getDrawable(R.drawable.divider_gradient));
    rvTest.addItemDecoration(decoration);
    

    在示例中,我为这个Decoration添加了一个 Drawable,这个 Drawable 就是上图的一个分割线效果,如果没有设置这个,那么将会有一个默认的灰色分割线:

    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
        <gradient
            android:endColor="#19f5e7"
            android:startColor="#b486e2" />
        <size android:height="4dp" />
    </shape>
    

    分割线的高度就是这个Drawable的高。

    (3) 具体实现

    用法很简单,但正所谓知其然,还要知其所以然,我们看一下这个 DividerItemDecoration 里面的具体实现是怎样的:

    • 先看getItemOffsets方法的具体实现

    // DividerItemDecoration.java
    
    private Drawable mDivider;
    
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());  //注释1
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
    
    

    直接看注释1,mOrientation == VERTICA的情况,在 getItemOffsets方法中,也是用了我们第1个实战点中最简单的那种方式,只不过他的高度变成了mDivider.getIntrinsicHeight()而已,这个mDivider就是我们 setDrawable中设置的一个 Drawable 对象,如果没有设置,那就会有一个默认的。

    • 再看 onDraw方法

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (parent.getLayoutManager() == null || mDivider == null) {
            return;
        }
        if (mOrientation == VERTICAL) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
    

    这里也分为两种情况,我们直接看 VERTICAL 下的,即 drawVertical(c, parent) 方法:

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        final int left;
        final int right;
        //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right,
                    parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }
        
        /***************分割***************/
        
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, mBounds);  //注释1
            final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
            final int top = bottom - mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }
    

    我们先看注释分割线的上边,逻辑很简单,主要就是为了拿到 Child 最大可用空间的左右边界,如果我们没有设置,parent.getClipToPadding() 默认是返回 ture 的,即最大可用空间的要减去RecyclerView的padding,这是为了让padding不被分割线覆盖。

    再看注释分割线的下边,这里遍历了所有的 Child 。先看注释1这句代码,parent.getDecoratedBoundsWithMargins(child, mBounds),这个方法有什么用呢,其实看名称就能大概猜出来,这个方法可以拿到

    child边界+decoration + margin

    所组成的Rect的边界值mbounds,即下图里面的橙色区域的外边框所对应的值。

    image

    注意:此图不严谨,详细内容请看这系列的上一篇文章

    然后便会将mbounds的 bottom 跟 top ,以及 上面得到的 left 跟 right 设置到 mDivider的边界中,就获得的我们上图的红色虚线边框的矩形,如果我们没有为 itemView 设置 margin,那么就会得到绿色虚线边框的范围,再将这部分画出来,就得到了我们想要的分割线了。

    3. (GridLayoutManager) 网格布局下的均分等距间距(分割线)

    GridSpaceDecoration

    (1) 实现效果

    image

    效果如上图,解决了下面的常见问题:

    1. 某些 item 占用多个 span 情况
    2. item 之前的间距相等
    3. item 的宽高可以保持一致,不会有某个 item 被压扁的情况
    4. 上下左右的边框可以与中间的分割线宽度不一致,每个都可以单独设置

    (2) 使用方法

    public GridSpaceDecoration(int horizontal, int vertical){
        //...
    }
    
    public GridSpaceDecoration(int horizontal, int vertical, int left, int right){
        //...
    }
    
    /**
     * @param horizontal 内部水平距离(px)
     * @param vertical   内部竖直距离(px)
     * @param left       最左边距离(px),默认为0
     * @param right      最右边距离(px),默认为0
     * @param top        最顶端距离(px),默认为0
     * @param bottom     最底端距离(px),默认为0
     */
    public GridSpaceDecoration(int horizontal, int vertical, int left, int right, int top, int bottom){
        //...
    }
    

    该类提供了三个构造方法,直接设置相应的值,然后 add 到
    RecyclerView中即可。

    (3) 具体实现

    step1: 分析

    要实现的功能很清晰,就是要解决上面的常见问题。其中,第2、3点比较麻烦,为什么呢?先分析一下

    image

    先看下上图,当使用 GridLayoutManager 时,GridLayoutManager会将每个 Item 的最大可用空间平均分配开来,就像上图黑线所对应的三个框就是3个 Item 的最大可分配空间。橙色区域就是 Decoration 设置的值跟 item 的 margin ,如果 margin 为0,那么橙色区域便是在 getItemOffsets 方法中设置的值(下面简称 offsets)。绿色虚线所围成的区域就是我们 itemView 的实际空间。

    通过上图,当我们为 item 设置相同的间距时,会发现 item 1 的空间被压缩了,那么怎么解决这一问题呢?

    1. 每个item 宽度相同
    2. item 之前的间距一样

    我们要解决的就是上面的问题

    • 先讨论第1点,因为每个 item 的最大可用空间(黑色框格子)是一致的,所以想要让 item 的宽度一样,就是让每个 item 的 offsets 保持一致。我们可以得到下面的公式:

      sizeAvg = (left + right + center * (spanCount-1)) / spanCount

      其中,left 、right 为最左、左右边间距,center 为中间间距,spanCount 为每一行的 span 个数,就可以得出每个 item 需要设置的 offsets 大小 sizeAvg,这样就可以保证每个 item 的宽度一致(均分)

    • 再看第2点,我们要保证每个中间间距都一样,左右间距达到我们设置的大小。首先,最左边的间距是已经确定了的,即 left,那么最左边 item 的右边 right1 就可以得出为 sizeAvg - left,第二个 item 左边间距 left2 就是 center - right1 同理可以推出接下来的 item ,看下图会更清晰:

      image

      然后把中间的实体线给去掉:

      image

      就可以看到每个 item 的宽度一样了,而且间距也是符合预期的效果。(图片是人工画的,可能会有点小误差)

    step2 实现

    上面分析完成,接着看看算法实现:

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                               @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        if (isFirst) {
            init(parent);
            isFirst = false;
        }
        if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
            handleVertical(outRect, view, parent, state);  //注释1
        } else {
            handleHorizontal(outRect, view, parent, state);
        }
    }
    

    很简单,先是做了一点初始化,然后分两个方向进行不同处理。直接看注释1(orientation == VERTICAL)部分:

    private void handleVertical(Rect outRect, View view, RecyclerView parent,
                                RecyclerView.State state) {
        GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
        int childPos = parent.getChildAdapterPosition(view);
        int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
        int spanSize = lp.getSpanSize();
        int spanIndex = lp.getSpanIndex();
        outRect.left = computeLeft(spanIndex, sizeAvg);    //注释1
        if (spanSize == 0 || spanSize == mSpanCount) {
            outRect.right = sizeAvg - outRect.left;
        } else {
            outRect.right = computeRight(spanIndex + spanSize - 1, sizeAvg);
        }
        outRect.top = mVertical / 2;
        outRect.bottom = mVertical / 2;
        if (isFirstRaw(childPos)) {
            outRect.top = mTop;
        }
        if (isLastRaw(childPos)) {
            outRect.bottom = mBottom;
        }
    }
    

    这里的 sizeAvg 就是我们上面分析的那个 sizeAvg,然后再调用 computeLeft 方法(注释1),先看下这个方法这怎样的实现:

    private int computeLeft(int spanIndex, int sizeAvg) {
        if (spanIndex == 0) {
            return mLeft;
        } else if (spanIndex >= mSpanCount / 2) {
            //从右边算起
            return sizeAvg - computeRight(spanIndex, sizeAvg);
        } else {
            //从左边算起
            return mHorizontal - computeRight(spanIndex - 1, sizeAvg);
        }
    }
    
    private int computeRight(int spanIndex, int sizeAvg) {
        if (spanIndex == mSpanCount - 1) {
            return mRight;
        } else if (spanIndex >= mSpanCount / 2) {
            //从右边算起
            return mHorizontal - computeLeft(spanIndex + 1, sizeAvg);
        } else {
            //从左边算起
            return sizeAvg - computeLeft(spanIndex, sizeAvg);
        }
    }
    

    其实就是一个递归的算法,用的就是上面分析的逻辑,不清楚可以回去翻翻上面的图。计算出水平的 offsets 后,后面的就很简单了,接下来会判断是否第一行跟最后一行来设置最顶部 top 跟最底部 bottom 。

    这个GridSpaceDecoration就算完成了,主要就是完成一个 offsets 的设置,如果想要自定义一些分割线的效果,可以继承此类并实现 onDraw 方法即可。

    4. (StaggeredLayoutManger) 瀑布流布局下均分等距间距(分割线)

    (1) 实现效果

    image

    (3) 具体实现

    这个实现跟上面的基本差不多,所以贴一下代码就好了:

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
                               @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        RecyclerView.LayoutManager originalManager = parent.getLayoutManager();
        if (originalManager == null || !(originalManager instanceof StaggeredGridLayoutManager)) {
            return;
        }
        StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) originalManager;
        if (manager.getOrientation() == StaggeredGridLayoutManager.VERTICAL) {
            handleVertical(outRect, view, parent);
        } else {
            handleHorizontal(outRect, view, parent);
        }
    }
    
    private void handleVertical(@NonNull Rect outRect, @NonNull View view,
                                @NonNull RecyclerView parent) {
        StaggeredGridLayoutManager.LayoutParams params =
                (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
        int spanIndex = params.getSpanIndex();
        int adapterPos = parent.getChildAdapterPosition(view);
        int sizeAvg = (int) ((mHorizontal * (mSpanCount - 1) + mLeft + mRight) * 1f / mSpanCount);
        int left = computeLeft(spanIndex, sizeAvg);
        int right = computeRight(spanIndex, sizeAvg);
        outRect.left = left;
        outRect.right = right;
        outRect.top = mVertical / 2;
        outRect.bottom = mVertical / 2;
        if (isFirstRaw(adapterPos, spanIndex)) {
            //第一行
            outRect.top = mTop;
        }
        if (isLastRaw(spanIndex)) {
            //最后一行
            outRect.bottom = mBottom;
        }
    }
    

    5. (GridLayoutManager) 网格布局下实现表格式边框

    StaggeredSpaceDecoration

    (1) 实现效果

    image

    (2) 具体实现

    TableDecoration

    TableDecoration 是继承于上面第3点的 GridSpaceDecoration来实现的,GridSpaceDecoration 负责间距处理,TableDecoration 则是将分割线给画出来。所以主要就是 onDraw 方法的实现:

    先看构造方法:

    public class TableDecoration extends GridSpaceDecoration {
    
        private Drawable mDivider;
        private int mSize;
        private Rect mBounds;
    
        /**
         * @param color 边框颜色
         * @param size 边框大小(px)
         */
        public TableDecoration(@ColorInt int color, int size) {
            super(size, size, size, size, size, size);
            mSize = size;
            mDivider = new ColorDrawable(color);
            mBounds = new Rect();
        }
    }
    

    就是将 item 的所有边框都设置为 size ,然后根据传进来的 color 创建一个 Drawable 对象。接着看 onDraw方法:

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            draw(c, parent, view);
        }
        drawLast(c, parent);
    }
    

    先是遍历所有 child ,然后进行每个 child 的绘制:

    private void draw(Canvas canvas, RecyclerView parent, View view) {
        canvas.save();
        int translationX = Math.round(view.getTranslationX());
        int translationY = Math.round(view.getTranslationY());
        int viewLeft = view.getLeft() + translationX;
        int viewRight = view.getRight() + translationX;
        int viewTop = view.getTop() + translationY;
        int viewBottom = view.getBottom() + translationY;
        parent.getDecoratedBoundsWithMargins(view, mBounds);
        drawLeft(canvas, mBounds, viewLeft);
        drawRight(canvas, mBounds, viewRight);
        drawTop(canvas, mBounds, viewTop);
        drawBottom(canvas, mBounds, viewBottom);
        canvas.restore();
    }
    
    private void drawLeft(Canvas canvas, Rect bounds, int left) {
        mDivider.setBounds(bounds.left, bounds.top, left, bounds.bottom);
        mDivider.draw(canvas);
    }
    //...
    

    逻辑也不难,跟第2点 自定义分割线实现 里的逻辑差不多,将我们设置的 item 的所有间距画出来,这里就不细说了。画完所有 item 后,还会在 onDraw 调用一个 drawLast 方法,我们先看看没有调用这个方法是怎样的效果:

    image

    可以很明显看出,最后那里如果 item 不是铺满整一行的话,会导致后面那里有一部分的缺陷,这个缺陷其实我们在第3点 网格布局下的均分等距间距(分割线)GridSpaceDecoration 时分析过程中就可以发现了,由于每个 item 的上下左右 offsets 并不一定一致,所以会导致当没有最后一行有空缺的话就会造成一个边框的缺陷。

    原因了解了,那么问题解决应该也不难:

    private void drawLast(Canvas canvas, RecyclerView parent) {
        View lastView = parent.getChildAt(parent.getChildCount() - 1);
        int pos = parent.getChildAdapterPosition(lastView);
        if (isLastColumn((GridLayoutManager.LayoutParams) lastView.getLayoutParams(),pos)){
            return;
        }
        int translationX = Math.round(lastView.getTranslationX());
        int translationY = Math.round(lastView.getTranslationY());
        int viewLeft = lastView.getLeft() + translationX;
        int viewRight = lastView.getRight() + translationX;
        int viewTop = lastView.getTop() + translationY;
        int viewBottom = lastView.getBottom() + translationY;
        parent.getDecoratedBoundsWithMargins(lastView, mBounds);
        canvas.save();
        if (mManager.getOrientation() == LinearLayoutManager.VERTICAL) {
            int contentRight = parent.getRight() - parent.getPaddingRight() - Math.round(parent.getTranslationX());
            //空白区域上边缘
            mDivider.setBounds(mBounds.right, mBounds.top, contentRight, viewTop);
            mDivider.draw(canvas);
            //空白区域左边缘
            mDivider.setBounds(viewRight, viewTop, viewRight + mSize, mBounds.bottom);
            mDivider.draw(canvas);
        }else {
            int contentBottom = parent.getBottom()-parent.getPaddingBottom()-Math.round(parent.getTranslationY());
            //空白区域上边缘
            mDivider.setBounds(mBounds.left,viewBottom,mBounds.right,viewBottom+mSize);
            mDivider.draw(canvas);
            //空白区域左边缘
            mDivider.setBounds(mBounds.left,mBounds.bottom,viewLeft,contentBottom);
            mDivider.draw(canvas);
        }
        canvas.restore();
    }
    

    主要逻辑就是将空缺出来的地方给补齐。

    6. (GridLayoutManager) 打造粘性头部

    StickHeaderDecoration

    (1) 实现效果

    image

    (2) 具体实现

    • 分析

      上面的几个例子中,getItemOffsets 以及 onDraw 方法都用过了,Decoration 中三大方法还有一个 onDrawOver,这个效果就是用 onDrawOver来实现的。

      逻辑是这样的:要实现这样的效果,我们需要在 RecyclerView 的顶部画上一个 StickHeader,也就是我们的第一个 Child。 同时也有一个问题就是我们怎么知道哪个 item 是可以当成头部(StickHeader)的,这里我提供了一个接口来进行判断:

      public interface StickProvider {
          boolean isStick(int position);
      }
      

      这是 StickHeaderDecoration 的一个内部实现类,需要将它的一个对象作为 StickHeaderDecoration的构造方法的参数,例如:

      StickHeaderDecoration decoration = new StickHeaderDecoration(new StickHeaderDecoration.StickProvider() {
      @Override
      public boolean isStick(int position) {
          return mList.get(position).type == StickBean.TYPE_HEADER;
      }
      });
      
      //使用labamda会更简洁
      StickHeaderDecoration decoration = 
          new StickHeaderDecoration(position -> mList.get(position).type == StickBean.TYPE_HEADER);
      

      然后我们就可以通过这个StickProvider对象进行判断是否是需要显示的头部了,接着看主要的方法onDrawOver:

      public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                             @NonNull RecyclerView.State state) {
          RecyclerView.Adapter adapter = parent.getAdapter();
          if (adapter == null || !(adapter instanceof StickProvider)) {
              return;
          }
          int itemCount = adapter.getItemCount();
          if (itemCount == 1) {
              return;
          }
          //找到当前的StickHeader对应的position
          int currStickPos = currStickPos(parent);       //注释1
          if (currStickPos == -1) {
              return;
          }
          c.save();
          if (parent.getClipToPadding()) {
              //考虑padding的情况
              c.clipRect(parent.getPaddingLeft(), parent.getPaddingTop(),
                      parent.getWidth() - parent.getPaddingRight(),
                      parent.getHeight() - parent.getPaddingBottom());
          }
          int currStickType = adapter.getItemViewType(currStickPos);
          //当前显示的StickHeader相应的ViewHolder,先看有没有缓存
          RecyclerView.ViewHolder currHolder = mViewMap.get(currStickType);
          if (currHolder == null) {
              //没有缓存则新生成
              currHolder = adapter.createViewHolder(parent, currStickType);
              //主动测量并布局
              measure(currHolder.itemView, parent);
              mViewMap.put(currStickType, currHolder);
          }
          adapter.bindViewHolder(currHolder, currStickPos);
          c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
          currHolder.itemView.draw(c);
          c.restore();
      }
      
      

      整体逻辑并不难,先是找到当前要显示的头部,这个头部怎么来的呢,看看注释1处的 currStickPos 方法:

      private int currStickPos(RecyclerView parent) {
          int childCount = parent.getChildCount();
          int paddingTop = parent.getPaddingTop();
          int currStickPos = -1;
          for (int i = 0; i < childCount; i++) {
              //考虑到parent padding 的情况,第一个item有可能不可见情况
              //从第1个child向后找
              View child = parent.getChildAt(i);
              if (child.getTop() >= paddingTop) {
                  break;
              }
              int pos = parent.getChildAdapterPosition(child);
              if (mProvider.isStick(pos)) {
                  currStickPos = pos;
              }
          }
          if (currStickPos != -1) {
              return currStickPos;
          }
          for (int i = parent.getChildAdapterPosition(parent.getChildAt(0)) - 1; i >= 0; i--) {
              //从第一个child的前一个开始找
              if (mProvider.isStick(i)) {
                  return i;
              }
          }
          return -1;
      }
      

      主要逻辑分为两步:

      • 因为当 RecyclerView 设置 paddingTop 时,第一个 Item 有可能是不可见的(被padding盖住了),所以第一步是从当前第一个 child 开始向后找(child的top<paddingTop),当找到时则返回对应的 Adapter position,如果没有找到,则进行二步。
      • 第二步就是从第一个child的 Adapter 前一个 position 开始找,找到则返回,如果都没找到,则返回-1。

      再回到 onDrawOver 方法中,当找到当前要显示的 Header 后,并会为他进行测量,然后布局(具体看项目源码),接着再调用 Adapter 的 bindViewHolder方法进行数据绑定,最后再画出来就ok了,接着看看效果:

    image

    看到效果图并不是我们想要达到的效果,很明显缺少一个推动的效果,那么这个怎么实现呢:

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent,
                        @NonNull RecyclerView.State state) {
     //...
     
     //寻找下一个StickHeader
     RecyclerView.ViewHolder nextStickHolder = nextStickHolder(parent, currStickPos);
     if (nextStickHolder != null) {
         RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) currHolder.itemView.getLayoutParams();
         int bottom = parent.getPaddingTop() + params.topMargin + currHolder.itemView.getMeasuredHeight();
         int nextStickTop = nextStickHolder.itemView.getTop();
         //下一个StickHeader如果顶部碰到了当前StickHeader的屁股,那么将当前的向上推
         if (nextStickTop < bottom && nextStickTop > 0) {
             c.translate(0, nextStickTop - bottom);
         }
     }
     adapter.bindViewHolder(currHolder, currStickPos);
     c.translate(currHolder.itemView.getLeft(), currHolder.itemView.getTop());
     currHolder.itemView.draw(c);
     c.restore();
    }
    

    逻辑也不难,就是找到下一个 Header ,如果它碰到了上面那个的屁股的话,就将上面那个向上移动一点,就可以形成我们的推动效果啦。

    三 总结

    从决定说要学习这个开始,到写完Demo,写完文章,大概花了2个星期,其中有一些点也是深入了解了部分源码,掉了不少头发才总结出来。其中也碰到不少坑,而且这个系列目前网上的文章比较杂,很少有一个整体的分析,甚至有一些理解是错的,所以这篇文章写了相对详细很多。

    由于编者水平有限,文章难免会有错漏的地方,如有发现,恳请指正,如果有更好的实现思路也可以提供。

    要看项目源码或者Demo的戳这里

    相关文章

      网友评论

        本文标题:ItemDecoration深入解析与实战(二)—— 实际运用

        本文链接:https://www.haomeiwen.com/subject/wttzpqtx.html