Esses projetos têm pelo menos uma coisa em comum: há uma lista de itens em todos os lugares. Por exemplo, uma lista de contatos da agenda telefônica ou uma lista de suas configurações de perfil.
Nossos projetos usam RecyclerView para listas. Não vou lhe dizer como escrever um adaptador para o RecyclerView ou como atualizar corretamente os dados da lista. Em meu artigo, vou falar sobre outro componente importante e muitas vezes esquecido - RecyclerView.ItemDecoration, vou mostrar como usá-lo para o layout de lista e o que ele pode fazer.
Além dos dados da lista, o RecyclerView também contém elementos decorativos importantes, por exemplo, separadores de células, barras de rolagem. E aqui RecyclerView.ItemDecoration nos ajudará a desenhar toda a decoração e não produzir Views desnecessárias no layout das células e na tela.
ItemDecoration é uma classe abstrata com 3 métodos:
Método para renderizar decoração antes de renderizar ViewHolder
public void onDraw(Canvas c, RecyclerView parent, State state)
Método para renderizar decoração após renderizar ViewHolder
public void onDrawOver(Canvas c, RecyclerView parent, State state)
Método para recuar ViewHolder ao preencher RecyclerView
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
Pela assinatura dos métodos onDraw *, você pode ver que 3 componentes principais são usados para desenhar a decoração.
- Tela - para renderizar a decoração necessária
- RecyclerView - para acessar os parâmetros do próprio RecyclerVIew
- RecyclerView.State - contém informações sobre o estado do RecyclerView
Conectando ao RecyclerView
Existem dois métodos para conectar uma instância ItemDecoration ao RecyclerView:
public void addItemDecoration(@NonNull ItemDecoration decor)
public void addItemDecoration(@NonNull ItemDecoration decor, int index)
Todas as instâncias de RecyclerView.ItemDecoration conectadas são adicionadas a uma lista e todas são renderizadas de uma vez.
Além disso, RecyclerView possui métodos adicionais para manipular com ItemDecoration.
Removendo ItemDecoration por Índice
public void removeItemDecorationAt(int index)
Removendo uma instância ItemDecoration
public void removeItemDecoration(@NonNull ItemDecoration decor)
Obter ItemDecoration por índice
public ItemDecoration getItemDecorationAt(int index)
Obtenha a contagem atual de ItemDecoration conectado no RecyclerView
public int getItemDecorationCount()
Redesenha a lista atual de itens de decoração
public void invalidateItemDecorations()
O SDK já possui herdeiros de RecyclerView.ItemDecoration, por exemplo, DeviderItemDecoration. Ele permite que você desenhe separadores para células.
Ele funciona de forma muito simples, você precisa usar um drawable e DeviderItemDecoration irá desenhá-lo como um separador de células.
Vamos criar 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>
E conecte o DividerItemDeoration ao RecyclerView:
val dividerItemDecoration = DividerItemDecoration(this, RecyclerView.VERTICAL)
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider_drawable))
recycler_view.addItemDecoration(dividerItemDecoration)
Nós temos:

Ideal para ocasiões simples.
Tudo é elementar sob o "capô" de 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);
}
Para cada chamada onDraw (...), faça um loop por toda a View atual no RecyclerView e desenhe o drawable passado.
Mas a tela pode conter elementos de layout mais complexos do que uma lista de elementos idênticos. A tela pode incluir:
a. Vários tipos de células;
b. Vários tipos de divisórias;
c. As células podem ter bordas arredondadas;
d. As células podem ter recuos verticais e horizontais diferentes dependendo de algumas condições;
e. Todos os itens acima de uma vez.
Vejamos o ponto e. Vamos nos colocar em uma tarefa difícil e considerar sua solução.
Tarefa:
- Existem 3 tipos de células únicos na tela, vamos chamá-los a, b e c .
- Todas as células são recuadas 16 dp horizontalmente.
- A célula b também tem um deslocamento vertical de 8 dp.
- A célula a tem bordas arredondadas na parte superior se for a primeira célula do grupo e na parte inferior se for a última célula do grupo.
- Os divisores são desenhados entre as células com, MAS não deve haver um divisor após a última célula do grupo.
- Uma imagem com efeito de paralaxe é desenhada contra o fundo da célula c .
Deve acabar assim:

