Android: Cloning a drawable in order to make a StateListDrawable with filters
Asked Answered
G

7

100

I'm trying to make a general framework function that makes any Drawable become highlighted when pressed/focused/selected/etc.

My function takes a Drawable and returns a StateListDrawable, where the default state is the Drawable itself, and the state for android.R.attr.state_pressed is the same drawable, just with a filter applied using setColorFilter.

My problem is that I can't clone the drawable and make a separate instance of it with the filter applied. Here is what I'm trying to achieve:

StateListDrawable makeHighlightable(Drawable drawable)
{
    StateListDrawable res = new StateListDrawable();

    Drawable clone = drawable.clone(); // how do I do this??

    clone.setColorFilter(0xFFFF0000, PorterDuff.Mode.MULTIPLY);
    res.addState(new int[] {android.R.attr.state_pressed}, clone);
    res.addState(new int[] { }, drawable);
    return res;
}

If I don't clone then the filter is obviously applied to both states. I tried playing with mutate() but it doesn't help..

Any ideas?

Update:

The accepted answer indeed clones a drawable. It didn't help me though because my general function fails on a different problem. It seems that when you add a drawable to a StateList, it loses all its filters.

Georgiegeorgina answered 2/11, 2011 at 11:17 Comment(5)
Hi, did you find a solution to drawables loosing filters? I've run into same issue :( I ended up generating other image from source image by cloning Bitmap and applying filter pixel-by-pixel. Yes, this is inefficient, but I have just a bunch of small images processed once.Daytime
I couldn't solve it with StateListDrawable, but if you're not using StateListDrawable and still losing your filters, make sure your bitmaps are mutable. There are good related questions: #5500137 , also I've discovered that LightingColorFilter works in places where PorterDuff fails.. lovin this android :)Georgiegeorgina
a great answer on this link #10889915Catechu
There is a similar side effect triggered by ImageView.setImageDrawable, which I was able to work around thanks to the accepted answer.Canker
I am trying to do the same thing and it works as expected somehow, the ColorFilter did not get lost... The difference is that I did mutate the drawable.Semiannual
E
176

Try the following:

