これらのプロジェクトには、少なくとも1つの共通点があります。それは、至る所にアイテムのリストがあるということです。たとえば、電話帳の連絡先のリストやプロファイル設定のリストです。
私たちのプロジェクトでは、リストにRecyclerViewを使用しています。 RecyclerViewのアダプタを作成する方法や、リスト内のデータを適切に更新する方法については説明しません。私の記事では、もう1つの重要で見過ごされがちなコンポーネントであるRecyclerView.ItemDecorationについて説明します。これをリストのレイアウトに使用する方法と、その機能について説明します。
リスト内のデータに加えて、RecyclerViewには、セルセパレーター、スクロールバーなどの重要な装飾要素も含まれています。そして、ここでRecyclerView.ItemDecorationは、装飾全体を描画し、セルと画面のレイアウトに不要なビューを生成しないようにするのに役立ちます。
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に接続するには、次の2つの方法があります。
public void addItemDecoration(@NonNull ItemDecoration decor)
public void addItemDecoration(@NonNull ItemDecoration decor, int index)
接続されているすべてのRecyclerView.ItemDecorationインスタンスが1つのリストに追加され、すべてが一度にレンダリングされます。
また、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には、DeviderItemDecorationなどのRecyclerView.ItemDecorationの継承者がすでに存在します。セルのセパレーターを描画できます。
それは非常に簡単に機能します。ドローアブルを使用する必要があり、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内の現在のすべてのビューをループして、渡されたドローアブルを描画します。
ただし、画面には、同一の要素のリストよりも複雑なレイアウト要素を含めることができます。画面には次のものが含まれる場合があります
。いくつかのタイプのセル。
b。いくつかのタイプの仕切り。
c。セルのエッジは丸くすることができます。
d。セルは、いくつかの条件に応じて、異なる垂直方向と水平方向のインデントを持つことができます。
e。上記のすべてを一度に。
ポイントeを見てみましょう。難しい課題を設定し、その解決策を考えてみましょう。
仕事:
- 画面には3種類の固有のセルがあり、それらをa、b、cと呼びます。
- すべてのセルは水平方向に16dpインデントされています。
- セルbの垂直オフセットも8dpです。
- セルaは、グループの最初のセルの場合は上部に、グループの最後のセルの場合は下部に丸みを帯びたエッジがあります。
- のセル間に仕切りが描画されますが、グループの最後のセルの後に仕切りがあってはなりません。
- 視差効果のある画像がセルcの背景に描画されます。
最終的には次のようになります。

