How to get a Drawable that's a mirrored version of a different Drawable?
Asked Answered
A

3

8

Background

I know it's possible to create a rotated version of a Drawable (or Bitmap), as such (written about it here) :

@JvmStatic
fun getRotateDrawable(d: Drawable, angle: Int): Drawable {
    if (angle % 360 == 0)
       return d
    return object : LayerDrawable(arrayOf(d)) {
        override fun draw(canvas: Canvas) {
            canvas.save()
            canvas.rotate(angle.toFloat(), (d.bounds.width() / 2).toFloat(), (d.bounds.height() / 2).toFloat())
            super.draw(canvas)
            canvas.restore()
        }
    }
}

The problem

I wanted to have autoMirrored being set to some drawable (VectorDrawable in my case), which would flip (mirror so that left is right, and right is left, but not affect top and bottom) it in case the locale of the device is RTL.

As an example (and it's only an example!), if you take a drawable that shows a left-arrow, after flipping it will be a right-arrow.

Sadly, this is available only from API 19.

That's why I decided to make a new Drawable out of it, to be a flipped version of the original one

What I've tried

I've found a nice article of doing the same thing to a View, here, using a Matrix. So I've tried this:

    @JvmStatic
    fun getMirroredDrawable(d: Drawable): Drawable {
        return object : LayerDrawable(arrayOf(d)) {
            override fun draw(canvas: Canvas) {
                canvas.save()
                val matrix = Matrix()
                // use this for the other flipping: matrix.preScale(1.0f, -1.0f)
                matrix.preScale(-1.0f, 1.0f);
                canvas.matrix = matrix
                super.draw(canvas)
                canvas.restore()
            }
        }
    }

Sadly, for some reason, this made the drawable not being shown at all. Maybe it does work, yet tries to get shown out of its bounds of whatever View that's showing it.

The question

How can I make a flipped version of a given Drawable, similar to what I did for rotating a Drawable?


Solution:

Based on the answer suggested below (here), here's a nice way to do it:

fun Drawable.getMirroredDrawable(): Drawable {
    return object : LayerDrawable(arrayOf(this)) {
        val drawingRect = Rect()
        val matrix = Matrix()
        override fun draw(canvas: Canvas) {
            matrix.reset()
            matrix.preScale(-1.0f, 1.0f, canvas.width / 2.0f, canvas.height / 2.0f)
            canvas.matrix = matrix
            drawingRect.left = (canvas.width - intrinsicWidth) / 2
            drawingRect.top = (canvas.height - intrinsicHeight) / 2
            drawingRect.right = drawingRect.left + intrinsicWidth
            drawingRect.bottom = drawingRect.top + intrinsicHeight
            if (bounds != drawingRect)
                bounds = drawingRect
            super.draw(canvas)
        }
    }
}
Arkose answered 16/1, 2018 at 14:11 Comment(5)
Why the downvote?Arkose
I have tested this and it will not work for scaled drawables, like vector drawablesHomicidal
@A.Steenbergen Actually, this works perfectly fine for VectorDrawable. It's what I tried to test. If you wish, put a link to a minimal project (here : uploadfiles.io ) , and I will show you how to fix it.Arkose
It did not resize to fill the imageView I was using it in (the non-mirrored version did). I had to change the drawingRect to left=0, top=0, right=canvas.width(), bottom=canvas.height()Homicidal
@A.Steenbergen Please show an example in a project.Arkose
L
5

Specify the center for the flip operation.

matrix.preScale(-1.0f, 1.0f, canvas.getWidth() / 2, canvas.getHeight() / 2);

Here is a custom Drawable class that you can use to mirror drawables:

public class MirroredDrawable extends Drawable {
    final Drawable mDrawable;
    final Matrix matrix = new Matrix();

    MirroredDrawable(Drawable drawable) {
        mDrawable = drawable;
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        matrix.reset();
        matrix.preScale(-1.0f, 1.0f, canvas.getWidth() / 2, canvas.getHeight() / 2);
        canvas.setMatrix(matrix);

        Rect drawingRect = new Rect();
        drawingRect.left = (canvas.getWidth() - mDrawable.getIntrinsicWidth()) / 2;
        drawingRect.top = (canvas.getHeight() - mDrawable.getIntrinsicHeight()) / 2;
        drawingRect.right = drawingRect.left + mDrawable.getIntrinsicWidth();
        drawingRect.bottom = drawingRect.top + mDrawable.getIntrinsicHeight();
        mDrawable.setBounds(drawingRect);
        mDrawable.draw(canvas);
    }

    // Other methods required to extend Drawable but aren't used here.

    @Override
    public void setAlpha(int alpha) { }

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

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

Here is how it can be applied:

    Drawable drawable = getResources().getDrawable(R.drawable.your_drawable);
    getSupportActionBar().setHomeAsUpIndicator(new MirroredDrawable(drawable));
Leadwort answered 29/1, 2018 at 13:11 Comment(8)
Doesn't seem to be shown. I use the output drawable for setHomeAsUpIndicator . Can you please try it? Or, maybe show full code, because what I did is use this on the problematic code I've made.Arkose
This works, but also the code I've written, if I had changed it to what you have. Just a small suggestion for improvement (which for some reason I didn't do): create the matrix once, as a field, and call reset() on it in the draw method, to reduce re-creation of it, over and over.Arkose
@androiddeveloper Updated. Also added code to make sure the drawable is the right size. It was too big for the "as up indicator."Leadwort
What should be implemented for the Other methods required to extend Drawable part? And how? Can you please fill it too ? Also, I've updated your code a bit.Arkose
@androiddeveloper Added them to answer. They are just stubs in this implementation.Leadwort
ok thank you for this. I guess that my solution, after I change it to have the draw implementation of yours, will also be ok, right? I've tested it and it worked fine.Arkose
@androiddeveloper Should be OK.Leadwort
OK granted bounty. Thank you for all the help.Arkose
A
4

You can use android:scaleX="-1" in the view's xml to show the drawable's mirror image. To make it work automatically depending on the layout's direction, you could use an integer resource value:

<ImageView
    android:scaleX="@integer/rtl_flip_factor"
    android:src="@android:drawable/ic_media_play"/>

To finish it off, you'd need a plain values/integers.xml for the default (LTR) case

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="rtl_flip_factor">1</integer>
</resources>

and another values-ldrtl/integers.xml for the RTL case:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="rtl_flip_factor">-1</integer>
</resources>
Avocet answered 28/1, 2018 at 23:59 Comment(1)
I need it in code, programmatically. I use it for setHomeAsUpIndicator, in case the device is in RTL locale, because autoMirrored is only supported on new Android APIs. There is no ImageView in my case. Just a Drawable.Arkose
R
0

Set android:autoMirrored="true" in your XML vector file,

Remde answered 13/2, 2022 at 23:13 Comment(3)
The question is how to do it programmatically, for any drawable. Not just VectorDrawable..Arkose
okay good , may help someone searching for mirroring xml vectorDrawableRemde
No, because I already mentioned that I know of "autoMirrored" and wanted to apply it to Drawable.Arkose

© 2022 - 2024 — McMap. All rights reserved.