Drawable clone = drawable.getConstantState().newDrawable();
Evidence answered 2/11, 2011 at 12:53 Comment(9)
Thanks! This method seems to clone a drawable successfully. The function I was trying to write though does not work.. It seems that when a drawable is inserted into a StateList it loses its filters :(Georgiegeorgina
+1 for helping me fix a very strange error in MapView where re-using a Drawable from the ItemizedOverlay in an AlertDialog made the ItemizedOverlay move when triggered. Making a new instance of the Drawable fixed the problem.Lenni
Do to worked correctly, if we try use setAlpha method. In this case both drawable change bitmap. Then I get first drawable as: getResources().getDrawable(), second as: getResources().getDrawable().mutate().Verne
Thanks a lot, it fixed the problem I had when I applied a bounding function, from the API Mapsforge. Now I can succesfully use the drawables everywhere!Thanhthank
Now i can deep clone BitmapDrawables and NinePatchDrawables THANKSConveyance
@Evidence - I tried this with a color filter, but it colored all instances of my drawable! Turns out it looks lik you have to use .mutate() (see my answer).Mcknight
Aggree with @PeterAjtai, it works but colored all instances of my drawables... (See PeterAjtai answer)Harmonize
I have a LayerDrawable, which I wanted to change the colorfilter of one of its inner drawable. I tried to use this, but it didn't work. Am I doing it wrong?Dairymaid
Will there a problem since .getConstantState() is nullable?Shrunken
M
117

If you apply a filter / etc to a drawable created with getConstantState().newDrawable() then all instances of that drawable will be changed as well, since drawables use the constantState as a cache!

So if you color a circle using a color filter and a newDrawable(), you will change the color of all the circles.

If you want to make this drawable updatable without affecting other instances then, then you must mutate that existing constant state.

// To make a drawable use a separate constant state
drawable.mutate()

For a good explanation see:

http://www.curious-creature.org/2009/05/02/drawable-mutations/

http://developer.android.com/reference/android/graphics/drawable/Drawable.html#mutate()

Mcknight answered 30/8, 2013 at 23:45 Comment(7)
In fact mutate() does return the same exact same instance, but its internal state is changed so applying a color filter will not impact other instances. Can you review and fix your answer?Textualism
@Textualism if you don't use mutate all the instances of the color change - you need to call mutate in order to only change the color of the cloneMcknight
Check the API ref ("Make this drawable mutable. - Returns this drawable") and the source code ("return this"). Calling mutate() is required, but the returned instance is the same. This does not create a clone, this only changes the internal state of the drawable instance to allow modifying it without impacting other instances of the same drawable.Textualism
Well I don't know about the question, but this answer does the exact thing I needed... tUSilveira
I am encountering this very problem only on Lollipop, but it used to work fine even on KitKat. It is a very strange issues but thanks for the pointer on mutate()Eddra
Those are the best Links, ones which you gave for referenceSherwood
Does using this method ("mutate") disable the caching caching of drawables via getResources().getDrawable ?Dairymaid
S
22

This is what works for me.

Drawable clone = drawable.getConstantState().newDrawable().mutate();
Steinbok answered 24/3, 2017 at 23:53 Comment(1)
YES I don not know WHY but only this combination newDrawable() and mutate() works for me any other single mutate() or single newDrawable() doesn't work correctly for meCountryandwestern
D
12

This is my solution, based on this SO question.

The idea is that ImageView gets color filter when user touches it, and color filter is removed when user stops touching it. Only 1 drawable/bitmap is in memory, so no need to waste it. It works as it should.

class PressedEffectStateListDrawable extends StateListDrawable {

    private int selectionColor;

    public PressedEffectStateListDrawable(Drawable drawable, int selectionColor) {
        super();
        this.selectionColor = selectionColor;
        addState(new int[] { android.R.attr.state_pressed }, drawable);
        addState(new int[] {}, drawable);
    }

    @Override
    protected boolean onStateChange(int[] states) {
        boolean isStatePressedInArray = false;
        for (int state : states) {
            if (state == android.R.attr.state_pressed) {
                isStatePressedInArray = true;
            }
        }
        if (isStatePressedInArray) {
            super.setColorFilter(selectionColor, PorterDuff.Mode.MULTIPLY);
        } else {
            super.clearColorFilter();
        }
        return super.onStateChange(states);
    }

    @Override
    public boolean isStateful() {
        return true;
    }
}

usage:

Drawable drawable = new FastBitmapDrawable(bm);
imageView.setImageDrawable(new PressedEffectStateListDrawable(drawable, 0xFF33b5e5));
Duplicate answered 28/1, 2014 at 15:30 Comment(2)
Works for me also! Thats an interesting solution, thanks!) P.S. android sucks, such bad not properly working API :(Reyreyes
I think this is the best solution by far to solve bugs in (StateListDrawable + BitmapDrawable)!Foret
R
1

I answered a related question here

Basically it seems like StateListDrawables indeed lose their filters. I created a new BitmapDrawale from a altered copy of the Bitmap I originally wanted to use.

Restful answered 13/5, 2012 at 12:37 Comment(0)
P
1

Get clone drawable using newDrawable() but make sure it is mutable otherwise your clone effect gone, I used these few lines of code and it is working as expected. getConstantState() may be null as suggested by annotation, so handle this RunTimeException while you cloning drawable.

Drawable.ConstantState state = d.mutate().getConstantState();
if (state != null) {
    Drawable drawable = state.newDrawable().mutate();
}
Pashto answered 22/8, 2020 at 6:2 Comment(1)
This worked, but you don't need to mutate it.Counterbalance
M
0
Drawable clone = drawable.mutate().getConstantState().newDrawable().mutate();

in case getConstantState() returns null.

Marchesa answered 6/12, 2019 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.