RecyclerView.ItemDecoration: Das Beste daraus machen

Hallo, lieber Habr-Leser. Mein Name ist Oleg Zhilo, seit 4 Jahren bin ich Android-Entwickler bei Surf. Während dieser Zeit nahm ich an allen möglichen coolen Projekten teil, hatte aber auch die Möglichkeit, mit Legacy-Code zu arbeiten.



Diese Projekte haben mindestens eines gemeinsam: Es gibt überall eine Liste von Elementen. Zum Beispiel eine Liste der Telefonbuchkontakte oder eine Liste Ihrer Profileinstellungen.



Unsere Projekte verwenden RecyclerView für Listen. Ich werde Ihnen nicht sagen, wie Sie einen Adapter für RecyclerView schreiben oder wie Sie die Daten in der Liste korrekt aktualisieren. In meinem Artikel werde ich über eine andere wichtige und oft übersehene Komponente sprechen - RecyclerView.ItemDecoration. Ich werde Ihnen zeigen, wie Sie sie im Layout der Liste verwenden und was sie kann.







Zusätzlich zu den Daten in der Liste enthält RecyclerView auch wichtige dekorative Elemente, z. B. Zellentrennzeichen und Bildlaufleisten. Und hier hilft uns RecyclerView.ItemDecoration, das gesamte Dekor zu zeichnen und keine unnötigen Ansichten im Layout der Zellen und des Bildschirms zu erzeugen.



ItemDecoration ist eine abstrakte Klasse mit drei Methoden:



Methode zum Rendern von Dekor vor dem Rendern von ViewHolder



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


Methode zum Rendern von Dekor nach dem Rendern von ViewHolder



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


Methode zum Einrücken von ViewHolder beim Füllen von RecyclerView



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


Anhand der Signatur der onDraw * -Methoden können Sie erkennen, dass 3 Hauptkomponenten zum Zeichnen des Dekors verwendet werden.



  • Leinwand - zum Rendern des notwendigen Dekors
  • RecyclerView - für den Zugriff auf die Parameter von RecyclerVIew selbst
  • RecyclerView.State - enthält Informationen zum Status von RecyclerView


Herstellen einer Verbindung zu RecyclerView



Es gibt zwei Methoden, um eine ItemDecoration-Instanz mit RecyclerView zu verbinden:



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


Alle verbundenen RecyclerView.ItemDecoration-Instanzen werden einer Liste hinzugefügt und alle gleichzeitig gerendert.



RecyclerView bietet außerdem zusätzliche Methoden zum Bearbeiten von ItemDecoration.

ItemDecoration nach Index entfernen



public void removeItemDecorationAt(int index)


Entfernen einer ItemDecoration-Instanz



public void removeItemDecoration(@NonNull ItemDecoration decor)


Holen Sie sich ItemDecoration nach Index



public ItemDecoration getItemDecorationAt(int index)


Ruft die aktuelle Anzahl der verbundenen ItemDecoration in RecyclerView ab



public int getItemDecorationCount()


Zeichnen Sie die aktuelle ItemDecoration-Liste neu



public void invalidateItemDecorations()


Das SDK hat bereits Erben von RecyclerView.ItemDecoration, z. B. DeviderItemDecoration. Sie können Trennzeichen für Zellen zeichnen.



Es funktioniert sehr einfach, Sie müssen ein Zeichenelement verwenden und DeviderItemDecoration zeichnet es als Zelltrennzeichen.



Erstellen wir 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>


Und verbinden Sie die DividerItemDeoration mit der RecyclerView:



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


Wir bekommen:





Ideal für einfache Anlässe.



Alles ist elementar unter der "Haube" von 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);
}


Durchlaufen Sie bei jedem Aufruf von onDraw (...) die gesamte aktuelle Ansicht in der RecyclerView und zeichnen Sie das übergebene Drawable.



Der Bildschirm kann jedoch komplexere Layoutelemente enthalten als eine Liste identischer Elemente. Der Bildschirm kann Folgendes enthalten:



a. Verschiedene Arten von Zellen;

b. Verschiedene Arten von Trennwänden;

c. Zellen können abgerundete Kanten haben;

d. Zellen können abhängig von bestimmten Bedingungen unterschiedliche vertikale und horizontale Einschnitte haben.

e. Alles oben auf einmal.



Schauen wir uns Punkt e an. Stellen wir uns einer schwierigen Aufgabe und betrachten ihre Lösung.



