Benoit
Benoit

Reputation: 4599

Android Compose - How to tile/repeat a bitmap/vector?

What is the Android Compose approach to tile an image to fill my background with a small pattern?

A naive approach for Bitmaps without rotation could be like this:

@Composable
fun TileImage() {
    val pattern = ImageBitmap.imageResource(R.drawable.pattern_bitmap)

    Canvas(modifier = Modifier.fillMaxSize()) {
//    rotate(degrees = -15f) { // The rotation does not produce the desired effect
        val totalWidth = size.width / pattern.width
        val totalHeight = size.height / pattern.height

        var x = 0f
        var y = 0f
        for (i in 0..totalHeight.toInt()) {
            y = (i * pattern.height).toFloat()
            for (j in 0..totalWidth.toInt()) {
                x = (j * pattern.width).toFloat()

                drawImage(
                    pattern,
                    colorFilter = giftColorFilter,
                    topLeft = Offset(x, y)
                )
            }
        }
//    }
    }
}

In Android XML you can easily create XML to repeat a bitmap

<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/pattern_bitmap" 
android:tileMode="repeat" />

Or if you need to tile a vector you can use a custom Drawable class to achieve your goal

TileDrawable(AppCompatResources.getDrawable(context, R.drawable.pattern_vector), Shader.TileMode.REPEAT)

class TileDrawable(drawable: Drawable, tileMode: Shader.TileMode, private val angle: Float? = null) : Drawable() {

    private val paint: Paint = Paint().apply {
        shader = BitmapShader(getBitmap(drawable), tileMode, tileMode)
    }

    override fun draw(canvas: Canvas) {
        angle?.let {
            canvas.rotate(it)
        }
        canvas.drawPaint(paint)
    }

    override fun setAlpha(alpha: Int) {
        paint.alpha = alpha
    }

    override fun getOpacity() = PixelFormat.TRANSLUCENT

    override fun setColorFilter(colorFilter: ColorFilter?) {
        paint.colorFilter = colorFilter
    }

    private fun getBitmap(drawable: Drawable): Bitmap {
        if (drawable is BitmapDrawable) {
            return drawable.bitmap
        }
        val bmp = Bitmap.createBitmap(
            drawable.intrinsicWidth, drawable.intrinsicHeight,
            Bitmap.Config.ARGB_8888
        )
        val c = Canvas(bmp)
        drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
        drawable.draw(c)
        return bmp
    }

}

Upvotes: 10

Views: 5074

Answers (3)

Victor Shpyrka
Victor Shpyrka

Reputation: 341

I couldn't find any existing extension to load vector resource and convert it to compose ImageBitmap, so here is the example of how to do that based on the standard extension which loads a BitmapDrawable:

fun ImageBitmap.Companion.vectorImageResource(res: Resources, @DrawableRes id: Int): ImageBitmap {
    return requireNotNull(ResourcesCompat.getDrawable(res, id, null))
        .toBitmap()
        .asImageBitmap()
}

@Composable
fun ImageBitmap.Companion.vectorImageResource(@DrawableRes id: Int): ImageBitmap {
    val context = LocalContext.current
    val value = remember { TypedValue() }
    context.resources.getValue(id, value, true)
    val key = value.string!!.toString() // image resource must have resource path.
    return remember(key) { vectorImageResource(context.resources, id) }
}

And then the resulted ImageBitmap can be passed to the example from Erik Uggeldahl above.

val brush = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) }

Upvotes: 1

Erik Uggeldahl
Erik Uggeldahl

Reputation: 1146

Based on Rafiul's answer, I was able to come up with something a bit more succinct. Here's hoping Compose comes up with something built-in to make this simpler in the future.

val image = ImageBitmap.imageResource(R.drawable.my_image)
val brush = remember(image) { ShaderBrush(ImageShader(image, TileMode.Repeated, TileMode.Repeated)) }
Box(Modifier
    .fillMaxSize()
    .background(brush)) {
}

Upvotes: 17

Rafiul
Rafiul

Reputation: 2020

If you want to use native canvas you can do something like this in jetpack compose.


    Canvas(
        modifier = Modifier
            .fillMaxSize()
    ) {
    
        val paint = Paint().asFrameworkPaint().apply {
            isAntiAlias = true
            shader = ImageShader(pattern, TileMode.Repeated, TileMode.Repeated)
        }
    
        drawIntoCanvas {
            it.nativeCanvas.drawPaint(paint)
        }
        paint.reset()
    }

And If you want to limit your repetition to a certain height and width you can use the clip modifier in canvas like below otherwise it will fill the entire screen.


    Canvas(
        modifier = Modifier
            .width(300.dp)
            .height(200.dp)
            .clip(RectangleShape)
    ) {
        ----
    }

Upvotes: 9

Related Questions