ItemDecoration实现RecyclerView item吸顶效果

ItemDecoration实现RecyclerView item吸顶效果

在这里插入图片描述

ItemDecoration可以帮助我们绘制RecyclerView item的分割线,吸顶效果就借助它来实现,效果如下:

再实战之前先弄清ItemDecoration的原理

ItemDecoration的原理

首先定义一个CustomDecoration类继承自ItemDecoration,使用ItemDecoration时,一般重写三个方法 getItemOffsetsonDraw ,onDrawOver

对于RecyclerView的使用不再赘述,其itemView的样式如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:gravity="center"
        android:textColor="@color/white"
        android:textSize="20sp" />

</RelativeLayout>

未加入ItemDecoration的实现效果

addItemDecoration

在使用ItemDecoration肯定会去调用addItemDecoration,看一下源码做了什么事情

RecyclerView#addItemDecoration

public void addItemDecoration(@NonNull ItemDecoration decor) {
    addItemDecoration(decor, -1);
}

public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
     ...
    //往集合添加一个元素
     mItemDecorations.add(decor);
    ...
}

getItemOffsets

Item绘制ItemDecoration提供空间

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    //左,上,右,下分别代表给item多分配多大的空间,这里是给每个item的上面多要15个单位空间
    outRect.set(0, 15, 0, 0);
}

效果图

为啥能多分配空间呢?

fill进行分析,fill方法是RecyclerView布局必走的方法对RecyclerView 进行填充,未来会在另一篇文章中进行解释,这里只需要知道fill是必走的即可

LinearLayoutManager#fill -> LinearLayoutManager#layoutChunk -> RecyclerView#measureChildWithMargins -> RecyclerView#getItemDecorInsetsForChild -> ItemDecoration.getItemOffsets

回到问题上,为啥就能预留空间呢? measureChildWithMargins开始分析

测量过程

RecyclerView#measureChildWithMargins

public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
	//返回Rect,根据返回的Rect测量,往下看getItemDecorInsetsForChild的解析
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    //insets是集合了全部Decoration的预留信息,加起来也就是多测量了一些空间,要比之前的item大
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
	//设置宽高信息
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight()
                    + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom()
                    + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

RecyclerView#getItemDecorInsetsForChild

Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        return lp.mDecorInsets;
    }

    if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
        // changed/invalid items should not be updated until they are rebound.
        return lp.mDecorInsets;
    }
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    //不一定只有一个Decoration,这里进行循环遍历
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        //getItemOffsets也就是对mTempRect进行操作,上面我们outRect.set(0, 15, 0, 0)也就是对mTempRect及逆行赋值
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        //把值添加进insets
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    //返回insets
    return insets;
}
布局过程

LinearLayoutManager#layoutChunk 执行完RecyclerView#measureChildWithMargins 会执行layoutDecoratedWithMargins

RecyclerView#layoutDecoratedWithMargins

//child是layoutChunk传过来的,代表正在布局哪个item
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
                                       int bottom) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    //拿到view的ItemDecoration大小信息
    final Rect insets = lp.mDecorInsets;
    //算上Decoration的信息,一块计算上下足左右,在布局的时候也就把相应的那一块给预留了出来
    child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                 right - insets.right - lp.rightMargin,
                 bottom - insets.bottom - lp.bottomMargin);
}

onDraw

下层画布绘制

@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDraw(c, parent, state);
    if (parent.getAdapter() instanceof CustomAdapter) {
        CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
        int count = parent.getChildCount();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        for (int i = 0; i < count; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(view);
            c.drawRect(left, view.getTop() - 100, right, view.getTop(), headPaint);
        }

    }
}

效果图

onDrawOver

覆盖画布绘制

@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    if (parent.getAdapter() instanceof CustomAdapter) {
        CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
        int count = parent.getChildCount();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        for (int i = 0; i < count; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(view);
            c.drawRect(left, view.getTop() - 80, right, view.getTop(), headPaint);
        }

    }

}

效果图

onDraw和onDrawOver对比

onDraw没有覆盖原先的itemonDrawOveritem覆盖掉了,为什么会有这个现象呢

view在绘制的时候会先走drawdraw会走onDraw

RecyclerView#draw

@Override
public void draw(Canvas c) {
    super.draw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        //调用onDrawOver进行覆盖绘制
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ...
}

super.draw( c ) == View#draw

public void draw(Canvas canvas) {
	...
    onDraw(canvas);
    //第二步先走ViewGroup的dispatchDraw,再调用drawChild,再到child.draw(canvas, this, drawingTime)绘制子view再回头看RecyclerView#draw
    dispatchDraw(canvas);
    ...
}

RecyclerView#onDraw

