Android - Set Button/ImageView states with one Drawable (PNG image)

    Building states list for ImageView or Button (pressed, enabled, disabled) will make our app more friendly and less rigid. Button - by default, have had it's own states but ImageView is not, so we have to customize states list for them. One of the things that used to drive me crazy was that we needed to create a full stack of various PNG’s to create different states on our drawables. That means if I wanted a button with a white default state, orange pressed state and grey disabled state I’d have to create three PNG’s (for each density) which as you know is a huge number of PNG’s and a real pain to update when the time comes.
    With this situation, we must find the way to make ImageView/Button states list with only one PNG image (drawable). Through this post, I will present 2 solutions to resolved this problem, they are so simple and easy to apply in your code.

Solution 1: Use StateListDrawable

   With this solution we have one PNG and then we use some Java code to create a StateListDrawable at runtime. The core of this process is make some "copy versions" of the drawable to make states list. Usally, we will make 3 states: pressed, enabled and disabled. Firstly, create a Bitmap and "copy version" by decoding Drawables resource:
        // Create the colorized image (pressed state)
        Bitmap one = BitmapFactory.decodeResource(context.getResources(), imageResource);
        Bitmap oneCopy = Bitmap.createBitmap(one.getWidth(), one.getHeight(), Bitmap.Config.ARGB_8888);
    Put "state Bitmap" into a Canvas and set it's color status:
        Canvas c = new Canvas(oneCopy);
        Paint p = new Paint();
        int color = ContextCompat.getColor(context, desiredColor);
        p.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
        c.drawBitmap(one, 0, 0, p);
    Finally, adding this state (pressed) to an StateListDrawable, we'll have a new state which is not the default:
        // Pressed State
        stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(context.getResources(), oneCopy));
    Put these step in a static method, wrap in a separated class, we can use it any where later:
package info.devexchanges.statesdrawable;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.StateListDrawable;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntRange;
import android.support.v4.content.ContextCompat;

public class DrawableUtils {

    public static StateListDrawable getStateListDrawable(Context context,
                                                         @DrawableRes int imageResource,
                                                         @ColorRes int desiredColor,
                                                         @IntRange(from = 0, to = 255) int disableAlpha) {

        // Create the colorized image (pressed state)
        Bitmap one = BitmapFactory.decodeResource(context.getResources(), imageResource);
        Bitmap oneCopy = Bitmap.createBitmap(one.getWidth(), one.getHeight(), Bitmap.Config.ARGB_8888);

        Canvas c = new Canvas(oneCopy);
        Paint p = new Paint();
        int color = ContextCompat.getColor(context, desiredColor);
        p.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
        c.drawBitmap(one, 0, 0, p);

        // Create the disabled bitmap for the disabled state
        Bitmap disabled = BitmapFactory.decodeResource(context.getResources(), imageResource);
        Bitmap disabledCopy = Bitmap.createBitmap(disabled.getWidth(), disabled.getHeight(), Bitmap.Config.ARGB_8888);

        Canvas disabledCanvas = new Canvas(disabledCopy);
        Paint alphaPaint = new Paint();
        alphaPaint.setAlpha(disableAlpha);
        disabledCanvas.drawBitmap(disabled, 0, 0, alphaPaint);

        StateListDrawable stateListDrawable = new StateListDrawable();

        // Pressed State
        stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, new BitmapDrawable(context.getResources(), oneCopy));

        // Disabled State
        stateListDrawable.addState(new int[]{-android.R.attr.state_enabled}, new BitmapDrawable(context.getResources(), disabledCopy));  // - symbol means opposite, in this case "disabled"

        // Default State
        stateListDrawable.addState(new int[]{}, ContextCompat.getDrawable(context, imageResource));

        return stateListDrawable;
    }
}
    In Activity, it's convenient to use through call:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            btnEnabled.setBackgroundDrawable(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.holo_red_light, 100));
            btnDisabled.setBackgroundDrawable(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.darker_gray, 100));
        } else {
            btnEnabled.setBackground(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.holo_red_light, 100));
            btnDisabled.setBackground(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.darker_gray, 100));
        }
    Note: I checked the sdk-version because setBackground() only available from API 16, so in the lower API device, use setBackgroundDrawable() instead.
Output when click Button:

Solution 2: Use DrawableCompat

    This approach looks simpler, DrawableCompat is the helper for accessing features in Drawable introduced after API level 4 in a backwards compatible fashion.. Firstly, put a drawable selector (xml) file in res/color folder:
    In programmatically code, just through setTinList() and setTinMode(), states which define in selector will be active:
        Drawable normalDrawable = ContextCompat.getDrawable(this, R.mipmap.logo);
        Drawable wrapDrawable = DrawableCompat.wrap(normalDrawable);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            DrawableCompat.setTintList(wrapDrawable, getResources().getColorStateList(R.color.drawable_selector, this.getTheme()));
        } else {
            DrawableCompat.setTintList(wrapDrawable, getResources().getColorStateList(R.color.drawable_selector));
        }
        DrawableCompat.setTintMode(wrapDrawable, PorterDuff.Mode.SRC_IN);
        imageView.setImageDrawable(wrapDrawable);
    Output when click at this ImageView:

Full Project code

    Finally, this is complete code for our activity, which use both 2 solutions in ImageView and Buttons:
package info.devexchanges.statesdrawable;

import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button btnEnabled = (Button) findViewById(R.id.btn_enabled);
        Button btnDisabled = (Button) findViewById(R.id.btn_disabled);
        ImageView imageView = (ImageView) findViewById(R.id.image);

        //Use DrawableUtils
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            btnEnabled.setBackgroundDrawable(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.holo_red_light, 100));
            btnDisabled.setBackgroundDrawable(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.darker_gray, 100));
        } else {
            btnEnabled.setBackground(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.holo_red_light, 100));
            btnDisabled.setBackground(DrawableUtils.getStateListDrawable(this, R.mipmap.bg_green, android.R.color.darker_gray, 100));
        }

        //Use DrawableCompat
        Drawable normalDrawable = ContextCompat.getDrawable(this, R.mipmap.logo);
        Drawable wrapDrawable = DrawableCompat.wrap(normalDrawable);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            DrawableCompat.setTintList(wrapDrawable, getResources().getColorStateList(R.color.drawable_selector, this.getTheme()));
        } else {
            DrawableCompat.setTintList(wrapDrawable, getResources().getColorStateList(R.color.drawable_selector));
        }
        DrawableCompat.setTintMode(wrapDrawable, PorterDuff.Mode.SRC_IN);
        imageView.setImageDrawable(wrapDrawable);
    }
}
    And it's layout:
 

Conclusions

    With 2 solutions I have presented in this post, you can make states for Button/ImageView from 1 drawable file. I prefer 2nd solution (use DrawableCompat) because of it's code is so light. Moreover, the support libary present TintImageView that has very similar settings but it is inside of the internal package inside of android.support.v7. Traditionally anything inside of an internal package name indicates that it is not a public API that should be consumed or relied upon, so you shouldn't use it, choose one of the two above ways instead!

Share


Previous post
« Prev Post
Next post
Next Post »