RecyclerView.ItemDecoration: memanfaatkannya semaksimal mungkin

Halo, pembaca Habr yang budiman. Nama saya Oleg Zhilo, selama 4 tahun terakhir saya menjadi pengembang Android di Surf. Selama waktu ini, saya mengambil bagian dalam semua jenis proyek keren, tetapi saya juga memiliki kesempatan untuk bekerja dengan kode lama.



Proyek-proyek ini memiliki setidaknya satu kesamaan: ada daftar item di mana-mana. Misalnya, daftar kontak buku telepon atau daftar pengaturan profil Anda.



Proyek kami menggunakan RecyclerView untuk daftar. Saya tidak akan memberi tahu Anda cara menulis Adaptor untuk RecyclerView atau cara memperbarui data dalam daftar dengan benar. Dalam artikel saya, saya akan berbicara tentang komponen penting dan sering diabaikan lainnya - RecyclerView.ItemDecoration, saya akan menunjukkan kepada Anda bagaimana menggunakannya dalam tata letak daftar dan apa yang dapat dilakukannya.







Selain data dalam daftar, RecyclerView juga berisi elemen dekoratif penting, misalnya pemisah sel, bilah gulir. Dan di sini RecyclerView.ItemDecoration akan membantu kita menggambar seluruh dekorasi dan tidak menghasilkan Tampilan yang tidak perlu dalam tata letak sel dan layar.



ItemDecoration adalah kelas abstrak dengan 3 metode:



Metode untuk merender dekorasi sebelum merender ViewHolder



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


Metode untuk merender dekorasi setelah merender ViewHolder



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


Metode untuk mengindentasi ViewHolder saat mengisi RecyclerView



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


Dengan tanda tangan metode onDraw *, Anda dapat melihat bahwa 3 komponen utama digunakan untuk menggambar dekorasi.



  • Kanvas - untuk menampilkan dekorasi yang diperlukan
  • RecyclerView - untuk mengakses parameter RecyclerVIew itu sendiri
  • RecyclerView.State - berisi informasi tentang status RecyclerView


Menghubungkan ke RecyclerView



Ada dua metode untuk menghubungkan instance ItemDecoration ke RecyclerView:



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


Semua instance RecyclerView.ItemDecoration yang terhubung ditambahkan ke satu daftar dan semuanya dirender sekaligus.



RecyclerView juga memiliki metode tambahan untuk memanipulasi ItemDecoration.

Menghapus ItemDecoration by Index



public void removeItemDecorationAt(int index)


Menghapus instance ItemDecoration



public void removeItemDecoration(@NonNull ItemDecoration decor)


Dapatkan ItemDecoration berdasarkan indeks



public ItemDecoration getItemDecorationAt(int index)


Dapatkan jumlah ItemDecoration terhubung saat ini di RecyclerView



public int getItemDecorationCount()


Gambar ulang daftar ItemDecoration saat ini



public void invalidateItemDecorations()


SDK sudah memiliki pewaris RecyclerView.ItemDecoration, misalnya, DeviderItemDecoration. Ini memungkinkan Anda menggambar pemisah untuk sel.



Ini bekerja dengan sangat sederhana, Anda perlu menggunakan drawable dan DeviderItemDecoration akan menggambarnya sebagai pemisah sel.



Mari buat 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>


Dan hubungkan DividerItemDeoration ke RecyclerView:



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


Kita mendapatkan:





Ideal untuk acara sederhana.



Semuanya dasar di bawah "kap" 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);
}


Untuk setiap panggilan onDraw (...), putar melalui semua View saat ini di RecyclerView dan gambar drawable yang diteruskan.



Namun layar dapat berisi elemen tata letak yang lebih kompleks daripada daftar elemen yang identik. Layar tersebut mungkin termasuk:



a. Beberapa jenis sel;

b. Beberapa jenis pembagi;

c. Sel bisa memiliki tepi membulat;

d. Sel dapat memiliki indentasi vertikal dan horizontal yang berbeda tergantung pada beberapa kondisi;

e. Semua hal di atas sekaligus.



Mari kita lihat poin e. Mari tentukan sendiri tugas yang sulit dan pertimbangkan solusinya.



