RecyclerView.ItemDecoration:充分利用它

您好,亲爱的哈伯读者。我叫Oleg Zhilo,在过去的四年中,我一直是Surf的一名Android开发人员。在这段时间里,我参加了各种很酷的项目,但是我也有机会使用遗留代码。



这些项目至少有一个共同点:到处都有项目清单。例如,电话簿联系人列表或您的个人资料设置列表。



我们的项目使用RecyclerView作为列表。我不会告诉您如何为RecyclerView编写适配器或如何正确更新列表中的数据。在我的文章中,我将讨论另一个重要且经常被忽视的组件-RecyclerView.ItemDecoration,我将向您展示如何在列表的布局中使用它以及它可以做什么。







除了列表中的数据,RecyclerView还包含重要的装饰元素,例如,单元格分隔符,滚动条。在这里,RecyclerView.ItemDecoration将帮助我们绘制整个装饰,并且不会在单元格和屏幕的布局中产生不必要的View。



ItemDecoration是具有3种方法的抽象类:



在呈现ViewHolder之前呈现装饰的方法



public void onDraw(Canvas c, RecyclerView parent, State state)


渲染ViewHolder后渲染装饰的方法



public void onDrawOver(Canvas c, RecyclerView parent, State state)


填充RecyclerView时缩进ViewHolder的方法



public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)


通过onDraw *方法的签名,您可以看到使用了3个主要组件来绘制装饰。



  • 画布-用于渲染必要的装饰
  • RecyclerView-用于访问RecyclerVIew本身的参数
  • RecyclerView.State-包含有关RecyclerView状态的信息


连接到RecyclerView



有两种方法可以将ItemDecoration实例连接到RecyclerView:



public void addItemDecoration(@NonNull ItemDecoration decor)
public void addItemDecoration(@NonNull ItemDecoration decor, int index)


所有连接的RecyclerView.ItemDecoration实例都添加到一个列表中,并且所有实例都立即呈现。



此外,RecyclerView还具有使用ItemDecoration进行操作的其他方法。

通过索引删除ItemDecoration



public void removeItemDecorationAt(int index)


删除ItemDecoration实例



public void removeItemDecoration(@NonNull ItemDecoration decor)


通过索引获取ItemDecoration



public ItemDecoration getItemDecorationAt(int index)


在RecyclerView中获取连接的ItemDecoration的当前计数



public int getItemDecorationCount()


重画当前的ItemDecoration列表



public void invalidateItemDecorations()


该SDK已经具有RecyclerView.ItemDecoration的继承人,例如DeviderItemDecoration。它允许您绘制单元格的分隔符。



它的工作原理非常简单,您需要使用drawable,并且DeviderItemDecoration会将其绘制为单元格分隔符。



让我们创建divider_drawable.xml:



<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:height="1dp" />
    <solid android:color="@color/gray_A700" />
</shape>


并将DividerItemDeoration连接到RecyclerView:



val dividerItemDecoration = DividerItemDecoration(this, RecyclerView.VERTICAL)
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider_drawable))
recycler_view.addItemDecoration(dividerItemDecoration)


我们得到:





简单场合的理想选择。



一切在DeviderItemDecoration的“幕后”中都是基本的:




final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
     final View child = parent.getChildAt(i);
     parent.getDecoratedBoundsWithMargins(child, mBounds);
     final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
     final int top = bottom - mDivider.getIntrinsicHeight();
     mDivider.setBounds(left, top, right, bottom);
     mDivider.draw(canvas);
}


对于每个onDraw(...)调用,在RecyclerView中循环浏览所有当前View并绘制传递的drawable。



但是,与包含相同元素的列表相比,屏幕可以包含更复杂的布局元素。屏幕可包括:



一。几种类型的细胞;

b。几种类型的分隔线;

C。细胞可以具有倒圆的边缘。

d。单元可以根据某些条件具有不同的垂直和水平凹痕;

