How can we tile a vector image?
Asked Answered
S

5

21

With the support library now fully supporting vector images, I'm trying to switch to vector images as much as I can in my app. An issue I'm running into is that it seems impossible to repeat them.

With bitmap images the following xml could be used:

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

This does not work, as vector images can not be used in bitmaps: https://code.google.com/p/android/issues/detail?id=187566

Is there any other way to tile/repeat vector images?

Steinway answered 17/3, 2016 at 10:19 Comment(2)
not directly, but you can draw() your VectorDrawable multiple timesLapp
Since vector images are scalable you would have to additionally specify the width/height before repeating. Also repeating manually (just display them several times) is surely an option.Bertold
D
6

Check Nick Butcher solution:
https://gist.github.com/nickbutcher/4179642450db266f0a33837f2622ace3
Add TileDrawable class to your project and then set tiled drawable to your image view:

// after view created
val d = ContextCompat.getDrawable(this, R.drawable.pattern)
imageView.setImageDrawable(TileDrawable(d, Shader.TileMode.REPEAT))
Dimer answered 31/5, 2018 at 9:43 Comment(2)
Answers shouldn't be mainly a link - which can be taken offline or change.Saxen
While Nick Butcher's solution does work, it has a potential memory utilization problem, depending on how you're using the TileDrawable since it requires rendering the child Drawable to a Bitmap every time a new TileDrawable is created.Nine
B
8

This is the java version of Nick Butcher solution:

import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

public class TileDrawable extends Drawable {

    private final Paint paint;

    public TileDrawable(Drawable drawable, Shader.TileMode tileMode) {
        paint = new Paint();
        paint.setShader(new BitmapShader(getBitmap(drawable), tileMode, tileMode));
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        canvas.drawPaint(paint);
    }

    @Override
    public void setAlpha(int alpha) {
        paint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        paint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    private Bitmap getBitmap(Drawable drawable) {
        if (drawable instanceof BitmapDrawable)
            return ((BitmapDrawable) drawable).getBitmap();
        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        drawable.draw(canvas);
        return bitmap;
    }

}

You can use this drawable class in code with vector patterns:

view.setBackground(new TileDrawable(getContext().getDrawable(R.drawable.pattern), Shader.TileMode.REPEAT));
Bedford answered 15/12, 2018 at 17:44 Comment(0)
S
6

Thanks to @pskink I made a drawable that tiles another drawable: https://gist.github.com/9ffbdf01478e36194f8f

This has to be set in code, it can not be used from XML:

public class TilingDrawable extends android.support.v7.graphics.drawable.DrawableWrapper {

    private boolean callbackEnabled = true;

    public TilingDrawable(Drawable drawable) {
        super(drawable);
    }

    @Override
    public void draw(Canvas canvas) {
        callbackEnabled = false;
        Rect bounds = getBounds();
        Drawable wrappedDrawable = getWrappedDrawable();

        int width = wrappedDrawable.getIntrinsicWidth();
        int height = wrappedDrawable.getIntrinsicHeight();
        for (int x = bounds.left; x < bounds.right + width - 1; x+= width) {
            for (int y = bounds.top; y < bounds.bottom + height - 1; y += height) {
                wrappedDrawable.setBounds(x, y, x + width, y + height);
                wrappedDrawable.draw(canvas);
            }
        }
        callbackEnabled = true;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
    }

