这些项目至少有一个共同点:到处都有项目清单。例如,电话簿联系人列表或您的个人资料设置列表。
我们的项目使用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,b和c。
- 所有单元格水平缩进16dp。
- 单元格b的垂直偏移也为8dp。
- 如果单元格a是组中的第一个单元格,则在顶部具有圆形边缘;如果是组中的最后一个单元格,则在底部具有圆形边缘。
- 在具有的单元之间绘制分隔线,但组中最后一个单元之后不应有分隔线。
- 在单元格c的背景下绘制了具有视差效果的图片。
它应该像这样结束:

让我们考虑解决的选项:
用不同类型的单元格填充列表。
您可以编写自己的适配器,也可以使用自己喜欢的库。
我将使用EasyAdapter。
缩进单元格。
有三种方法:
- 为RecyclerView设置paddingStart和paddingEnd。
如果不是所有单元格都具有相同的缩进,则此解决方案将不起作用。 - 在单元格上设置layout_marginStart和layout_marginEnd。
您将必须向列表中的所有单元格添加相同的缩进。 - 编写ItemDecoration的实现,并重写getItemOffsets方法。
该解决方案已经更好了,它将变得更加通用和可重用。
组单元的圆角。
解决方案似乎很明显:我想立即添加一些枚举{Start,Middle,End}并将其与数据一起放入单元格中。但是缺点立即弹出:
- 列表中的数据模型变得更加复杂。
- 对于此类操作,您将必须预先计算要分配给每个单元格的枚举。
- 在将元素删除/添加到列表后,您将不得不重新计算。
- ItemDecoration。您可以了解组中的哪个单元格,并使用onDraw * ItemDecoration方法正确绘制背景。
绘图分隔线。
在单元格内绘制分隔线是一个坏习惯,因为结果将是复杂的布局,复杂的屏幕将无法动态显示分隔线。因此ItemDecoration再次获胜。 sdk中现成的DeviderItemDecoration对我们不起作用,因为它会在每个单元格之后绘制分隔线,而这无法立即解决。您需要编写自己的实现。
在单元格的背景上的视差。
可能会想到将RecyclerView OnScrollListener放入并使用一些自定义View渲染图片的想法。但是,这里的ItemDecoration可以帮助我们,因为它可以访问Canvas Recycler和所有必要的参数。
总共,我们至少需要编写4个ItemDecoration实现。可以将所有要点简化为仅与ItemDecoration一起使用,而不涉及功能的布局和业务逻辑,这是非常好。另外,如果我们在应用程序中有类似的情况,则所有ItemDecoration实现都可以重用。
但是,在过去的几年中,复杂的列表越来越多地出现在我们的项目中,并且每次我们必须编写一个ItemDecoration集来满足项目的需求时。需要一个更加通用和灵活的解决方案,以便可以在其他项目中重用。
您想要实现什么目标:
- 编写尽可能少的ItemDecoration继承人。
- 将“画布”和填充上的渲染逻辑分开。
- 具有使用onDraw和onDrawOver方法的好处。
- 使装饰器的自定义更灵活(例如,按条件而不是所有单元格绘制分隔线)。
- 在不参考Dividers的情况下做出决定,因为ItemDecoration不仅能够绘制水平和垂直线。
- 通过查看示例项目可以很容易地利用它。
结果,我们有了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 |
带有示例的源代码
结论
尽管ItemDecoration接口很简单,但是它允许您在不更改布局的情况下使用列表执行复杂的操作。我希望我能够证明它是一个足够强大的工具,值得您注意。而且我们的资料库将帮助您更轻松地装饰列表。
谢谢大家的关注,我很高兴您的评论。
UPD:08/06/2020为Sticky标头添加了示例