RecyclerView使用指南(五)—— 实现吸顶效果

声明:原创作品,转载请注明出处:https://www.jianshu.com/p/c197e3bf8329

前一篇文字我讲解了ItemDecoration的使用方式,这篇文章默认大家已经读过RecyclerView使用指南(四)—— 使用ItemDecoration,所以,不熟悉ItemDecoration的同学请先去看前一篇文章。
OK,我们先来看一下我们将要实现的效果:

一、实现带有Section的样式

我们先重写getItemOffsets()方法,增加outRect的高度,然后重写onDraw()方法,画出一个rectangle。代码如下:

public class DemoItemDecoration extends RecyclerView.ItemDecoration {
    private int mSectionHeight = 80;
    private Paint mPaint;

    public DemoItemDecoration() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.top = mSectionHeight;
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            float sectionLeft = parent.getLeft();
            float sectionTop = child.getTop() - mSectionHeight;
            float sectionRight = parent.getWidth();
            float sectionBottom = child.getTop();
            c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaint);
        }
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
    }
}

实现了这样一个效果:

二、实现分组

刚刚实现的每一个Item都有section,这与实际需求时不符的,那么我们要把数据进行分组,每一组的第一条Item上面才有section,这里,为了让ItemDecoration不与数据源发生直接关系,我们新增一个GroupBean类来描述是否需要增加section。如下:

public class GroupBean {
    private int mGroupId;
    private int mGroupPosition;
    private boolean mIsFirst;
    private boolean mIsLast;

    public GroupBean(int groupId, int groupPosition, boolean isFirst, boolean isLast) {
        mGroupId = groupId;
        mGroupPosition = groupPosition;
        this.mIsFirst = isFirst;
        this.mIsLast = isLast;
    }

    public int getGroupId() {
        return mGroupId;
    }

    public int getGroupPosition() {
        return mGroupPosition;
    }

    public boolean isFirst() {
        return mIsFirst;
    }

    public boolean isLast() {
        return mIsLast;
    }
}

然后改写我们的getItemOffsets()方法和onDraw()方法,只有每一个分组的第一条Item才显示section。代码如下:

public class DemoItemDecoration extends RecyclerView.ItemDecoration {
    private int mSectionHeight = 80;
    private Paint mPaintSection;
    private Paint mPaintText;
    private List<GroupBean> mGroupBeans;

    public DemoItemDecoration(List<GroupBean> groupBeans) {
        //数据
        mGroupBeans = groupBeans;
        //画笔
        mPaintSection = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintSection.setColor(Color.BLUE);
        mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaintText.setTextSize(60);
        mPaintText.setColor(Color.YELLOW);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = parent.getChildAdapterPosition(view);
        if (mGroupBeans.get(position).isFirst()) {
            outRect.top = mSectionHeight;
        }
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull 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 position = parent.getChildAdapterPosition(child);
            GroupBean groupBean = mGroupBeans.get(position);
            if (mGroupBeans.get(position).isFirst()) {
                float sectionLeft = parent.getLeft();
                float sectionTop = child.getTop() - mSectionHeight;
                float sectionRight = parent.getWidth();
                float sectionBottom = child.getTop();
                c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
            }
        }
    }
}

最后,在Activity中进行ItemDecoration与GroupBean列表的绑定,以及ItemDecoration与RecyclerView的绑定:

    private void initRv() {
        List<GroupBean> groupBeans = new ArrayList<>();
        //根据RecyclerView的数据源,设置需要增加section的item
        for (Data data : mList) {
            //这里就是模拟一下,所以我取4的倍数增加section
            int i = mList.indexOf(data);
            int groupId = i / 4;
            int groupPosition = i % 4;
            GroupBean groupBean = null;
            //这里是假数据嘛,4的倍数有section,那余数是3的时候肯定是分组的最后一个啦
            if (groupPosition == 0) {
                groupBean = new GroupBean(groupId, groupPosition, true, false);
            }
            if (groupPosition == 3) {
                groupBean = new GroupBean(groupId, groupPosition, false, true);
            }
            groupBeans.add(groupBean);
        }
        RecyclerView recyclerView = findViewById(R.id.rv);
        recyclerView.setAdapter(new SingleItemAdapter(mList));
        recyclerView.addItemDecoration(new DemoItemDecoration(groupBeans));
    }

我们来看下效果:

好,这样,我们就实现了分组的效果,但是我们想要的吸顶效果,section是应该显示到Item图层的上方的,那么我们使用onDraw()方法来实现,显然是不合理的,既然如此,我们就将onDraw()方法中的内容剪切到onDrawOver()中好了~

三、实现section在列表顶部悬浮

实现吸顶效果,我们还需要做到让我们的section在列表顶部悬浮,来分析一下逻辑:

  • 每个分组的第一条数据需要有section
  • 列表的最上方必须显示一个section
    现在来修改一下onDrawOver(),实现一下:
@Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull 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 position = parent.getChildAdapterPosition(child);
            GroupBean groupBean = mGroupBeans.get(position);
            //所有分组的第一条数据有section
            if (groupBean.isFirst()) {
                float sectionLeft = parent.getLeft();
                float sectionTop = child.getTop() - mSectionHeight;
                float sectionRight = parent.getWidth();
                float sectionBottom = child.getTop();
                c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
            }
            //列表的最上方显示section信息(这里section是第一条显示的条目所对应的groupId)
            LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
            int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
            if (position == firstVisibleItemPosition) {
                float sectionLeft = parent.getLeft();
                float sectionTop = parent.getTop();
                float sectionRight = parent.getWidth();
                float sectionBottom = parent.getTop() + mSectionHeight;
                c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
            }
        }
    }

来看下效果:

嗯,我们实现了一个吸顶效果,但是两个section进行更替的特效显得比较粗糙啊,我们想要的是下面的section将上面的section顶上去,OK,我们再进行优化一下。

四、优化section更替的特效

我们仔细观察上面的效果图,当下面的section向上移动的时候,上面的section没有移动,所以,看起来下面的section直接覆盖到了它的上面。
那么上面的section应该在什么时机进行移动呢?它的底边应该是在该分组中最后一个Item的底部的上方,所以,我们更改,当section的底部低于“分组中最后一个Item”时,section整体上移,移动的距离就是section的高度与条目底部的差。
我们只需更改onDrawOver()方法的代码:

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull 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 position = parent.getChildAdapterPosition(child);
            GroupBean groupBean = mGroupBeans.get(position);
            //所有分组的第一条数据有section
            if (groupBean.isFirst()) {
                float sectionLeft = parent.getLeft();
                float sectionTop = child.getTop() - mSectionHeight;
                float sectionRight = parent.getWidth();
                float sectionBottom = child.getTop();
                c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
            }
            //列表的最上方显示section信息(这里section是第一条显示的条目所对应的groupId)
            LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
            int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
            if (position == firstVisibleItemPosition) {
                //如果是本组的最后一条,section的底部就不能低于这个条目的底部
                if (groupBean.isLast()) {
                    //当条目的底部已经高于section的时候,section应该随着条目的底部往上移动
                    if (child.getBottom() < mSectionHeight) {
                        float sectionLeft = parent.getLeft();
                        float sectionTop = parent.getTop() - (mSectionHeight - child.getBottom());
                        float sectionRight = parent.getWidth();
                        float sectionBottom = parent.getTop() + mSectionHeight - (mSectionHeight - child.getBottom());
                        c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                        c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
                    } else {
                        float sectionLeft = parent.getLeft();
                        float sectionTop = parent.getTop();
                        float sectionRight = parent.getWidth();
                        float sectionBottom = parent.getTop() + mSectionHeight;
                        c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                        c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
                    }
                } else {
                    float sectionLeft = parent.getLeft();
                    float sectionTop = parent.getTop();
                    float sectionRight = parent.getWidth();
                    float sectionBottom = parent.getTop() + mSectionHeight;
                    c.drawRect(sectionLeft, sectionTop, sectionRight, sectionBottom, mPaintSection);
                    c.drawText(String.valueOf(groupBean.getGroupId()), sectionLeft, sectionBottom - 5, mPaintText);
                }
            }
        }
    }

在Activity中添加数据:

    private void initRv() {
        List<GroupBean> groupBeans = new ArrayList<>();
        //根据RecyclerView的数据源,设置需要增加section的item
        for (Data data : mList) {
            //这里就是模拟一下,所以我取4的倍数增加section
            int i = mList.indexOf(data);
            int groupId = i / 4;
            int groupPosition = i % 4;
            GroupBean groupBean;
            //这里是假数据嘛,4的倍数有section,那余数是3的时候肯定是分组的最后一个啦
            if (groupPosition == 0) {
                groupBean = new GroupBean(groupId, groupPosition, true, false);
            } else if (groupPosition == 3) {
                groupBean = new GroupBean(groupId, groupPosition, false, true);
            } else {
                groupBean = new GroupBean(groupId, groupPosition, false, false);
            }
            groupBeans.add(groupBean);
        }
        RecyclerView recyclerView = findViewById(R.id.rv);
        recyclerView.setAdapter(new SingleItemAdapter(mList));
        recyclerView.addItemDecoration(new DemoItemDecoration(groupBeans));
    }

最后看一下效果:

总结

这篇文章我们实现了一个吸顶效果的特效,是属于比较高级的用法了,关于ItemDecoration的用法也用它进行收尾了。另外,示例代码中的冗余代码比较多,主要是为了看起来容易理解,请小朋友们在使用过程中合理地优化代码。

参考文献

https://blog.csdn.net/briblue/article/details/70211942

系列文章

《RecyclerView使用指南(一)—— 基本使用》
《RecyclerView使用指南(二)—— 多种ItemLayout》
《RecyclerView使用指南(三)—— 添加分割线和点击事件》
《RecyclerView使用指南(四)—— 使用ItemDecoration》
《RecyclerView使用指南(五)—— 实现吸顶效果》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容