StateListDrawable to switch colorfilters

后端 未结 5 968
傲寒
傲寒 2020-12-15 11:34

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. S

相关标签:
5条回答
  • 2020-12-15 11:41

    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-15 11:43

    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));
    
    0 讨论(0)
  • 2020-12-15 11:53

    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);
    
    0 讨论(0)
  • 2020-12-15 11:58

    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.

    0 讨论(0)
  • 2020-12-15 11:58

    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.

    0 讨论(0)
提交回复
热议问题