美文网首页android 集结号recyclerViewAndroid
RecyclerView之ItemDecoration由浅入深

RecyclerView之ItemDecoration由浅入深

作者: 小武站台 | 来源:发表于2016-09-19 18:20 被阅读28256次

    译文的GitHub地址:RecyclerView之ItemDecoration由浅入深

    译者注:RecyclerView第一篇,希望后面坚持下来

    RecyclerView没有像之前ListView提供divider属性,而是提供了方法

    recyclerView.addItemDecoration()
    

    其中ItemDecoration需要我们自己去定制重写,一开始可能有人会觉得麻烦不好用,最后你会发现这种可插拔设计不仅好用,而且功能强大。

    ItemDecoration类主要是三个方法:

    public void onDraw(Canvas c, RecyclerView parent, State state)
    public void onDrawOver(Canvas c, RecyclerView parent, State state)
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
    

    官方源码虽然都写的很清楚,但还不少小伙伴不知道怎么理解,怎么用或用哪个方法,下面我画个简单的图来帮你们理解一下。

    ItemDecoration

    图画的丑请见谅,首先我们假设绿色区域代表的是我们的内容,红色区域代表我们自己绘制的装饰,可以看到:

    图1:代表了getItemOffsets(),可以实现类似padding的效果

    图2:代表了onDraw(),可以实现类似绘制背景的效果,内容在上面

    图3:代表了onDrawOver(),可以绘制在内容的上面,覆盖内容

    注意上面是我个人从应用角度的看法,事实上实现上面的效果可能三个方法每个方法都可以实现。只不过这种方法更好理解。

    下面是我们没有添加任何ItemDecoration的界面

    Activity

    主页布局界面很简单,背景设成灰色

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/gray">//灰色背景
    
    
        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|enterAlways"/>
    
        </android.support.design.widget.AppBarLayout>
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            />
    
    </android.support.design.widget.CoordinatorLayout>
    

    ok 接下来,让我们来实现实际开发中常遇到的场景。

    padding

    从前面的图可以看到实现这个效果,需要重写getItemOffsets方法。

    public class SimplePaddingDecoration extends RecyclerView.ItemDecoration {
    
        private int dividerHeight;
    
    
        public SimplePaddingDecoration(Context context) {
            dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.bottom = dividerHeight;//类似加了一个bottom padding
        }
    }
    

    没错,就这么2行代码,然后添加到RecyclerView

    recyclerView.addItemDecoration(new SimplePaddingDecoration(this));
    

    实现效果:

    Padding ItemDecoration

    分割线

    分割线在app中是经常用到的,用ItemDecoration怎么实现呢,其实上面padding改成1dp就实现了分割线的效果,但是分割线的颜色只能是背景灰色,所以不能用这种方法。

    要实现分割线效果需要 getItemOffsets()和 onDraw()2个方法,首先用 getItemOffsets给item下方空出一定高度的空间(例子中是1dp),然后用onDraw绘制这个空间

    public class SimpleDividerDecoration extends RecyclerView.ItemDecoration {
    
        private int dividerHeight;
        private Paint dividerPaint;
    
        public SimpleDividerDecoration(Context context) {
            dividerPaint = new Paint();
            dividerPaint.setColor(context.getResources().getColor(R.color.colorAccent));
            dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
        }
    
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.bottom = dividerHeight;
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            int childCount = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
    
            for (int i = 0; i < childCount - 1; i++) {
                View view = parent.getChildAt(i);
                float top = view.getBottom();
                float bottom = view.getBottom() + dividerHeight;
                c.drawRect(left, top, right, bottom, dividerPaint);
            }
        }
    }
    

    实现效果:

    Divider ItemDecoration

    标签

    现在很多电商app会给商品加上一个标签,比如“推荐”,“热卖”,“秒杀”等等,可以看到这些标签都是覆盖在内容之上的,这就可以用onDrawOver()来实现,我们这里简单实现一个有趣的标签

    public class LeftAndRightTagDecoration extends RecyclerView.ItemDecoration {
        private int tagWidth;
        private Paint leftPaint;
        private Paint rightPaint;
    
        public LeftAndRightTagDecoration(Context context) {
            leftPaint = new Paint();
            leftPaint.setColor(context.getResources().getColor(R.color.colorAccent));
            rightPaint = new Paint();
            rightPaint.setColor(context.getResources().getColor(R.color.colorPrimary));
            tagWidth = context.getResources().getDimensionPixelSize(R.dimen.tag_width);
        }
    
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int pos = parent.getChildAdapterPosition(child);
                boolean isLeft = pos % 2 == 0;
                if (isLeft) {
                    float left = child.getLeft();
                    float right = left + tagWidth;
                    float top = child.getTop();
                    float bottom = child.getBottom();
                    c.drawRect(left, top, right, bottom, leftPaint);
                } else {
                    float right = child.getRight();
                    float left = right - tagWidth;
                    float top = child.getTop();
                    float bottom = child.getBottom();
                    c.drawRect(left, top, right, bottom, rightPaint);
    
                }
            }
        }
    }
    

    实现效果:

    Tag ItemDecoration

    组合

    不要忘记的是ItemDecoration是可以叠加的

     recyclerView.addItemDecoration(new LeftAndRightTagDecoration(this));
    recyclerView.addItemDecoration(new SimpleDividerDecoration(this));
    

    我们把上面2个ItemDecoration同时添加到RecyclerView看下什么效果

    ItemDecoration

    是不是有种狂拽炫酷吊炸天的赶脚。。。

    三个方法都用了一遍,你以为这就结束了?呵呵 并没有

    section

    这个是什么呢,先看下我们实现的效果

    Section ItemDecoration

    一看这个就很熟悉吧,手机上面的通讯录联系人,知乎日报都是这样效果,可以叫分组,也可以叫section分块 先不管它叫什么。

    这个怎么实现呢? 其实和实现分割线是一样的道理 ,只是不是所有的item都需要分割线,只有同组的第一个需要。

    我们首先定义一个接口给activity进行回调用来进行数据分组和获取首字母

    public interface DecorationCallback {
    
            long getGroupId(int position);
    
            String getGroupFirstLine(int position);
        }
    

    然后再来看我们的ItemDecoration

    public class SectionDecoration extends RecyclerView.ItemDecoration {
        private static final String TAG = "SectionDecoration";
    
        private DecorationCallback callback;
        private TextPaint textPaint;
        private Paint paint;
        private int topGap;
        private Paint.FontMetrics fontMetrics;
    
    
        public SectionDecoration(Context context, DecorationCallback decorationCallback) {
            Resources res = context.getResources();
            this.callback = decorationCallback;
    
            paint = new Paint();
            paint.setColor(res.getColor(R.color.colorAccent));
    
            textPaint = new TextPaint();
            textPaint.setTypeface(Typeface.DEFAULT_BOLD);
            textPaint.setAntiAlias(true);
            textPaint.setTextSize(80);
            textPaint.setColor(Color.BLACK);
            textPaint.getFontMetrics(fontMetrics);
            textPaint.setTextAlign(Paint.Align.LEFT);
            fontMetrics = new Paint.FontMetrics();
            topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);//32dp
    
    
        }
    
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int pos = parent.getChildAdapterPosition(view);
            Log.i(TAG, "getItemOffsets:" + pos);
            long groupId = callback.getGroupId(pos);
            if (groupId < 0) return;
            if (pos == 0 || isFirstInGroup(pos)) {//同组的第一个才添加padding
                outRect.top = topGap;
            } else {
                outRect.top = 0;
            }
        }
    
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(view);
                long groupId = callback.getGroupId(position);
                if (groupId < 0) return;
                String textLine = callback.getGroupFirstLine(position).toUpperCase();
                if (position == 0 || isFirstInGroup(position)) {
                    float top = view.getTop() - topGap;
                    float bottom = view.getTop();
                    c.drawRect(left, top, right, bottom, paint);//绘制红色矩形
                    c.drawText(textLine, left, bottom, textPaint);//绘制文本
                }
            }
        }
    
        
        private boolean isFirstInGroup(int pos) {
            if (pos == 0) {
                return true;
            } else {
                long prevGroupId = callback.getGroupId(pos - 1);
                long groupId = callback.getGroupId(pos);
                return prevGroupId != groupId;
            }
        }
    
        public interface DecorationCallback {
    
            long getGroupId(int position);
    
            String getGroupFirstLine(int position);
        }
    }
    

    可以看到和divider实现一样,都是重写getItemOffsets()和onDraw()2个方法,不同的是根据数据做了处理。

    在Activity中使用

     recyclerView.addItemDecoration(new SectionDecoration(this, new SectionDecoration.DecorationCallback() {
                @Override
                public long getGroupId(int position) {
                    return Character.toUpperCase(dataList.get(position).getName().charAt(0));
                }
    
                @Override
                public String getGroupFirstLine(int position) {
                    return dataList.get(position).getName().substring(0, 1).toUpperCase();
                }
            }));
    

    干净舒服,不少github类似的库都是去adapter进行处理 侵入性太强 或许ItemDecoration是个更好的选择,可插拔,可替换。

    到这里细心的人就会发现了,header不会动啊,我手机上的通讯录可是会随的滑动而变动呢,这个可以实现么?

    StickyHeader

    这个东西怎么叫我也不知道啊 粘性头部?英文也有叫 pinned section 取名字真是个麻烦事。

    先看下我们简单实现的效果

    stickyheader

    首先一看到图,我们就应该想到header不动肯定是要绘制item内容之上的,需要重写onDrawOver()方法,其他地方和section实现一样。

    public class PinnedSectionDecoration extends RecyclerView.ItemDecoration {
        private static final String TAG = "PinnedSectionDecoration";
    
        private DecorationCallback callback;
        private TextPaint textPaint;
        private Paint paint;
        private int topGap;
        private Paint.FontMetrics fontMetrics;
    
    
        public PinnedSectionDecoration(Context context, DecorationCallback decorationCallback) {
            Resources res = context.getResources();
            this.callback = decorationCallback;
    
            paint = new Paint();
            paint.setColor(res.getColor(R.color.colorAccent));
    
            textPaint = new TextPaint();
            textPaint.setTypeface(Typeface.DEFAULT_BOLD);
            textPaint.setAntiAlias(true);
            textPaint.setTextSize(80);
            textPaint.setColor(Color.BLACK);
            textPaint.getFontMetrics(fontMetrics);
            textPaint.setTextAlign(Paint.Align.LEFT);
            fontMetrics = new Paint.FontMetrics();
            topGap = res.getDimensionPixelSize(R.dimen.sectioned_top);
    
    
        }
    
    
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int pos = parent.getChildAdapterPosition(view);
            long groupId = callback.getGroupId(pos);
            if (groupId < 0) return;
            if (pos == 0 || isFirstInGroup(pos)) {
                outRect.top = topGap;
            } else {
                outRect.top = 0;
            }
        }
    
    
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            int itemCount = state.getItemCount();
            int childCount = parent.getChildCount();
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            float lineHeight = textPaint.getTextSize() + fontMetrics.descent;
    
            long preGroupId, groupId = -1;
            for (int i = 0; i < childCount; i++) {
                View view = parent.getChildAt(i);
                int position = parent.getChildAdapterPosition(view);
    
                preGroupId = groupId;
                groupId = callback.getGroupId(position);
                if (groupId < 0 || groupId == preGroupId) continue;
    
                String textLine = callback.getGroupFirstLine(position).toUpperCase();
                if (TextUtils.isEmpty(textLine)) continue;
    
                int viewBottom = view.getBottom();
                float textY = Math.max(topGap, view.getTop());
                if (position + 1 < itemCount) { //下一个和当前不一样移动当前
                    long nextGroupId = callback.getGroupId(position + 1);
                    if (nextGroupId != groupId && viewBottom < textY ) {//组内最后一个view进入了header
                        textY = viewBottom;
                    }
                }
                c.drawRect(left, textY - topGap, right, textY, paint);
                c.drawText(textLine, left, textY, textPaint);
            }
    
        }
    
    }
    

    好了,现在发现ItemDecoration有多强大了吧! 当然还有更多就需要你自己去发现了。

    相关文章

      网友评论

      • 4d226614f116:想问下博主怎么清除每次绘制的 间隔线呢?
      • dfc0ce271de5:你好,我想问下外面如果是NestedScrollView 粘性事件怎么就失效了?
      • aabe58eef2cc:你好!
        使用Decoration 做出来的StickyHeader,在这个列表里增加输入框,要根据输入框输入的内容更新列表,怎么去更新header?
        shinjiko:如果想配合事件的话,就直接自定义LayoutManager吧。这些画上去的都没法相应事件。
      • junjunxx:很棒,一下子就明白ItemDecoration的使用方式,感谢分享!
      • 正规程序员:c.drawArc(left, top, right, bottom,dividerPaint);报错
      • 64d8ef6f7a79:preGroupId = groupId;
        groupId = callback.getGroupId(position);
        if (groupId < 0 || groupId == preGroupId) continue;
        ----------------------------------------------------------------------------------

        if (position + 1 < itemCount) { //下一个和当前不一样移动当前
        long nextGroupId = callback.getGroupId(position + 1);
        if (nextGroupId != groupId && viewBottom < textY ) {//组内最后一个view进入了header
        textY = viewBottom;
        }
        }
        当一个group item有多个的时候,这两段代码不是有冲突的吗?
      • c84a6998a6c6:那个section的效果,怎么section还没划出界面就消失了呢??
      • c84a6998a6c6:不错,循序渐进,很适合我
      • 高清马里奥:挺好,谢谢楼主分享
      • 上官若枫:功能不错,就是你的代码能不能加上点注释啊
      • 星之声:请问一下粘性头部的点击事件该怎么写呢?谢谢
      • 冷鸢J:有没有老铁遇到第一个带条目ABC的item一消失相应的头布局就消失的问题啊
        c84a6998a6c6:@冷鸢J 怎么解决的?
        冷鸢J:@可乐在睡觉 他这个的逻辑处理好像有点问题还是怎么,我记不太清楚了,后面我自己想了一下,把逻辑优化了一下解决了这个问题
        c84a6998a6c6:是不是section还没划出界面就消失了??我也是
      • 科科然:马克
      • 勿wang:为什么我的上滑时header换item时都会推上去,下拉时会消失然后下拉出来
      • MigrationUK:吊炸天,原来分割线还可以这样用
      • Wu_android:如果你在页面每次刷新的时候 ,重新添加一次就会知道每次都会往下移动 一段距离 ,你可以试试,:cold_sweat:
        DorisSunny:@那个唐僧 请问解决了吗? 我发现只要给第0位,动态添加item,自动刷新界面之后 布局会上移一个分组高度呢
        8aedfe220e6d:@那个唐僧 你看看是不是在刷新的时候重新添加了一次item
        那个唐僧:你说的太对了,就是每次刷新的时候分组都会往下移动一个分组高度的问题..你怎么解决的?
      • da777bb9de0d:作者你好,有项目刚好参考这里的Section那块,有点小错误建议修改如下:
        fontMetrics = new Paint.FontMetrics();
        要放到
        textPaint.getFontMetrics(fontMetrics); 的前面,不然后面使用的时候一直是0值

        然后发现Section的文字垂直方向不能居中,
        在绘制文本的时候加一个基准线,计算如下:
        int baseline = (int) (top + (bottom - top - (fontMetrics.ascent + fontMetrics.descent)) / 2);
        最后drawText的时候用到,
        c.drawText(textLine, left + offsetX, baseline, textPaint);//绘制文本

        以上是我使用中遇到的,仅供参考 :)
        da777bb9de0d:@笔绘丹心 是getTop()和getBottom()
        在代码onDraw函数里面:
        float top = view.getTop() - topGap;
        笔绘丹心:请问下你这个top和botton是getTop()和getBotton()还是getPaddingTop()和getPaddingBotton();:blush:
      • 713c5ee276b9:可以下载demo么?
      • 安静的菜鸟:真棒 点赞
      • Mrxxy:写的不错
      • 1446be8a39a0:很不错的文章
      • 捡淑:马克
      • 积木Blocks:武哥,厉害

      本文标题:RecyclerView之ItemDecoration由浅入深

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