Custom image with dynamic text on top with background

641 Views Asked by At

I'm trying to create a custom ImageView or Drawable in Kotlin which enables dynamic file extensions can be drawn on a base image at runtime. The end result will look like this. Tried creating custom AppCompatImageView class and overriding onDraw() with no luck. Being a novice in this area, can you suggest me a good starting point to achieve this?

enter image description here

EDIT

The file extension is a text that needs to be drawn on the base image with a background as shown in the attachment.

3

There are 3 best solutions below

6
beigirad On BEST ANSWER

I prefer to use a custom view than a custom drawable. because of its flexibility in measuring and customizing height and width.

So I've created the FileView:

import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.TextPaint
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView

class FileView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

    init {
        setImageResource(R.drawable.ic_file)
    }

    var icon: Drawable? = null
        set(value) {
            field = value
            postInvalidate()
        }

    var ext: CharSequence? = null
        set(value) {
            field = value
            postInvalidate()
        }

    private val iconRect = Rect()
    private val extRect = Rect()
    private val extPaint by lazy {
        TextPaint().apply {
            style = Paint.Style.FILL
            color = Color.WHITE
            isAntiAlias = true
            textAlign = Paint.Align.CENTER
            textSize = 12f * Resources.getSystem().displayMetrics.density + 0.5f
        }
    }
    private val extBackgroundPaint by lazy {
        TextPaint().apply {
            style = Paint.Style.FILL
            color = Color.BLACK
            isAntiAlias = true
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        val centerX = width / 2
        val centerY = height / 2

        icon?.let { icon ->
            iconRect.set(
                centerX - icon.intrinsicWidth / 2,
                centerY - icon.intrinsicHeight / 2,
                centerX + icon.intrinsicWidth / 2,
                centerY + icon.intrinsicHeight / 2
            )

            icon.bounds = iconRect
            icon.draw(canvas)
        }


        ext?.let { ext ->
            val truncatedExt =
                if (ext.length > 6) ext.subSequence(0, 6).toString().plus('…')
                else ext

            // extRect is used for measured ext height
            extPaint.getTextBounds("X", 0, 1, extRect)
            val extHeight = extRect.height() // keep ext height
            val extWidth = extPaint.measureText(truncatedExt, 0, truncatedExt.length).toInt() // keep ext width

            val extPadding = 4.toPx
            val extMargin = 4.toPx

            val extRight = width - extMargin
            val extBottom = height - extMargin
            // extRect is reused for ext background bound
            extRect.set(
                extRight - extWidth - extPadding * 2,
                extBottom - extHeight - extPadding * 2,
                extRight,
                extBottom
            )
            canvas.drawRect(extRect, extBackgroundPaint)

            canvas.drawText(
                truncatedExt,
                0,
                truncatedExt.length,
                extRect.exactCenterX(),
                extRect.bottom - ((extRect.height() - extHeight) / 2f),
                extPaint
            )
        }
    }

    private val Int.toPx get() = (this * Resources.getSystem().displayMetrics.density).toInt()
}

and use it:

with(binding.fileView) {
    icon = ContextCompat.getDrawable(context, R.drawable.ic_music)
    ext = ".aiff"
}

Output: enter image description here

1
Mathieu Chabas On

You could create a LayerDrawable at runtime resulting in the superposition of two drawables (one for the background and one for the extension) and position the extension drawable at the bottom right.

It would look like this

val layerDrawable = LayerDrawable(
                        arrayOf(
                            AppCompatResources.getDrawable(context, R.drawable.ic_base_sound_file),
                            AppCompatResources.getDrawable(context, R.drawable.ic_aiff_extension)
                        )
                    ).apply {
                        setLayerInset(1, 20, 40, 0, 10)
                    }
imageView.setImageDrawable(layerDrawable)

The method setLayerInset(index, left, top, right, bottom) will add insets to the drawable at position 'index' (here 1 -> the extension drawable).

You can also use a remote image if needed for the base image.

0
prateek On

I can think of 2 solutions for this. I am simply sharing ideas/approaches here and related code can easily be found.

  1. Simpler approach would be to have this layout designed in your xml. Then create your custom class extending ViewGroup and in its constructor you can inflate the view xml and initialise things. Then you can define any helper method ,say setData() where you can pass the file extension info and/or thumb image. Then you can update your view right there.

  2. Another approach would be to not create any xml but programmatically create them in your custom ViewGroup constructor. Then you can have a similar helper method as above to set values to various view components. After you have set everything, call requestLayout() at the end. You can then, if required, update views in onLayout() and perform any spacing/margin calculations. Then using these values draw them inside onDraw().