解決するためのオプションを考えてみましょう
。リストをさまざまなタイプのセルで埋めます。
独自のアダプタを作成することも、お気に入りのライブラリを使用することもできます。EasyAdapter
を使用します。
セルのインデント。
3つの方法があります。
- RecyclerViewのpaddingStartとpaddingEndを設定します。
すべてのセルに同じインデントがない場合、このソリューションは機能しません。 - セルにlayout_marginStartとlayout_marginEndを設定します。
リスト内のすべてのセルに同じインデントを追加する必要があります。 - ItemDecorationの実装を記述し、getItemOffsetsメソッドをオーバーライドします。
すでに優れているため、ソリューションはより用途が広く、再利用可能になります。
セルのグループの角を丸めます。
解決策は明らかなようです。すぐにいくつかの列挙型{Start、Middle、End}を追加し、データと一緒にセルに入れたいと思います。しかし、短所はすぐに現れます:
- リスト内のデータモデルはより複雑になります。
- このような操作では、各セルに割り当てる列挙を事前に計算する必要があります。
- リストに要素を削除/追加した後、それを再計算する必要があります。
- ItemDecoration。グループ内のどのセルであるかを理解し、onDraw * ItemDecorationメソッドで背景を正しく描画できます。
仕切りを描く。
セル内に仕切りを描画することは悪い習慣です。結果として複雑なレイアウトになり、複雑な画面では仕切りの動的表示に問題が発生します。そして、ItemDecorationが再び勝ちます。 sdkの既製のDeviderItemDecorationは、各セルの後に仕切りを描画するため、機能しません。これは、そのままでは解決できません。独自の実装を作成する必要があります。
セルの背景のパララックス。
RecyclerView OnScrollListenerを配置し、カスタムビューを使用して画像をレンダリングするというアイデアが思い浮かぶかもしれません。ただし、ここでもItemDecorationが役立ちます。これは、CanvasRecyclerと必要なすべてのパラメーターにアクセスできるためです。
合計で、少なくとも4つのItemDecoration実装を作成する必要があります。機能のレイアウトやビジネスロジックに触れずに、ItemDecorationのみを使用するようにすべてのポイントを減らすことができるのは非常に良いことです。さらに、アプリケーションに同様のケースがある場合は、すべてのItemDecoration実装を再利用できます。
ただし、過去数年間で、複雑なリストがプロジェクトに頻繁に表示され、プロジェクトのニーズに合わせてItemDecorationセットを作成する必要がありました。他のプロジェクトで再利用できるように、より普遍的で柔軟なソリューションが必要でした。
達成したい目標は何ですか。
- ItemDecorationの相続人をできるだけ少なく書いてください。
- Canvasとパディングのレンダリングロジックを分離します。
- onDrawメソッドとonDrawOverメソッドを使用する利点があります。
- デコレータのカスタマイズをより柔軟にします(たとえば、すべてのセルではなく、条件ごとに仕切りを描画します)。
- ItemDecorationは水平線と垂直線を描画する以上の機能があるため、分周器を参照せずに決定を下します。
- これは、サンプルプロジェクトを見ると簡単に利用できます。
その結果、RecyclerViewデコレータライブラリができました。
ライブラリには、シンプルなBuilderインターフェイス、Canvasとインデントを操作するための個別のインターフェイス、およびonDrawメソッドとonDrawOverメソッドを操作する機能があります。ItemDecorationの実装は1つだけです。
問題に戻って、ライブラリを使用して問題を解決する方法を見てみましょう。
私たちのデコレータのビルダーはシンプルに見えます:
Decorator.Builder()
.underlay()
...
.overlay()
...
.offset()
...
.build()
- .underlay(...)-ViewHolderでのレンダリングに必要です。
- .overlay(...)-ViewHolderを描画するために必要です。
- .offset(...)-ViewHolderのオフセットを設定するために使用されます。
装飾の描画とインデントの設定に使用される3つのインターフェイスがあります。
- RecyclerViewDecor-装飾をRecyclerViewにレンダリングします。
- ViewHolderDecor-装飾をRecyclerViewにレンダリングしますが、ViewHolderへのアクセスを提供します。
- OffsetDecor-インデントを設定するために使用されます。
しかし、それだけではありません。 ViewHolderDecorとOffsetDecorは、viewTypeを使用して特定のViewHolderにバインドできます。これにより、1つのリストまたはセルに複数のタイプの装飾を組み合わせることができます。 viewTypeが渡されない場合、ViewHolderDecorとOffsetDecorはRecyclerViewのすべてのViewHolderに適用されます。 RecyclerViewDecorは、ViewHoldersではなくRecyclerViewで一般的に機能するように設計されているため、このような機会はありません。さらに、同じViewHolderDecor / RecyclerViewDecorインスタンスをオーバーレイ(...)とアンダーレイ(...)の両方に渡すことができます。
コードを書き始めましょう
EasyAdapterライブラリは、ItemControllersを使用してViewHolderを作成します。つまり、ViewHolderの作成と識別を担当します。この例では、1つのコントローラーで十分であり、さまざまなViewHolderを表示できます。主なことは、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の継承が必要です。ここでは、Press-Stateもエッジでクリップされるように、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
}
}
}
}
仕切りを描画するために、もう1つの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
バブルチャットメッセージとスクロールバー:
より複雑なケース-形状、アイコンの描画、画面をリロードせずにテーマを変更する:
スティッキーヘッダー
例を含むソースコード
結論
ItemDecorationインターフェースは単純ですが、レイアウトを変更せずにリストを使用して複雑なことを行うことができます。これが十分に強力で、注目に値するツールであることを示すことができたと思います。そして私たちのライブラリはあなたがあなたのリストをより簡単に飾るのを助けます。
ご清聴ありがとうございました。コメントをお待ちしております。
UPD:2020年8月6日スティッキーヘッダーの例を追加