    /**
     * {@inheritDoc}
     */
    public void invalidateDrawable(Drawable who) {
        if (callbackEnabled) {
            super.invalidateDrawable(who);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void scheduleDrawable(Drawable who, Runnable what, long when) {
        if (callbackEnabled) {
            super.scheduleDrawable(who, what, when);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void unscheduleDrawable(Drawable who, Runnable what) {
        if (callbackEnabled) {
            super.unscheduleDrawable(who, what);
        }
    }
}
Steinway answered 17/3, 2016 at 12:9 Comment(1)
This code has a lot of warnings that you shouldn't call those functions on your own, because your code is not a part of the support library. Is there an alternative way to write it, which doesn't use hidden API ?Trochanter
D
6

Check Nick Butcher solution:
https://gist.github.com/nickbutcher/4179642450db266f0a33837f2622ace3
Add TileDrawable class to your project and then set tiled drawable to your image view:

// after view created
val d = ContextCompat.getDrawable(this, R.drawable.pattern)
imageView.setImageDrawable(TileDrawable(d, Shader.TileMode.REPEAT))
Dimer answered 31/5, 2018 at 9:43 Comment(2)
Answers shouldn't be mainly a link - which can be taken offline or change.Saxen
While Nick Butcher's solution does work, it has a potential memory utilization problem, depending on how you're using the TileDrawable since it requires rendering the child Drawable to a Bitmap every time a new TileDrawable is created.Nine
T
4

I'd like to put a full solution that doesn't require the support library hidden class, written in Kotlin, based on what was suggested in one of the answers (here) :

DrawableWrapper.kt

open class DrawableWrapper(drawable: Drawable) : Drawable(), Drawable.Callback {
    var wrappedDrawable: Drawable = drawable
        set(drawable) {
            field.callback = null
            field = drawable
            drawable.callback = this
        }

    override fun draw(canvas: Canvas) = wrappedDrawable.draw(canvas)

    override fun onBoundsChange(bounds: Rect) {
        wrappedDrawable.bounds = bounds
    }

    override fun setChangingConfigurations(configs: Int) {
        wrappedDrawable.changingConfigurations = configs
    }

    override fun getChangingConfigurations() = wrappedDrawable.changingConfigurations

    override fun setDither(dither: Boolean) = wrappedDrawable.setDither(dither)

    override fun setFilterBitmap(filter: Boolean) {
        wrappedDrawable.isFilterBitmap = filter
    }

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

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

    override fun isStateful() = wrappedDrawable.isStateful

    override fun setState(stateSet: IntArray) = wrappedDrawable.setState(stateSet)

    override fun getState() = wrappedDrawable.state


    override fun jumpToCurrentState() = DrawableCompat.jumpToCurrentState(wrappedDrawable)

    override fun getCurrent() = wrappedDrawable.current

    override fun setVisible(visible: Boolean, restart: Boolean) = super.setVisible(visible, restart) || wrappedDrawable.setVisible(visible, restart)

    override fun getOpacity() = wrappedDrawable.opacity

    override fun getTransparentRegion() = wrappedDrawable.transparentRegion

    override fun getIntrinsicWidth() = wrappedDrawable.intrinsicWidth

    override fun getIntrinsicHeight() = wrappedDrawable.intrinsicHeight

    override fun getMinimumWidth() = wrappedDrawable.minimumWidth

    override fun getMinimumHeight() = wrappedDrawable.minimumHeight

    override fun getPadding(padding: Rect) = wrappedDrawable.getPadding(padding)

    override fun invalidateDrawable(who: Drawable) = invalidateSelf()

    override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) = scheduleSelf(what, `when`)

    override fun unscheduleDrawable(who: Drawable, what: Runnable) = unscheduleSelf(what)

    override fun onLevelChange(level: Int) = wrappedDrawable.setLevel(level)

    override fun setAutoMirrored(mirrored: Boolean) = DrawableCompat.setAutoMirrored(wrappedDrawable, mirrored)

    override fun isAutoMirrored() = DrawableCompat.isAutoMirrored(wrappedDrawable)

    override fun setTint(tint: Int) = DrawableCompat.setTint(wrappedDrawable, tint)

    override fun setTintList(tint: ColorStateList?) = DrawableCompat.setTintList(wrappedDrawable, tint)

    override fun setTintMode(tintMode: PorterDuff.Mode) = DrawableCompat.setTintMode(wrappedDrawable, tintMode)

    override fun setHotspot(x: Float, y: Float) = DrawableCompat.setHotspot(wrappedDrawable, x, y)

    override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) = DrawableCompat.setHotspotBounds(wrappedDrawable, left, top, right, bottom)
}

TilingDrawable.kt

class TilingDrawable(drawable: Drawable) : DrawableWrapper(drawable) {
    private var callbackEnabled = true

    override fun draw(canvas: Canvas) {
        callbackEnabled = false
        val bounds = bounds
        val width = wrappedDrawable.intrinsicWidth
        val height = wrappedDrawable.intrinsicHeight
        var x = bounds.left
        while (x < bounds.right + width - 1) {
            var y = bounds.top
            while (y < bounds.bottom + height - 1) {
                wrappedDrawable.setBounds(x, y, x + width, y + height)
                wrappedDrawable.draw(canvas)
                y += height
            }
            x += width
        }
        callbackEnabled = true
    }

    override fun onBoundsChange(bounds: Rect) {}

    override fun invalidateDrawable(who: Drawable) {
        if (callbackEnabled)
            super.invalidateDrawable(who)
    }

    override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
        if (callbackEnabled)
            super.scheduleDrawable(who, what, `when`)
    }

    override fun unscheduleDrawable(who: Drawable, what: Runnable) {
        if (callbackEnabled)
            super.unscheduleDrawable(who, what)
    }
}

Sample usage:

yourView.background = TilingDrawable(AppCompatResources.getDrawable(this, R.drawable.your_drawable)!!)
Trochanter answered 27/3, 2018 at 11:25 Comment(4)
This solution has one very big improvement over the Nick Butcher solution (https://mcmap.net/q/586758/-how-can-we-tile-a-vector-image) -- this solution uses significantly less memory in certain use cases, since it does not require rendering the child Drawable to a Bitmap before drawing to the screen.Nine
I modified the TilingDrawable to also override getIntrinsicHeight() and getIntrinsicWidth() to both return -1 which gave me the results I was hoping for when setting this on an ImageView (e.g. as a placeholder). This drawable doesn't really have an intrinsic size, since it will scale to any size, and the docs indicate -1 for these cases.Nine
I don't understand. In some cases you mean that this code doesn't work? Can you please share ? Maybe I should change the answer?Trochanter
I was struggling, because the code as written in this answer was not providing the desired results when setting the TilingDrawable on an ImageView (i.e. with setImageDrawable()) unless the ImageView's scaleType was set to fitXY. By adding in the -1 for intrinsic width/height, I got the desired results.Nine
O
-1

I see 2 easy workarounds to this problem:

1. To create (repeat) pattern you want in SVG manipulative software like 'Inkscape' or 'CorelDraw'. And then use this 'created_manually_pattern_svg' in your 'ImageView' as

... 
app:srcCompat="@drawable/created_manually_pattern_svg"
...

or even

...
android:background="@drawable/created_manually_pattern_svg"
...

in any ather 'view' (but i'm not sure if it works in all API levels)

2. Export your '.svg' file into '.png' and then use 'bitmap'.

Outskirts answered 18/4, 2020 at 12:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.