[TUT] Programmatically create a RippleDrawable of any color

This tutorial is going to be quick and to the point. When you want to create a Button or other Android Widget you more than likely want a ripple effect when it is pressed. I’ll show you how to do this. What we’ll do:

– Create a simple RippleDrawable
– Ensure it reacts to touch with different colors
– Add it to a Button
– Consider lower versions of Android without ripples

Ok here .. we .. go

You make a RippleDrawable like the below, however if you read the javadoc the param’s have the names color, content or mask. These I feel aren’t the most explanatory, so check the names below:

new RippleDrawable(pressedColor, defaultColor, rippleColor);

The RippleDrawable needs 3 parameters, lets build each one at a time.

ColorStateList pressedColor
ColorDrawable defaultColor
Drawable rippleColor

This tutorial is for creating a RippleDrawable for any color. The example will use red.

int color = Color.parseColor("#ff0000");
ColorDrawable defaultColor = new ColorDrawable(color);

To create the pressedColor we need to lighten or darken the defaultColor, because the code we are writing here can account for any color we check what the passed in color is and lighten it if we can otherwise we darken it. For example a red of #ff0000 cannot be lightened any more so we darken it instead. We allow our method to take a fraction so that we can control the lightening/darkening, a good value is 0.2.

        public static int lightenOrDarken(int color, double fraction) {
            if (canLighten(color, fraction)) {
                return lighten(color, fraction);
            } else {
                return darken(color, fraction);
            }
        }

        private static int lighten(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            red = lightenColor(red, fraction);
            green = lightenColor(green, fraction);
            blue = lightenColor(blue, fraction);
            int alpha = Color.alpha(color);
            return Color.argb(alpha, red, green, blue);
        }

        private static int darken(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            red = darkenColor(red, fraction);
            green = darkenColor(green, fraction);
            blue = darkenColor(blue, fraction);
            int alpha = Color.alpha(color);

            return Color.argb(alpha, red, green, blue);
        }

        private static boolean canLighten(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            return canLightenComponent(red, fraction)
                && canLightenComponent(green, fraction)
                && canLightenComponent(blue, fraction);
        }

        private static boolean canLightenComponent(int colorComponent, double fraction) {
            int red = Color.red(colorComponent);
            int green = Color.green(colorComponent);
            int blue = Color.blue(colorComponent);
            return red + (red * fraction) < 255
                && green + (green * fraction) < 255
                && blue + (blue * fraction) < 255;
        }

        private static int darkenColor(int color, double fraction) {
            return (int) Math.max(color - (color * fraction), 0);
        }

        private static int lightenColor(int color, double fraction) {
            return (int) Math.min(color + (color * fraction), 255);
        }

Once we have the pressedColor and defaultColor we then build the rippleColor from the original color:

        private static Drawable getRippleColor(int color) {
            float[] outerRadii = new float[8];
            Arrays.fill(outerRadii, 3);
            RoundRectShape r = new RoundRectShape(outerRadii, null, null);
            ShapeDrawable shapeDrawable = new ShapeDrawable(r);
            shapeDrawable.getPaint().setColor(color);
            return shapeDrawable;
        }

Thats all the pieces for a RippleDrawable that will react to touches. To use it on a Button:

Button myButton = ...
myButton.setBackground(rippleDrawable);

RippleDrawable is only available from Android Lollipop, so if you want to still create programmatically colored buttons with touch feedback you can do the below:

                StateListDrawable stateListDrawable = new StateListDrawable();
                stateListDrawable.addState(
                    new int[]{android.R.attr.state_pressed},
                    new ColorDrawable(lightenOrDarken(color, 0.20D))
                );
                stateListDrawable.addState(
                    new int[]{android.R.attr.state_focused},
                    new ColorDrawable(lightenOrDarken(color, 0.40D))
                );
                stateListDrawable.addState(
                    new int[]{},
                    new ColorDrawable(color)
                );

Putting it all together you can have a class to create your programmatic RippleDrawable and fallback to the StateListDrawable on lower Android versions. Noticing they both implement Drawable.

    public class Drawables {

        @NonNull
        public static Drawable getSelectableDrawableFor(int color) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                StateListDrawable stateListDrawable = new StateListDrawable();
                stateListDrawable.addState(
                    new int[]{android.R.attr.state_pressed},
                    new ColorDrawable(lightenOrDarken(color, 0.20D))
                );
                stateListDrawable.addState(
                    new int[]{android.R.attr.state_focused},
                    new ColorDrawable(lightenOrDarken(color, 0.40D))
                );
                stateListDrawable.addState(
                    new int[]{},
                    new ColorDrawable(color)
                );
                return stateListDrawable;
            } else {
                ColorStateList pressedColor = ColorStateList.valueOf(lightenOrDarken(color, 0.2D));
                ColorDrawable defaultColor = new ColorDrawable(color);
                Drawable rippleColor = getRippleColor(color);
                return new RippleDrawable(
                    pressedColor,
                    defaultColor,
                    rippleColor
                );
            }
        }

        @NonNull
        private static Drawable getRippleColor(int color) {
            float[] outerRadii = new float[8];
            Arrays.fill(outerRadii, 3);
            RoundRectShape r = new RoundRectShape(outerRadii, null, null);
            ShapeDrawable shapeDrawable = new ShapeDrawable(r);
            shapeDrawable.getPaint().setColor(color);
            return shapeDrawable;
        }

        private static int lightenOrDarken(int color, double fraction) {
            if (canLighten(color, fraction)) {
                return lighten(color, fraction);
            } else {
                return darken(color, fraction);
            }
        }

        private static int lighten(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            red = lightenColor(red, fraction);
            green = lightenColor(green, fraction);
            blue = lightenColor(blue, fraction);
            int alpha = Color.alpha(color);
            return Color.argb(alpha, red, green, blue);
        }

        private static int darken(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            red = darkenColor(red, fraction);
            green = darkenColor(green, fraction);
            blue = darkenColor(blue, fraction);
            int alpha = Color.alpha(color);

            return Color.argb(alpha, red, green, blue);
        }

        private static boolean canLighten(int color, double fraction) {
            int red = Color.red(color);
            int green = Color.green(color);
            int blue = Color.blue(color);
            return canLightenComponent(red, fraction)
                && canLightenComponent(green, fraction)
                && canLightenComponent(blue, fraction);
        }

        private static boolean canLightenComponent(int colorComponent, double fraction) {
            int red = Color.red(colorComponent);
            int green = Color.green(colorComponent);
            int blue = Color.blue(colorComponent);
            return red + (red * fraction) < 255
                && green + (green * fraction) < 255
                && blue + (blue * fraction) < 255;
        }

        private static int darkenColor(int color, double fraction) {
            return (int) Math.max(color - (color * fraction), 0);
        }

        private static int lightenColor(int color, double fraction) {
            return (int) Math.min(color + (color * fraction), 255);
        }

    }

Then you use it like so:

Button myButton = ...
myButton.setBackground(Drawables.getSelectableDrawableFor(Color.parseColor("#ff0000"));

Hope that helps!