StateListDrawable to switch colorfilters

只谈情不闲聊 提交于 2019-12-18 02:46:43

问题


I want to create custom buttons to use in a TabHost. I haven been trying to just use the same image resource (png), but have the colorfilter change depending on the state. So I made this bit to serve as the layout for the custom button:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent" android:layout_height="fill_parent">
    <ImageView android:id="@+id/tab_icon"
        android:layout_centerInParent="true" android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true" android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView android:id="@+id/tab_text" android:layout_below="@id/tab_icon"
        android:layout_centerHorizontal="true" android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

In my activity, I add the tabs like this:

tabHost.addTab(tabHost.newTabSpec(TAB_NAME_NEWS).setIndicator(buildTab(R.drawable.tab_icon_news, R.string.news))
          .setContent(newsIntent));

And this is the 'buildTab' method:

private final static int[] SELECTED = new int[] { android.R.attr.state_selected };
private final static int[] IDLE = new int[] { -android.R.attr.state_selected };

private View buildTab(int icon, int label) {
    LayoutInflater inflater = LayoutInflater.from(this);
    View view = inflater.inflate(R.layout.tab_button, null);
    StateListDrawable drawable = new StateListDrawable();

    Drawable selected = getResources().getDrawable(icon);
    selected.mutate();
    selected.setBounds(0, 0, selected.getIntrinsicWidth(), selected.getIntrinsicHeight());
    selected.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0x0000FF00));
    drawable.addState(SELECTED, selected);

    Drawable idle = getResources().getDrawable(icon);
    idle.mutate();
    idle.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0x000000FF));
    drawable.addState(IDLE, idle);

    ((ImageView) view.findViewById(R.id.tab_icon)).setImageDrawable(drawable);
    ((TextView) view.findViewById(R.id.tab_text)).setText(getString(label));
    return view;
}

In the selected state, the image should be completely green (0x0000FF00), and in the non-selected state, it should be blue (0x000000FF).

The problem is that the colorfilters appear to be be completely ignored. I can not see the colors change under any circumstances.

I've also tried to get the same result by setting the android:tint property on the <ImageView/>, but apparently you cannot use a reference to a <selector> there, since it throws a NumberFormatException.

I don't see what I'm doing wrong so any help would be appreciated.


回答1:


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.




回答2:


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



回答3:


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.




回答4:


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



回答5:


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;
    }
}


来源:https://stackoverflow.com/questions/6018602/statelistdrawable-to-switch-colorfilters

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