Aufgabe:



  • Es gibt 3 Arten von eindeutigen Zellen auf dem Bildschirm. Nennen wir sie a, b und c .
  • Alle Zellen werden horizontal mit 16 dp eingerückt.
  • Zelle b hat auch einen vertikalen Versatz von 8 dp.
  • Zelle a hat oben abgerundete Kanten, wenn es sich um die erste Zelle in der Gruppe handelt, und unten, wenn es sich um die letzte Zelle in der Gruppe handelt.
  • Teiler werden zwischen Zellen mit gezeichnet, ABER es sollte keinen Teiler nach der letzten Zelle in der Gruppe geben.
  • Ein Bild mit Parallaxeeffekt wird vor dem Hintergrund von Zelle c gezeichnet.


Es sollte so enden:





Berücksichtigen Sie die Lösungsoptionen:



Füllen Sie die Liste mit Zellen unterschiedlichen Typs.



Sie können Ihren eigenen Adapter schreiben oder Ihre Lieblingsbibliothek verwenden.

Ich werde EasyAdapter verwenden .



Zellen einrücken.



Es gibt drei Möglichkeiten:



  1. Legen Sie paddingStart und paddingEnd für RecyclerView fest.

    Diese Lösung funktioniert nicht, wenn nicht alle Zellen dieselbe Einrückung haben.
  2. Legen Sie layout_marginStart und layout_marginEnd für die Zelle fest.

    Sie müssen allen Zellen in der Liste dieselben Einrückungen hinzufügen.
  3. Schreiben Sie eine ItemDecoration-Implementierung und überschreiben Sie die getItemOffsets-Methode.

    Bereits besser wird die Lösung vielseitiger und wiederverwendbarer.


Abgerundete Ecken für Zellgruppen.



Die Lösung liegt auf der Hand: Ich möchte sofort eine Aufzählung {Start, Mitte, Ende} hinzufügen und zusammen mit den Daten in die Zelle einfügen. Aber die Nachteile tauchen sofort auf:



  • Das Datenmodell in der Liste wird komplizierter.
  • Für solche Manipulationen müssen Sie im Voraus berechnen, welche Aufzählung jeder Zelle zugewiesen werden soll.
  • Nachdem Sie ein Element gelöscht / zur Liste hinzugefügt haben, müssen Sie es neu berechnen.
  • ItemDecoration. Sie können verstehen, welche Zelle in der Gruppe ist, und den Hintergrund in der onDraw * ItemDecoration-Methode korrekt zeichnen.


Teiler zeichnen.



Das Zeichnen von Teilern in einer Zelle ist eine schlechte Praxis, da das Ergebnis ein kompliziertes Layout ist und komplexe Bildschirme Probleme mit der dynamischen Anzeige von Teilern haben. Und so gewinnt ItemDecoration erneut. Die vorgefertigte DeviderItemDecoration aus dem SDK funktioniert bei uns nicht, da nach jeder Zelle Teiler gezeichnet werden und dies nicht sofort gelöst werden kann. Sie müssen Ihre eigene Implementierung schreiben.



Parallaxe auf dem Hintergrund der Zelle.



Möglicherweise fällt Ihnen die Idee ein, den RecyclerView OnScrollListener zu verwenden und eine benutzerdefinierte Ansicht zum Rendern des Bildes zu verwenden. Aber auch hier hilft uns ItemDecoration, da es Zugriff auf den Canvas Recycler und alle erforderlichen Parameter hat.



Insgesamt müssen mindestens 4 ItemDecoration-Implementierungen geschrieben werden. Es ist sehr gut, dass wir alle Punkte darauf reduzieren können, nur mit ItemDecoration zu arbeiten und nicht das Layout und die Geschäftslogik der Funktion zu berühren. Außerdem können alle ItemDecoration-Implementierungen wiederverwendet werden, wenn die Anwendung ähnliche Fälle enthält.



In den letzten Jahren sind in unseren Projekten jedoch immer häufiger komplexe Listen aufgetaucht, und jedes Mal mussten wir ein ItemDecoration-Set für die Anforderungen des Projekts schreiben. Eine universellere und flexiblere Lösung war erforderlich, damit sie für andere Projekte wiederverwendet werden konnte.



Welche Ziele wollten Sie erreichen:



  1. Schreiben Sie so wenige ItemDecoration-Erben wie möglich.
  2. Trennen Sie die Renderlogik auf der Leinwand und die Auffüllung.
  3. Nutzen Sie die Vorteile der Arbeit mit den Methoden onDraw und onDrawOver.
  4. Machen Sie Dekorateure flexibler bei der Anpassung (z. B. Zeichnen von Trennwänden nach Bedingungen, nicht aller Zellen).
  5. Treffen Sie eine Entscheidung ohne Bezug auf Teiler, da ItemDecoration mehr kann als horizontale und vertikale Linien zu zeichnen.
  6. Dies kann leicht ausgenutzt werden, indem man sich das Beispielprojekt ansieht.


Als Ergebnis haben wir eine RecyclerView Decorator- Bibliothek .