Vamos considerar as opções de solução:
Preencher a lista com células de diferentes tipos.
Você pode escrever seu próprio adaptador ou usar sua biblioteca favorita.
Vou usar o EasyAdapter .
Células recuadas.
Existem três maneiras:
- Defina paddingStart e paddingEnd para RecyclerView.
Esta solução não funcionará se nem todas as células tiverem o mesmo recuo. - Defina layout_marginStart e layout_marginEnd na célula.
Você terá que adicionar os mesmos recuos a todas as células da lista. - Escreva a implementação de ItemDecoration e substitua o método getItemOffsets.
Melhor ainda, a solução será mais versátil e reutilizável.
Arredondando os cantos para grupos de células.
A solução parece óbvia: eu quero adicionar imediatamente algum enum {Start, Middle, End} e colocá-lo na célula junto com os dados. Mas os contras surgem imediatamente:
- O modelo de dados na lista fica mais complicado.
- Para tais manipulações, você terá que calcular com antecedência qual enum atribuir a cada célula.
- Depois de remover / adicionar um elemento à lista, você terá que recalculá-lo.
- ItemDecoration. Você pode entender qual célula do grupo está e desenhar corretamente o fundo no método onDraw * ItemDecoration.
Divisores de desenho.
Desenhar divisórias dentro de uma célula é uma má prática, pois o resultado será um layout complicado, telas complexas terão problemas com a exibição dinâmica de divisórias. E assim o ItemDecoration vence novamente. O DeviderItemDecoration pronto do sdk não funcionará para nós, uma vez que desenha divisores após cada célula, e isso não pode ser resolvido fora da caixa. Você precisa escrever sua própria implementação.
Paralaxe no plano de fundo da célula.
Pode surgir uma ideia de colocar o RecyclerView OnScrollListener e usar um View customizado para renderizar a imagem. Mas aqui novamente o ItemDecoration vai nos ajudar, pois tem acesso ao Canvas Recycler e a todos os parâmetros necessários.
No total, precisamos escrever pelo menos 4 implementações ItemDecoration. É muito bom podermos reduzir todos os pontos para trabalhar apenas com ItemDecoration e não mexer no layout e na lógica de negócios do recurso. Além disso, todas as implementações ItemDecoration podem ser reutilizadas se houver casos semelhantes no aplicativo.
No entanto, nos últimos anos, listas complexas têm aparecido em nossos projetos cada vez com mais frequência e, a cada vez, tínhamos que escrever um conjunto ItemDecoration para as necessidades do projeto. Era necessária uma solução mais universal e flexível para que pudesse ser reutilizada em outros projetos.
Quais objetivos você deseja alcançar:
- Escreva o mínimo possível de herdeiros de ItemDecoration.
- Separe a lógica de renderização no Canvas e no preenchimento.
- Tenha os benefícios de trabalhar com os métodos onDraw e onDrawOver.
- Torne os decoradores mais flexíveis na personalização (por exemplo, desenhando divisores por condição, em vez de todas as células).
- Tome uma decisão sem referência aos divisores, porque ItemDecoration é capaz de mais do que desenhar linhas horizontais e verticais.
- Isso pode ser facilmente explorado olhando para o projeto de amostra.
Como resultado, temos uma biblioteca de decoradores RecyclerView.
A biblioteca tem uma interface simples do Builder, interfaces separadas para trabalhar com Canvas e indentações e a capacidade de trabalhar com os métodos onDraw e onDrawOver. A implementação de ItemDecoration é apenas uma.
Vamos voltar ao nosso problema e ver como resolvê-lo usando a biblioteca.
O Builder do nosso decorador parece simples:
Decorator.Builder()
.underlay()
...
.overlay()
...
.offset()
...
.build()
- .underlay (...) - necessário para renderizar no ViewHolder.
- .overlay (...) - necessário para desenhar sobre o ViewHolder.
- .offset (...) - usado para definir o deslocamento do ViewHolder.
Existem 3 interfaces usadas para desenhar a decoração e definir recuos.
- RecyclerViewDecor - Renderiza a decoração para o RecyclerView.
- ViewHolderDecor - Renderiza a decoração para o RecyclerView, mas dá acesso ao ViewHolder.
- OffsetDecor - usado para definir recuos.
Mas isso não é tudo. ViewHolderDecor e OffsetDecor podem ser vinculados a um ViewHolder específico usando um viewType, que permite combinar vários tipos de decorações em uma lista ou mesmo em uma célula. Se viewType não for passado, ViewHolderDecor e OffsetDecor serão aplicados a todos os ViewHolders no RecyclerView. O RecyclerViewDecor não tem essa oportunidade, pois foi projetado para funcionar com o RecyclerView em geral, e não com ViewHolders. Além disso, a mesma instância ViewHolderDecor / RecyclerViewDecor pode ser passada para overlay (...) e underlay (...).
Vamos começar a escrever o código
A biblioteca EasyAdapter usa ItemControllers para criar um ViewHolder. Resumindo, eles são responsáveis por criar e identificar o ViewHolder. Para nosso exemplo, basta um controlador, que pode exibir ViewHolders diferentes. O principal é que o viewType é único para cada layout de célula. Se parece com isso:
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)
Para definir os recuos, precisamos de um descendente de 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)
}
}
Para desenhar cantos arredondados, ViewHolder precisa de um herdeiro de ViewHolderDecor. Aqui, precisamos de um OutlineProvider para que o estado de impressão também seja cortado nas bordas.
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
}
}
}
}
Para desenhar divisórias, escreveremos mais um herdeiro 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)
}
}
}
Para configurar nosso divader, usaremos a classe 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
)
Isso ajudará a ajustar a cor, altura, preenchimento horizontal e regras de desenho
do divisor.O último herdeiro de ViewHolderDecor permanece. Para desenhar uma imagem com efeito de paralaxe.
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
)
}
}
}
Vamos colocar tudo junto agora.
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()
}
Inicializamos o RecyclerView, adicionamos nosso decorador e controladores a ele:
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)
}
Isso é tudo. A decoração da nossa lista está pronta.
Conseguimos escrever um conjunto de decoradores que podem ser facilmente reutilizados e personalizados de forma flexível.
Vamos ver de que outra forma os decoradores podem ser aplicados.
PageIndicator para RecyclerView horizontal
Mensagens de chat no balão e barra de rolagem:
Um caso mais complexo - desenhar formas, ícones, mudar o tema sem recarregar a tela:
Cabeçalho fixo
Código-fonte com exemplos
Conclusão
Apesar da simplicidade da interface ItemDecoration, ela permite que você faça coisas complexas com a lista sem alterar o layout. Espero ter conseguido mostrar que esta é uma ferramenta poderosa o suficiente e digna de sua atenção. E nossa biblioteca ajudará você a decorar suas listas mais facilmente.
Obrigado a todos pela atenção, ficarei feliz em ouvir seus comentários.
UPD: 08/06/2020 adicionado exemplo para cabeçalho Sticky