Tugas:



  • Ada 3 jenis sel unik di layar, sebut saja a, b dan c .
  • Semua sel menjorok 16dp secara horizontal.
  • Sel b juga memiliki offset vertikal 8dp.
  • Sel a memiliki tepi membulat di bagian atas jika itu adalah sel pertama dalam grup dan di bagian bawah jika itu adalah sel terakhir dalam grup.
  • Pembagi ditarik di antara sel dengan, TETAPI tidak boleh ada pembatas setelah sel terakhir dalam grup.
  • Sebuah gambar dengan efek paralaks digambar dengan latar belakang sel c .


Ini harus berakhir seperti ini:





Mari pertimbangkan opsi untuk memecahkan:



Mengisi daftar dengan sel dari tipe yang berbeda.



Anda dapat menulis Adaptor Anda sendiri, atau Anda dapat menggunakan perpustakaan favorit Anda.

Saya akan menggunakan EasyAdapter .



Mengindentasi sel.



Ada tiga cara:



  1. Setel paddingStart dan paddingEnd untuk RecyclerView.

    Solusi ini tidak akan berfungsi jika tidak semua sel memiliki lekukan yang sama.
  2. Setel layout_marginStart dan layout_marginEnd di sel.

    Anda harus menambahkan indentasi yang sama ke semua sel dalam daftar.
  3. Tulis implementasi ItemDecoration dan ganti metode getItemOffsets.

    Sudah lebih baik, solusinya akan lebih serbaguna dan dapat digunakan kembali.


Membulatkan sudut untuk kelompok sel.



Solusinya tampak jelas: Saya ingin segera menambahkan beberapa enum {Start, Middle, End} dan memasukkannya ke dalam sel bersama dengan datanya. Tapi kontra segera muncul:



  • Model data dalam daftar menjadi lebih rumit.
  • Untuk manipulasi seperti itu, Anda harus menghitung terlebih dahulu enum mana yang akan ditetapkan ke setiap sel.
  • Setelah menghapus / menambahkan elemen ke daftar, Anda harus menghitung ulang.
  • ItemDecoration. Anda dapat memahami sel mana dalam grup tersebut dan menggambar latar belakang dengan benar menggunakan metode onDraw * ItemDecoration.


Menggambar pembatas.



Menggambar pembagi di dalam sel adalah praktik yang buruk, akibatnya akan menjadi tata letak yang rumit, layar yang rumit akan bermasalah dengan tampilan pembagi yang dinamis. Dan ItemDecoration menang lagi. DeviderItemDecoration yang sudah jadi dari sdk tidak akan berfungsi untuk kita, karena menggambar pemisah setelah setiap sel, dan ini tidak dapat diselesaikan di luar kotak. Anda perlu menulis implementasi Anda sendiri.



Parallax di latar belakang sel.



Sebuah ide mungkin muncul di benak Anda untuk meletakkan RecyclerView OnScrollListener dan menggunakan beberapa Tampilan kustom untuk merender gambar. Tapi di sini lagi ItemDecoration akan membantu kami, karena ia memiliki akses ke Canvas Recycler dan semua parameter yang diperlukan.



Secara total, kita perlu menulis setidaknya 4 implementasi ItemDecoration. Sangat bagus bahwa kami dapat mengurangi semua poin menjadi hanya bekerja dengan ItemDecoration dan tidak menyentuh tata letak dan logika bisnis fitur. Selain itu, semua implementasi ItemDecoration dapat digunakan kembali jika kami memiliki kasus serupa dalam aplikasi.



Namun, selama beberapa tahun terakhir, daftar kompleks semakin sering muncul di proyek kami dan setiap kali kami harus menulis kumpulan ItemDecoration untuk kebutuhan proyek. Diperlukan solusi yang lebih universal dan fleksibel agar dapat digunakan kembali pada proyek lain.



Tujuan apa yang ingin Anda capai:



  1. Tuliskan sesedikit mungkin ahli waris ItemDecoration.
  2. Pisahkan logika rendering pada Canvas dan padding.
  3. Memiliki keuntungan bekerja dengan metode onDraw dan onDrawOver.
  4. Jadikan dekorator lebih fleksibel dalam penyesuaian (misalnya, menggambar pembatas berdasarkan kondisi, tidak semua sel).
  5. Buat keputusan tanpa mengacu pada Pemisah, karena ItemDecoration mampu lebih dari sekadar menggambar garis horizontal dan vertikal.
  6. Ini dapat dengan mudah dieksploitasi dengan melihat proyek sampel.


