StateListDrawable to switch colorfilters

走远了吗. 提交于 2019-11-28 23:44:25

OK, I never got the above code to work, so here's what I ended up doing.

First, I subclassed LayerDrawable:

public class StateDrawable extends LayerDrawable {

    public StateDrawable(Drawable[] layers) {
        super(layers);
    }

    @Override
    protected boolean onStateChange(int[] states) {
        for (int state : states) {
            if (state == android.R.attr.state_selected) {
                super.setColorFilter(Color.argb(255, 255, 195, 0), PorterDuff.Mode.SRC_ATOP);
            } else {
                super.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_ATOP);
            }
        }
        return super.onStateChange(states);
    }

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

}

I changed the buildTab() method to the following:

private View buildTab(int icon, int label) {
    LayoutInflater inflater = LayoutInflater.from(this);
    View view = inflater.inflate(R.layout.tab_button, null);
    ((ImageView) view.findViewById(R.id.tab_icon)).setImageDrawable(new StateDrawable(new Drawable[] { getResources()
          .getDrawable(icon) }));
    ((TextView) view.findViewById(R.id.tab_text)).setText(getString(label));
    return view;
}

I still add the tabs like this:

Intent fooIntent = new Intent().setClass(this, FooActivity.class);
tabHost.addTab(tabHost.newTabSpec(TAB_NAME_INFO).setIndicator(buildTab(R.drawable.tab_icon_info, R.string.info)).setContent(infoIntent));

This works for me, compatible with android 1.6.

Couldn't solve it with applying a colorfilter directly to the drawable either. What worked for me was getting the image as a Bitmap, create an empty second one with same measures, define a canvas for the second one, apply that colorfilter to a paint object and draw the first bitmap on the second one. Finally create a BitmapDrawable from the new Bitmap and you're done. Here is the code

    ImageButton imageButton = (ImageButton)findViewById(R.id.aga);

    Bitmap one = BitmapFactory.decodeResource(getResources(), R.drawable.pen_circle);
    Bitmap oneCopy = Bitmap.createBitmap(one.getWidth(), one.getHeight(), Config.ARGB_8888);

    Canvas c = new Canvas(oneCopy);
    Paint p = new Paint();
    p.setColorFilter(new LightingColorFilter(Color.CYAN, 1));
    c.drawBitmap(one, 0, 0, p);

    StateListDrawable states = new StateListDrawable();
    states.addState(new int[] {android.R.attr.state_pressed}, new BitmapDrawable(oneCopy));
    states.addState(new int[] { }, imageButton.getDrawable());
    imageButton.setImageDrawable(states);

This is my class, hacked to support ColorFilter:

Usage:

final Drawable icon = getResources().getDrawable(iconResId);
final Drawable filteredIcon = // this is important
        icon.getConstantState().newDrawable();
final FilterableStateListDrawable selectorDrawable =
        new FilterableStateListDrawable();
selectorDrawable.addState(ICON_STATE_SELECTED, filteredIcon,
        new PorterDuffColorFilter(mIconOverlayColor, PorterDuff.Mode.SRC_ATOP));
selectorDrawable.addState(ICON_STATE_DEFAULT, icon);

As you see the ColorFilter is not applied directly to the drawable, it is associated to it while adding a state to the selector Drawable.

What's important here is that

  • you need to create a new drawable from the constant state or you'll modify the constant state and thus any instance of that drawable around your activity.
  • you need to use my custom addState method, it has the same name of the framework method addState but I've added an additional argument (ColorFilter). This method does NOT exist in the framework superclass!

The code (dirty, but work for me):

/**
 * This is an extension to {@link android.graphics.drawable.StateListDrawable} that workaround a bug not allowing
 * to set a {@link android.graphics.ColorFilter} to the drawable in one of the states., it add a method
 * {@link #addState(int[], android.graphics.drawable.Drawable, android.graphics.ColorFilter)} for that purpose.
 */
public class FilterableStateListDrawable extends StateListDrawable {

    private int currIdx = -1;
    private int childrenCount = 0;
    private SparseArray<ColorFilter> filterMap;

    public FilterableStateListDrawable() {
        super();
        filterMap = new SparseArray<ColorFilter>();
    }

    @Override
    public void addState(int[] stateSet, Drawable drawable) {
        super.addState(stateSet, drawable);
        childrenCount++;
    }

    /**
     * Same as {@link #addState(int[], android.graphics.drawable.Drawable)}, but allow to set a colorFilter associated to this Drawable.
     *
     * @param stateSet    - An array of resource Ids to associate with the image.
     *                    Switch to this image by calling setState().
     * @param drawable    -The image to show.
     * @param colorFilter - The {@link android.graphics.ColorFilter} to apply to this state
     */
    public void addState(int[] stateSet, Drawable drawable, ColorFilter colorFilter) {
        // this is a new custom method, does not exist in parent class
        int currChild = childrenCount;
        addState(stateSet, drawable);
        filterMap.put(currChild, colorFilter);
    }

    @Override
    public boolean selectDrawable(int idx) {
        if (currIdx != idx) {
            setColorFilter(getColorFilterForIdx(idx));
        }
        boolean result = super.selectDrawable(idx);
        // check if the drawable has been actually changed to the one I expect
        if (getCurrent() != null) {
            currIdx = result ? idx : currIdx;
            if (!result) {
                // it has not been changed, meaning, back to previous filter
                setColorFilter(getColorFilterForIdx(currIdx));
            }
        } else if (getCurrent() == null) {
            currIdx = -1;
            setColorFilter(null);
        }
        return result;
    }

    private ColorFilter getColorFilterForIdx(int idx) {
        return filterMap != null ? filterMap.get(idx) : null;
    }
}

I've opened a bug about this: https://code.google.com/p/android/issues/detail?id=60183

UPDATE: the bug has been fixed in the framework, since Lollipop I think. I think the fix commit is this: https://android.googlesource.com/platform/frameworks/base/+/729427d%5E!/

or on Github: https://github.com/android/platform_frameworks_base/commit/729427d451bc4d4d268335b8dc1ff6404bc1c91e

My workaround should still work after Lollipop, it just don't use the fix by Google.

Here is my variation of Mopper's code. The idea is that ImageView gets color filter when user touches it, and color filter is removed when user stops touching it.

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));
Rafael Nobre

Here is my variation of @Malachiasz code, this lets you pick whatever combination of states and colors to apply to the base drawable.

public class ColorFilteredStateDrawable extends StateListDrawable {

    private final int[][] states;
    private final int[] colors;

    public ColorFilteredStateDrawable(Drawable drawable, int[][] states,  int[] colors) {
        super();
        drawable.mutate();
        this.states = states;
        this.colors = colors;
        for (int i = 0; i < states.length; i++) {
            addState(states[i], drawable);
        }
    }

    @Override
    protected boolean onStateChange(int[] states) {
        if (this.states != null) {
            for (int i = 0; i < this.states.length; i++) {
                    if (StateSet.stateSetMatches(this.states[i], states)) {
                        super.setColorFilter(this.colors[i], PorterDuff.Mode.MULTIPLY);
                        return super.onStateChange(states);
                    }
            }
            super.clearColorFilter();
        }
        return super.onStateChange(states);
    }

    @Override
    public boolean isStateful() {
        return true;
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!