Die Bibliothek verfügt über eine einfache Builder-Oberfläche, separate Schnittstellen für die Arbeit mit Canvas und Einrückungen sowie die Möglichkeit, mit den Methoden onDraw und onDrawOver zu arbeiten. Die ItemDecoration-Implementierung ist nur eine.



Kehren wir zu unserem Problem zurück und sehen, wie es mithilfe der Bibliothek gelöst werden kann.

Der Builder unseres Dekorateurs sieht einfach aus:




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


  • .underlay (...) - wird zum Rendern unter dem ViewHolder benötigt.
  • .overlay (...) - wird benötigt, um über den ViewHolder zu zeichnen.
  • .offset (...) - wird verwendet, um den Offset des ViewHolder festzulegen.


Es gibt 3 Schnittstellen zum Zeichnen von Dekor und Einstellen von Einrückungen.



  • RecyclerViewDecor - Rendert das Dekor in RecyclerView.
  • ViewHolderDecor - Rendert das Dekor in RecyclerView, gewährt jedoch Zugriff auf ViewHolder.
  • OffsetDecor - wird verwendet, um Einrückungen festzulegen.


Aber das ist nicht alles. ViewHolderDecor und OffsetDecor können mithilfe von viewType an einen bestimmten ViewHolder gebunden werden, sodass Sie mehrere Arten von Dekoren in einer Liste oder sogar Zelle kombinieren können. Wenn der viewType nicht übergeben wird, gelten ViewHolderDecor und OffsetDecor für alle ViewHolders in RecyclerView. RecyclerViewDecor hat keine solche Möglichkeit, da es für die Verwendung mit RecyclerView im Allgemeinen und nicht mit ViewHolders ausgelegt ist. Außerdem kann dieselbe ViewHolderDecor / RecyclerViewDecor-Instanz sowohl an Overlay (...) als auch an Underlay (...) übergeben werden.



Beginnen wir mit dem Schreiben des Codes



Die EasyAdapter-Bibliothek verwendet ItemController, um einen ViewHolder zu erstellen. Kurz gesagt, sie sind für die Erstellung und Identifizierung des ViewHolder verantwortlich. In unserem Beispiel reicht ein Controller aus, der verschiedene ViewHolders anzeigen kann. Die Hauptsache ist, dass der viewType für jedes Zellenlayout eindeutig ist. Es sieht aus wie das:



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)


Um die Einrückungen zu setzen, benötigen wir einen Nachkommen von 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)
    }
}


Um abgerundete Ecken zu zeichnen, benötigt ViewHolder einen Erben von ViewHolderDecor. Hier benötigen wir einen OutlineProvider, damit der Druckstatus auch an den Rändern abgeschnitten wird.



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
            }
        }
    }
}


Um Trennlinien zu zeichnen, schreiben wir einen weiteren ViewHolderDecor-Erben:



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)
        }
    }
}


Um unseren Divader zu konfigurieren, verwenden wir die Gap.kt-Klasse:



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
)


Es hilft dabei, die Regeln für Farbe, Höhe, horizontale Auffüllung und Zeichnung des



Teilers anzupassen . Der letzte Erbe von ViewHolderDecor bleibt erhalten. Zum Zeichnen eines Bildes mit Parallaxeeffekt.



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
            )
        }
    }
}


Lassen Sie uns jetzt alles zusammenfügen.



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()
    }


Wir initialisieren die RecyclerView, fügen unseren Dekorator und unsere Controller hinzu:



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)
    }


Das ist alles. Das Dekor auf unserer Liste ist fertig.



Wir haben es geschafft, eine Reihe von Dekorateuren zu schreiben, die einfach wiederverwendet und flexibel angepasst werden können.



Mal sehen, wie sonst Dekorateure angewendet werden können.



PageIndicator für horizontale RecyclerView

CarouselDecoratorActivityView.kt


Bubble-Chat-Nachrichten und Bildlaufleiste:

ChatActivityView.kt


Ein komplexerer Fall - Formen, Symbole zeichnen, das Thema ändern, ohne den Bildschirm neu zu laden:

TimeLineActivity.kt




Sticky Header



StickyHeaderDecor.kt


Quellcode mit Beispielen



Fazit



Trotz der Einfachheit der ItemDecoration-Oberfläche können Sie komplexe Aufgaben mit der Liste ausführen, ohne das Layout zu ändern. Ich hoffe, ich konnte zeigen, dass dies ein ausreichend leistungsfähiges Werkzeug ist und Ihre Aufmerksamkeit verdient. Unsere Bibliothek hilft Ihnen dabei, Ihre Listen einfacher zu dekorieren.



Vielen Dank für Ihre Aufmerksamkeit, ich freue mich über Ihre Kommentare.



UPD: 08/06/2020 Beispiel für Sticky-Header hinzugefügt



All Articles