Search code examples
androidandroid-canvasandroid-custom-drawable

Custom image with dynamic text on top with background


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.


Solution

  • 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