RecyclerView.ItemDecoration:吸顶效果实现

此效果由 RecyclerView.ItemDecoration 实现
RecyclerView.ItemDecoration此类包含三个方法
  • onDraw(Canvas c, RecyclerView parent, State state)
    • 为divider设置绘制范围,并绘制到canvas上。绘制范围可以超出getItemOffsets设置的范围,但会绘制在item的下面
  • onDrawOver(Canvas c, RecyclerView parent, State state)
    • 绘制在最上层,绘制位置不受限
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
    • outRect设置4个方向的值,将被计算进所有decoration的尺寸中,而这个尺寸会被计入RecyclerView的每个item的padding中
  • 执行顺序:decoration 的 onDraw,child view 的 onDraw,decoration 的 onDrawOver
实现逻辑
  1. 调用getItemOffsets给每个item设置偏移量,根据位置(或者其他)设置不同偏移量,留出操作空间
  2. 调用onDraw方法,在其不同的偏移范围作出不同操作
  3. onDrawOver绘制在最上层,顶上去的逻辑在这搞

getItemOffsets代码我是这么写的
/**
*      每个item的偏移量,在顶部留出空间
*/
override fun getItemOffsets(
    outRect: Rect,        //    绘画的工具类,画一个长方形
    view: View,           //    当前的item
    parent: RecyclerView,
    state: RecyclerView.State
) {
    super.getItemOffsets(outRect, view, parent, state)
    if (parent.adapter is StarAdapter) {
        var adapter : StarAdapter = parent.adapter as StarAdapter
        var position: Int = parent.getChildLayoutPosition(view)
        if (adapter.isGroupHead(position)) {
            //  如果是头部,留更多空间
            outRect.set(0, groupHeaderHeight, 0, 0)
        } else {
            //  如果不是头部,留一条线的空间
            outRect.set(0, 4, 0, 0)
        }
    }
}
然鹅,为啥调用了outRect.set()方法就能使item偏移了呢,咋实现的?
先看看它是在哪调用的,它在RecyclerView中唯一被调用的地方就是 getItemDecorInsetsForChild(View child) 函数,代码如下
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();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        //   遍历,在此把每个item的偏移量放进 mTempRect
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        //    把每个item的偏移量放进 inserts 中
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}
看到这有点蒙,遍历把所有的item偏移量放进了 insets ,也就是说insets记录了所有ItemDecoration所需尺寸的总和,继续深入
在 RecyclerView 的 measureChild(View child, int widthUsed, int heightUsed) 函数中,调用了 getItemDecorInsetsForChild,并把它算在了 child view 的 padding 中
/**
* Measure a child view using standard measurement policy, taking the padding
* of the parent RecyclerView and any added item decorations into account.
*
* <p>If the RecyclerView can be scrolled in either dimension the caller may
* pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
*
* @param child      Child view to measure
* @param widthUsed  Width in pixels currently consumed by other views, if relevant
* @param heightUsed Height in pixels currently consumed by other views, if relevant
*/
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}
上面这段代码中调用 getChildMeasureSpec 函数的第三个参数就是 child view 的 padding,而这个参数就把 insets 的值算进去了。那么现在就可以确认了,getItemOffsets 中为 outRect 设置的4个方向的值,将被计算进所有 decoration 的尺寸中,而这个尺寸,被计入了 RecyclerView 每个 item view 的 padding 中。我们操作的空间也正在这个padding中

空间偏移出来后咱们开始画,如果是一组的开头咱们给它写点东西,如果不是则画条红色的线
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDraw(c, parent, state)
    //  parent.getAdapter() instanceof StarAdapter ===> parent.adapter is StarAdapter
    if (parent.adapter is StarAdapter) {
        var adapter :StarAdapter = parent.adapter as StarAdapter
        //  当前屏幕 item 个数
        var count: Int = parent.childCount
        var left: Float = parent.paddingLeft.toFloat()
        var right: Float = (parent.width - parent.paddingRight).toFloat()
        for (i in 0 until count) {
            // 获取对应i的View
            var view: View = parent.getChildAt(i)
            // 获取View的布局位置
            var position: Int = parent.getChildLayoutPosition(view)
            // 是否是头部
            var isHeader:Boolean = adapter.isGroupHead(position)
            //  要考虑 xml 中设置的 padding;如果是一组的第一个则在偏移空间写入内容
            if (isHeader && view.top - groupHeaderHeight - parent.paddingTop >= 0) {
                //  画出头部长方形区域
                c.drawRect(
                    left, (view.top - groupHeaderHeight).toFloat(),
                    right, view.bottom.toFloat(), headPaint)
                //  写入的内容
                var groupName: String = adapter.getGroupName(position)
                //  textRect,获得文字的宽高
                textPaint.getTextBounds(groupName, 0, groupName.length, textRect)
                c.drawText(groupName, left + 20,
                    (view.top - groupHeaderHeight / 2 + textRect.height() / 2).toFloat(),
                    textPaint
                )
            } else {
                //  分割线
                c.drawRect(left, (view.top-4).toFloat(),
                    right, view.top.toFloat(),headPaint)
            }
        }
    }
}

最后则是吸顶效果实现,onDrawOver画的内容在最上层
  1. 一组的最上层没到来之前显示上一组的,来了则换下一组的
  2. 头部顶上去咋实现?根据界面第一个item的top位置,设置顶上吸附模块的高度与内容位置
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
    super.onDrawOver(c, parent, state)
    if (parent.adapter is StarAdapter) {
        var adapter: StarAdapter = parent.adapter as StarAdapter
        // 返回可见区域第一个item
        // 返回可见区域内的第一个item的position
        var position:Int =(parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
        // 获取对应position的View
        val itemView = parent.findViewHolderForAdapterPosition(position)!!.itemView
        var left: Float = parent.paddingLeft.toFloat()
        var right: Float = (parent.width - parent.paddingRight).toFloat()
        var top: Float = parent.paddingTop.toFloat()
        //  此处应判别可见区域第二个是否是一组的开始
        var isHeader: Boolean = adapter.isGroupHead(position + 1)
        var groupName: String = adapter.getGroupName(position)
        if (isHeader) {
            //  可见区域第一个item在界面留下的空间也就是被顶模块的空间
            //  随着可见区域第一个item上移,吸顶模块的高度逐渐压缩
            var bottom: Int = min(groupHeaderHeight, itemView.bottom - parent.paddingTop)
            c.drawRect(left, top, right, top + bottom, headPaint)
            textPaint.getTextBounds(groupName, 0, groupName.length, textRect)
            c.drawText(groupName, left + 20,
                top + bottom - groupHeaderHeight / 2 + textRect.height() / 2,
                textPaint)
        } else {
            //  如果不是一组的开始,则保持吸顶
            c.drawRect(left, top, right, top + groupHeaderHeight,
                headPaint)
            val groupName = adapter.getGroupName(position)
            textPaint.getTextBounds(groupName, 0, groupName.length, textRect)
            c.drawText(groupName, left + 20,
                top + groupHeaderHeight / 2 + textRect.height() / 2, textPaint)
        }
    }
}
效果实现
github
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值