Tint / dim drawable on touch

2019-03-08 20:25发布

The app I'm currently working on uses a lot of ImageViews as buttons. The graphics on these buttons use the alpha channel to fade out the edges of the button and make them look irregular. Currently, we have to generate 2 graphics for each button (1 for the selected/focused/pressed state and the other for the default unselected state) and use a StateListDrawable defined in an XML file for each button.

While this works fine it seems extremely wasteful since all of the selected graphics are simply tinted versions of the unselected buttons. These take time to produce (however little) and take up space in the final APK. It seems like there should be an easy way to this automatically.

The perfect solution, it would seem, is to use ImageViews for each button and specify in its tint attribute a ColorStateList. This approach has the advantage that only a single XML ColorStateList is needed for all of the buttons (that share the same tint). However it does not work. As mentioned here, ImageView throws a NumberFormatException when the value provided to tint is anything other than a single color.

My next attempt was to use a LayerDrawable for the selected drawable. Inside the layer list, we would have the original image at the bottom of the stack covered by a semi-transparent rectangle. This worked on the solid parts of the button graphic. However the edges which were supposed to be entirely transparent, were now the same color as the top layer.

Has anyone encountered this issue before and found a reasonable solution? I would like to stick to XML approaches but will probably code up a simple ImageView subclass that will do the required tinting in code.

8条回答
唯我独甜
2楼-- · 2019-03-08 21:06

You can combine StateListDrawable and LayerDrawable for this.

public Drawable getDimmedDrawable(Drawable drawable) {
        Resources resources = getContext().getResources();
        StateListDrawable stateListDrawable = new StateListDrawable();
        LayerDrawable layerDrawable = new LayerDrawable(new Drawable[]{
                drawable,
                new ColorDrawable(resources.getColor(R.color.translucent_black))
        });
        stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, layerDrawable);
        stateListDrawable.addState(new int[]{android.R.attr.state_focused}, layerDrawable);
        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, layerDrawable);
        stateListDrawable.addState(new int[]{}, drawable);
        return stateListDrawable;
}

I assume our colors.xml looks like this

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="translucent_black">#80000000</color>
</resources>
查看更多
啃猪蹄的小仙女
3楼-- · 2019-03-08 21:08

I think this solution is simple enough:

    final Drawable drawable = ... ;
    final int darkenValue = 0x3C000000;
    mButton.setOnTouchListener(new OnTouchListener() {
        Rect rect;
        boolean hasColorFilter = false;

        @Override
        public boolean onTouch(final View v, final MotionEvent motionEvent) {
            switch (motionEvent.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    rect = new Rect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
                    drawable.setColorFilter(darkenValue, Mode.DARKEN);
                    hasColorFilter = true;
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (rect.contains(v.getLeft() + (int) motionEvent.getX(), v.getTop()
                            + (int) motionEvent.getY())) {
                        if (!hasColorFilter)
                            drawable.setColorFilter(darkenValue, Mode.DARKEN);
                        hasColorFilter = true;
                    } else {
                        drawable.setColorFilter(null);
                        hasColorFilter = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    drawable.setColorFilter(null);
                    hasColorFilter = false;
                    break;
            }
            return false;
        }
    });
查看更多
别忘想泡老子
4楼-- · 2019-03-08 21:18

You can use a class I've created https://github.com/THRESHE/TintableImageButton Not exactly the best solution but it works.

查看更多
女痞
5楼-- · 2019-03-08 21:25

I too have irregular looking buttons and needed the tint. In the end, I just resorted to having a state colorlist as my ImageButton background. Gives the impression that the button color is changing on press and is very straightforward and less compute intensive. I know that this isn't exactly a tint, but it does give it does give necessary visual feedback to the user in what usually is a fleeting moment.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:state_pressed="true"
        android:drawable="@android:color/darker_gray" />
    <item android:drawable="@android:color/transparent" />
</selector>
查看更多
Anthone
6楼-- · 2019-03-08 21:26

For those who have encountered a similar need, solving this in code is fairly clean. Here is a sample:

public class TintableButton extends ImageView {

    private boolean mIsSelected;

    public TintableButton(Context context) {
        super(context);
        init();
    }   

    public TintableButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }   

    public TintableButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mIsSelected = false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN && !mIsSelected) {
            setColorFilter(0x99000000);
            mIsSelected = true;
        } else if (event.getAction() == MotionEvent.ACTION_UP && mIsSelected) {
            setColorFilter(Color.TRANSPARENT);
            mIsSelected = false;
        }

        return super.onTouchEvent(event);
    }
}

It's not finished but works well as a proof of concept.

查看更多
Root(大扎)
7楼-- · 2019-03-08 21:28

Your answer was also excellent ;)

I am modifying little bit, it will give you result like this:

button_normal button_pressed

Rect rect;
Drawable background;
boolean hasTouchEventCompleted = false;

@Override
public boolean onTouch(View v, MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        rect = new Rect(v.getLeft(), v.getTop(), v.getRight(),
                v.getBottom());
        hasTouchEventCompleted = false;
        background = v.getBackground();
        background.setColorFilter(0x99c7c7c7,
                android.graphics.PorterDuff.Mode.MULTIPLY);
        v.setBackgroundDrawable(background);
    } else if (event.getAction() == MotionEvent.ACTION_UP
            && !hasTouchEventCompleted) {
        background.setColorFilter(null);
        v.setBackgroundDrawable(background);

    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
        if (!rect.contains(v.getLeft() + (int) event.getX(), v.getTop()
                + (int) event.getY())) {
            // User moved outside bounds
            background.setColorFilter(null);
            v.setBackgroundDrawable(background);
            hasTouchEventCompleted = true;
        }
    }


    //Must return false, otherwise you need to handle Click events yourself.  
    return false;
}
查看更多
登录 后发表回答