@Override
public void onDraw(Canvas c) {
    super.onDraw(c);
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        //第一步先走ItemDecoration的onDraw,再回头看View#draw
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

综上分析,上方的效果是因为绘制方法的调用顺序不同

第一步 ItemDecorationonDraw

第二步 ItemViewonDraw

第三步 ItemDecorationonDrawOver

吸顶实现

经上原理的分析,定义一个继承自RecyclerView.ItemDecoration的类,重写上面三个方法即可实现。

数据Bean只有两个属性名字和组名

现在分步实现文章开头的效果

给绘制区域预留空间

预留空间要重写getItemOffsets方法,想要的效果是头部预留更大的空间,非头部需要预留一个小空间绘制分割线.

判断是否是头部

在预留之前需要判断是否是头部,这个方法封装在RecyclerView Adapter比较合适,因为getItemOffsets的参数返回了RecyclerView 和当前的ItemView

RecyclerView提供了根据View获取下标的方法,拿到下标即可在Adapter拿到Bean数据,根据Bean即可判断是否是头部

CustomAdapter#isGourpHeader

//判断当前位置是否是顶部
public boolean isGourpHeader(int position) {
    //如果是0直接就是顶
    if (position == 0) {
        return true;
    } else {
        //得到当前组名
        String currentGroupName = getGroupName(position);
        //拿到前一个组名
        String preGroupName = getGroupName(position - 1);
        //和前一个相同则为false
        if (preGroupName.equals(currentGroupName)) {
            return false;
        } else {
            return true;
        }
    }
}

public String getGroupName(int position) {
    return dataList.get(position).getGroundName();
}
预留空间

CustomDecoration#getItemOffsets

@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    if (parent.getAdapter() instanceof CustomAdapter) {
        //获取adapter
        CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
        //根据itemview获取下标
        int position = parent.getChildLayoutPosition(view);
        //调用之前封装的isGourpHeader方法
        boolean isGroupHeader = adapter.isGourpHeader(position);
        if (isGroupHeader) {
            //为头设置100空间
            outRect.set(0, groupHeaderHeight, 0, 0);
        } else {
            //不为头设置2空间
            outRect.set(0, 2, 0, 0);
        }
    }

}
效果图

实现不吸顶的效果

根据之前onDrawonDrawOver的解析,不吸顶需要实现onDraw

@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDraw(c, parent, state);
    if (parent.getAdapter() instanceof CustomAdapter) {
        //获取adapter
        CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
        //拿到全部的子View
        int count = parent.getChildCount();
        //获取左右位置
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        //对每个Decoration进行绘制
        for (int i = 0; i < count; i++) {
            //获取itemView的下标
            View view = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(view);
            if (adapter.isGourpHeader(position)) {
                //绘制背景
                c.drawRect(left, view.getTop() - groupHeaderHeight, right, view.getTop(), headPaint);
                String groupName = adapter.getGroupName(position);
                //绘制文字
                c.drawText(groupName, left + 20, view.getTop() -
                           groupHeaderHeight / 2, textPaint);
            } else {
                //绘制分割线
                c.drawRect(left, view.getTop() - 2, right, view.getTop(), headPaint);
            }
        }

    }
}
效果与上面预留空间一样只是颜色不同

实现吸顶

吸顶是在最上层画布绘制,因此重写onDrawOver

思路:LayoutManager给我们提供了获取当前屏幕第一个ItemView的下标的方法,可以获取下标意味着可以获取对应的itemView。我们要思考的是啥时候会切换顶部置顶(根据以上的数据进行判断),当第二个为顶部且第一个itemViewbottomItemDecorationbottom要小的时候,此时就需要将当前顶部顶上去切换下一组,这是满足切换顶部的唯一条件,看下面实现

@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
    super.onDrawOver(c, parent, state);
    if (parent.getAdapter() instanceof CustomAdapter) {
        //获取Adapter
        CustomAdapter adapter = (CustomAdapter) parent.getAdapter();
        // 返回可见区域内的第一个item的position
        int position = ((LinearLayoutManager) parent.getLayoutManager()).findFirstVisibleItemPosition();
        // 获取对应position的View
        View itemView = parent.findViewHolderForAdapterPosition(position).itemView;
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int top = parent.getPaddingTop();

        boolean isGroupHeader = adapter.isGourpHeader(position + 1);
        //当第二个是组的头部的时候
        if (isGroupHeader) {
            //谁更靠近上方用谁的bottom
            int bottom = Math.min(groupHeaderHeight, itemView.getBottom());
            //绘制背景
            c.drawRect(left, top, right, top + bottom, headPaint);
            String groupName = adapter.getGroupName(position);
            //绘制文字
            c.drawText(groupName, left + 20, top + bottom
                       - groupHeaderHeight / 2, textPaint);
        } else {
            //置顶不变
            c.drawRect(left, top, right, top + groupHeaderHeight, headPaint);
            String groupName = adapter.getGroupName(position);
            c.drawText(groupName, left + 20, top + groupHeaderHeight / 2, textPaint);
        }

    }

}
效果图

paddingTop的BUG

RecyclerView 设置paddingTop会导致下面情况发生:

]

先弄清楚此bug是哪个方法绘制的,发现并没有覆盖,则一定是onDraw绘制的,所以要修改onDraw方法

修改绘制条件:

//当前view的顶部如果没有超过最上先就可以绘制
if(view.getTop() - groupHeaderHeight - parent.getPaddingTop() >= 0) {
	//绘制操作
}

加上以后的效果图:

]

此时发现顶不上去了,是因为求较小值的代码失效了

//groupHeaderHeight永远比itemView.getBottom()小,导致失效
//int bottom = Math.min(groupHeaderHeight, itemView.getBottom());
//上方代码修改为
int bottom = Math.min(groupHeaderHeight, itemView.getBottom() - parent.getPaddingTop());

效果图

发现文字被顶上去了,根据之前文字绘制文章中的知识

onDrawOver绘制文字前对画布进行裁剪

c.clipRect(left, top, right, top + bottom);

最终效果

demo点赞加评论找我要哦

原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下}

👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!}

⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!}

✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值