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.
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.
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".
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()
.
Figured it out (Oops my bounty)
In order for a View
to be created correctly, it needs 3 steps:
view.measure()
)view.layout()
)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.