e。所有上述所有。



让我们看一下e点。让我们为自己设定一个艰巨的任务并考虑其解决方案。



任务:



  • 屏幕上有3种独特的单元格,我们称它们为a,bc
  • 所有单元格水平缩进16dp。
  • 单元格b的垂直偏移也为8dp。
  • 如果单元格a是组中的第一个单元格,则在顶部具有圆形边缘;如果是组中的最后一个单元格,则在底部具有圆形边缘。
  • 在具有的单元之间绘制分隔线,但组中最后一个单元之后不应有分隔线。
  • 在单元格c的背景下绘制了具有视差效果的图片。


它应该像这样结束:





让我们考虑解决的选项:



用不同类型的单元格填充列表。



您可以编写自己的适配器,也可以使用自己喜欢的库。

我将使用EasyAdapter



缩进单元格。



有三种方法:



  1. 为RecyclerView设置paddingStart和paddingEnd。

    如果不是所有单元格都具有相同的缩进,则此解决方案将不起作用。
  2. 在单元格上设置layout_marginStart和layout_marginEnd。

    您将必须向列表中的所有单元格添加相同的缩进。
  3. 编写ItemDecoration的实现,并重写getItemOffsets方法。

    该解决方案已经更好了,它将变得更加通用和可重用。


组单元的圆角。



解决方案似乎很明显:我想立即添加一些枚举{Start,Middle,End}并将其与数据一起放入单元格中。但是缺点立即弹出:



  • 列表中的数据模型变得更加复杂。
  • 对于此类操作,您将必须预先计算要分配给每个单元格的枚举。
  • 在将元素删除/添加到列表后,您将不得不重新计算。
  • ItemDecoration。您可以了解组中的哪个单元格,并使用onDraw * ItemDecoration方法正确绘制背景。


绘图分隔线。



在单元格内绘制分隔线是一个坏习惯,因为结果将是复杂的布局,复杂的屏幕将无法动态显示分隔线。因此ItemDecoration再次获胜。 sdk中现成的DeviderItemDecoration对我们不起作用,因为它会在每个单元格之后绘制分隔线,而这无法立即解决。您需要编写自己的实现。



在单元格的背景上的视差。



可能会想到将RecyclerView OnScrollListener放入并使用一些自定义View渲染图片的想法。但是,这里的ItemDecoration可以帮助我们,因为它可以访问Canvas Recycler和所有必要的参数。



总共,我们至少需要编写4个ItemDecoration实现。可以将所有要点简化为仅与ItemDecoration一起使用,而不涉及功能的布局和业务逻辑,这是非常好。另外,如果我们在应用程序中有类似的情况,则所有ItemDecoration实现都可以重用。



但是,在过去的几年中,复杂的列表越来越多地出现在我们的项目中,并且每次我们必须编写一个ItemDecoration集来满足项目的需求时。需要一个更加通用和灵活的解决方案,以便可以在其他项目中重用。



您想要实现什么目标:



  1. 编写尽可能少的ItemDecoration继承人。
  2. 将“画布”和填充上的渲染逻辑分开。
  3. 具有使用onDraw和onDrawOver方法的好处。
  4. 使装饰器的自定义更灵活(例如,按条件而不是所有单元格绘制分隔线)。
  5. 在不参考Dividers的情况下做出决定,因为ItemDecoration不仅能够绘制水平和垂直线。
  6. 通过查看示例项目可以很容易地利用它。


结果,我们有了RecyclerView装饰器



该库具有一个简单的Builder接口,用于使用Canvas和缩进的单独接口,以及使用onDraw和onDrawOver方法的功能。ItemDecoration实现只是其中之一。



让我们回到问题所在,看看如何使用库解决问题。

我们的装饰器的生成器看起来很简单:




Decorator.Builder()
            .underlay()
            ...
            .overlay()
            ...
            .offset()
            ...
            .build()


  • .underlay(...)-在ViewHolder下呈现所需。
  • .overlay(...)-需要绘制ViewHolder。
  • .offset(...)-用于设置ViewHolder的偏移量。


有3个用于绘制装饰和设置缩进的界面。



  • RecyclerViewDecor-将装饰呈现到RecyclerView。
  • ViewHolderDecor-将装饰呈现到RecyclerView,但可以访问ViewHolder。
  • OffsetDecor-用于设置缩进。


但这还不是全部。可以使用viewType将ViewHolderDecor和OffsetDecor绑定到特定的ViewHolder,这使您可以在一个列表甚至一个单元格中组合几种类型的装饰。如果未传递viewType,则ViewHolderDecor和OffsetDecor将应用于RecyclerView中的所有ViewHolders。 RecyclerViewDecor没有这样的机会,因为它被设计为通常与RecyclerView一起使用,而不是与ViewHolders一起使用。另外,可以将相同的ViewHolderDecor / RecyclerViewDecor实例传递到覆盖(...)和底层(...)。



让我们开始编写代码



EasyAdapter库使用ItemControllers创建一个ViewHolder。简而言之,他们负责创建和识别ViewHolder。对于我们的示例,一个控制器就足够了,它可以显示不同的ViewHolders。最主要的是viewType对于每个单元格布局都是唯一的。看起来像这样:



private val shortCardController = Controller(R.layout.item_controller_short_card)
private val longCardController = Controller(R.layout.item_controller_long_card)
private val spaceController = Controller(R.layout.item_controller_space)


要设置缩进量,我们需要OffsetDecor的后代:



class SimpleOffsetDrawer(
    private val left: Int = 0,
    private val top: Int = 0,
    private val right: Int = 0,
    private val bottom: Int = 0
) : Decorator.OffsetDecor {

    constructor(offset: Int) : this(offset, offset, offset, offset)

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        recyclerView: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.set(left, top, right, bottom)
    }
}


要绘制圆角,ViewHolder需要ViewHolderDecor的继承者。在这里,我们需要一个OutlineProvider,以便将按下状态也修剪到边缘。



class RoundDecor(
    private val cornerRadius: Float,
    private val roundPolitic: RoundPolitic = RoundPolitic.Every(RoundMode.ALL)
) : Decorator.ViewHolderDecor {

    override fun draw(
        canvas: Canvas,
        view: View,
        recyclerView: RecyclerView,
        state: RecyclerView.State
    ) {

        val viewHolder = recyclerView.getChildViewHolder(view)
        val nextViewHolder =
            recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition + 1)
        val previousChildViewHolder =
            recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition - 1)

        if (cornerRadius.compareTo(0f) != 0) {
            val roundMode = getRoundMode(previousChildViewHolder, viewHolder, nextViewHolder)
            val outlineProvider = view.outlineProvider
            if (outlineProvider is RoundOutlineProvider) {
                outlineProvider.roundMode = roundMode
                view.invalidateOutline()
            } else {
                view.outlineProvider = RoundOutlineProvider(cornerRadius, roundMode)
                view.clipToOutline = true
            }
        }
    }
}


为了绘制分隔线,我们将再写一个ViewHolderDecor继承人:



class LinearDividerDrawer(private val gap: Gap) : Decorator.ViewHolderDecor {

    private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val alpha = dividerPaint.alpha

    init {
        dividerPaint.color = gap.color
        dividerPaint.strokeWidth = gap.height.toFloat()
    }

    override fun draw(
        canvas: Canvas,
        view: View,
        recyclerView: RecyclerView,
        state: RecyclerView.State
    ) {
        val viewHolder = recyclerView.getChildViewHolder(view)
        val nextViewHolder = recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition + 1)

        val startX = recyclerView.paddingLeft + gap.paddingStart
        val startY = view.bottom + view.translationY
        val stopX = recyclerView.width - recyclerView.paddingRight - gap.paddingEnd
        val stopY = startY