Hasilnya, kami memiliki pustaka dekorator RecyclerView.



Pustaka memiliki antarmuka Builder sederhana, antarmuka terpisah untuk bekerja dengan Canvas dan indentasi, serta kemampuan untuk bekerja dengan metode onDraw dan onDrawOver. Implementasi ItemDecoration hanya satu.



Mari kembali ke masalah kita dan lihat bagaimana menyelesaikannya menggunakan perpustakaan.

Builder dekorator kami terlihat sederhana:




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


  • .underlay (...) - diperlukan untuk rendering di bawah ViewHolder.
  • .overlay (...) - diperlukan untuk menggambar di atas ViewHolder.
  • .offset (...) - digunakan untuk mengatur offset ViewHolder.


Ada 3 antarmuka yang digunakan untuk menggambar dekorasi dan mengatur indentasi.



  • RecyclerViewDecor - Merender dekorasi ke RecyclerView.
  • ViewHolderDecor - Merender dekorasi ke RecyclerView, tetapi memberikan akses ke ViewHolder.
  • OffsetDecor - digunakan untuk mengatur indentasi.


Tapi itu belum semuanya. ViewHolderDecor dan OffsetDecor bisa diikat ke ViewHolder tertentu menggunakan viewType, yang memungkinkan Anda menggabungkan beberapa tipe dekorasi pada satu daftar atau bahkan sel. Jika viewType tidak diteruskan, ViewHolderDecor dan OffsetDecor akan berlaku untuk semua ViewHolders di RecyclerView. RecyclerViewDecor tidak memiliki peluang seperti itu, karena dirancang untuk bekerja dengan RecyclerView secara umum, dan bukan dengan ViewHolders. Selain itu, instance ViewHolderDecor / RecyclerViewDecor yang sama dapat diteruskan ke overlay (...) dan underlay (...).



Mari mulai menulis kodenya



Pustaka EasyAdapter menggunakan ItemControllers untuk membuat ViewHolder. Singkatnya, mereka bertanggung jawab untuk membuat dan mengidentifikasi ViewHolder. Untuk contoh kita, satu pengontrol sudah cukup, yang dapat menampilkan ViewHolders yang berbeda. Hal utama adalah viewType itu unik untuk setiap tata letak sel. Ini terlihat seperti ini:



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)


Untuk mengatur indentasi, kita membutuhkan turunan 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)
    }
}


Untuk menggambar sudut membulat, ViewHolder membutuhkan pewaris ViewHolderDecor. Di sini kita membutuhkan OutlineProvider sehingga keadaan tekan juga terpotong di tepinya.



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


Untuk menggambar pemisah, kita akan menulis satu lagi pewaris 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)
        }
    }
}


Untuk mengkonfigurasi divader kami, kami akan menggunakan kelas 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
)


Ini akan membantu untuk menyesuaikan warna, tinggi, padding horizontal dan aturan menggambar



pembatas. Pewaris terakhir ViewHolderDecor tetap ada. Untuk menggambar gambar dengan efek paralaks.



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


Mari kita gabungkan semuanya sekarang.



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


Kami menginisialisasi RecyclerView, menambahkan dekorator dan pengontrol kami ke sana:



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


Itu saja. Dekorasi di daftar kami sudah siap.



Kami berhasil menulis satu set dekorator yang dapat digunakan kembali dengan mudah dan disesuaikan secara fleksibel.



Mari kita lihat bagaimana lagi dekorator dapat diterapkan.



PageIndicator untuk RecyclerView horizontal



Pesan bubble chat dan scroll bar:



Kasus yang lebih kompleks - menggambar bentuk, ikon, mengubah tema tanpa memuat ulang layar:





Header lengket



StickyHeaderDecor.kt


Kode sumber dengan contoh



Kesimpulan



Terlepas dari kesederhanaan antarmuka ItemDecoration, ini memungkinkan Anda untuk melakukan hal-hal kompleks dengan daftar tanpa mengubah tata letak. Saya harap saya dapat menunjukkan bahwa ini adalah alat yang cukup kuat dan layak untuk diperhatikan. Dan perpustakaan kami akan membantu Anda menghias daftar Anda dengan lebih mudah.



Terima kasih atas perhatiannya, saya akan senang mendengar komentar Anda.



UPD: 08/06/2020 menambahkan contoh untuk header Sticky



All Articles