Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw a view in an ItemDecoration?

I know already how to draw things in an ItemDecoration, but now I want to draw a View in an ItemDecoration.

Since the setting is a bit complicated, I have created a sample project that can reproduce the problem.

What I want to achieve

I have a RecyclerView with 20 items, displaying just numbers. I want to add a black header with the text "This is Number 5" above item 5.

Of course, this is a simplified version of my real problem, and in my real problem I must do this by ItemDecoration, so please do not give alternatives that do not use ItemDecoration.

The problem

As shown in the below screenshot, the decoration has correct size, and the first layer of the xml (which has android:background="@color/black") can be drawn; but not the child views that include the TextView which is supposed to display "This is Number 5".

enter image description here

How am I doing this now

FiveHeaderDecoration.kt:

class FiveHeaderDecoration: RecyclerView.ItemDecoration() {

    private var header: Bitmap? = null
    private val paint = Paint()

    override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
        val params = view?.layoutParams as? RecyclerView.LayoutParams
        if (params == null || parent == null) {
            super.getItemOffsets(outRect, view, parent, state)
        } else {
            val position = params.viewAdapterPosition
            val number = (parent.adapter as? JustAnAdapter)?.itemList?.getOrNull(position)
            if (number == 5) {
                outRect?.set(0, 48.dp(), 0, 0)
            } else {
                super.getItemOffsets(outRect, view, parent, state)
            }
        }
    }

    override fun onDraw(c: Canvas?, parent: RecyclerView?, state: RecyclerView.State?) {
        initHeader(parent)
        if (parent == null) return
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val view = parent.getChildAt(i)
            val position = parent.getChildAdapterPosition(view)
            val number = (parent.adapter as? JustAnAdapter)?.itemList?.getOrNull(position)
            if (number == 5) {
                header?.let {
                    c?.drawBitmap(it, 0.toFloat(), view.top.toFloat() - 48.dp(), paint)
                }
            } else {
                super.onDraw(c, parent, state)
            }
        }
    }

    private fun initHeader(parent: RecyclerView?) {
        if (header == null) {
            val view = parent?.context?.inflate(R.layout.decoration, parent, false)
            val bitmap = Bitmap.createBitmap(parent?.width?:0, 40.dp(), Bitmap.Config.ARGB_8888)
            val canvas = Canvas(bitmap)
            view?.layout(0, 0, parent.width, 40.dp())
            view?.draw(canvas)
            header = bitmap
        }
    }

}

You can find other classes in the sample project. But I guess they are not really related.

As you can see, I am trying to layout and draw the view to a bitmap first. This is because I can only draw something to the canvas in onDraw() but not inflate a view (I don't even have a ViewGroup to addView()).

And by using debugger, I can see already that the bitmap generated in initHeader() is just a block of black. So the problem probably lies in how I initHeader().

like image 458
Sira Lam Avatar asked Dec 23 '22 06:12

Sira Lam


1 Answers

Figured it out (Oops my bounty)

In order for a View to be created correctly, it needs 3 steps:

  1. Measure (view.measure())
  2. Layout (view.layout())
  3. Draw (view.draw())

Usually these are done by the parent ViewGroup, or in addView(). But now because we are not doing any of these, we need to call all of these by ourselves.

The problem is apparently I missed the first step.

So the initHeader method should be:

private fun initHeader(parent: RecyclerView?) {
    if (header == null) {
        val view = parent?.context?.inflate(R.layout.decoration, parent, false)
        val bitmap = Bitmap.createBitmap(parent?.width?:0, 40.dp(), Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent?.width ?: 0, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(40.dp(), View.MeasureSpec.EXACTLY)
        view?.measure(widthSpec, heightSpec)
        view?.layout(0, 0, parent.width, 40.dp())
        view?.draw(canvas)
        header = bitmap
    }
}

Note that the widthSpec and heightSpec will be different depending on your use case. That's another topic so I am not explaining here.

like image 128
Sira Lam Avatar answered Jan 13 '23 15:01

Sira Lam