        dividerPaint.alpha = (view.alpha * alpha).toInt()

        val areSameHolders =
            viewHolder.itemViewType == nextViewHolder?.itemViewType ?: UNDEFINE_VIEW_HOLDER

        val drawMiddleDivider = Rules.checkMiddleRule(gap.rule) && areSameHolders
        val drawEndDivider = Rules.checkEndRule(gap.rule) && areSameHolders.not()

        if (drawMiddleDivider) {
            canvas.drawLine(startX.toFloat(), startY, stopX.toFloat(), stopY, dividerPaint)
        } else if (drawEndDivider) {
            canvas.drawLine(startX.toFloat(), startY, stopX.toFloat(), stopY, dividerPaint)
        }
    }
}


要配置我们的divader,我们将使用Gap.kt类:



class Gap(
    @ColorInt val color: Int = Color.TRANSPARENT,
    val height: Int = 0,
    val paddingStart: Int = 0,
    val paddingEnd: Int = 0,
    @DividerRule val rule: Int = MIDDLE or END
)


它将有助于调整



分隔线的颜色,高度,水平填充和绘制规则,而ViewHolderDecor的最后一个继承者仍然保留。用于绘制具有视差效果的图片。



class ParallaxDecor(
    context: Context,
    @DrawableRes resId: Int
) : Decorator.ViewHolderDecor {

    private val image: Bitmap? = AppCompatResources.getDrawable(context, resId)?.toBitmap()

    override fun draw(
        canvas: Canvas,
        view: View,
        recyclerView: RecyclerView,
        state: RecyclerView.State
    ) {

        val offset = view.top / 3
        image?.let { btm ->
            canvas.drawBitmap(
                btm,
                Rect(0, offset, btm.width, view.height + offset),
                Rect(view.left, view.top, view.right, view.bottom),
                null
            )
        }
    }
}


让我们现在将所有内容放在一起。



private val decorator by lazy {
        Decorator.Builder()
            .underlay(longCardController.viewType() to roundDecor)
            .underlay(spaceController.viewType() to paralaxDecor)
            .overlay(shortCardController.viewType() to dividerDrawer2Dp)
            .offset(longCardController.viewType() to horizontalOffsetDecor)
            .offset(shortCardController.viewType() to horizontalOffsetDecor)
            .offset(spaceController.viewType() to horizontalAndVerticalOffsetDecor)
            .build()
    }


我们初始化RecyclerView,向其添加装饰器和控制器:



private fun init() {
        with(recycler_view) {
            layoutManager = LinearLayoutManager(this@LinearDecoratorActivityView)
            adapter = easyAdapter
            addItemDecoration(decorator)
            setPadding(0, 16.px, 0, 16.px)
        }

        ItemList.create()
            .apply {
                repeat(3) {
                    add(longCardController)
                }
                add(spaceController)
                repeat(5) {
                    add(shortCardController)
                }
            }
            .also(easyAdapter::setItems)
    }


就这样。我们列表上的装饰已经准备就绪。



我们设法编写了一组可以轻松重用和灵活定制的装饰器。



让我们看看如何应用装饰器。



卧式RecyclerView的PageIndicator

CarouselDecoratorActivityView.kt


气泡聊天消息和滚动条:

ChatActivityView.kt


一个更复杂的情况-绘制形状,图标,更改主题而无需重新加载屏幕:

TimeLineActivity.kt




粘页眉



StickyHeaderDecor.kt


带有示例的源代码



结论



尽管It​​emDecoration接口很简单,但是它允许您在不更改布局的情况下使用列表执行复杂的操作。我希望我能够证明它是一个足够强大的工具,值得您注意。而且我们的资料库将帮助您更轻松地装饰列表。



谢谢大家的关注,我很高兴您的评论。



UPD:08/06/2020为Sticky标头添加了示例



All Articles