Android - Tinting RippleDrawables

     Android starting from Lollipop provided ability to tint UI elements for better user feedback. Pressing and holding a button starts a tint animation which is stopped as soon as the user lets go of the button. This is done by handling the touch event and using appropriate drawable resources to redraw the button. Besides, the tint animation is pivoted at the touch coordinates known as a hot spot. Lollipop by default supports tint animations for all applications and the SDK offers API to customize the color. This would be needed for applications which have a custom color for the UI elements and the platform's tint color doesn't necessarily work with the app custom color. Besides, new versions of support library even offers compat versions to apply tint colors.

   DrawableCompat offers a setTintList API to apply tint colors based on various states of the drawable resource. This is usually applicable for stateful drawables (Drawable.isStateful).

        DrawableCompat.setTintList(Drawable drawable, ColorStateList tint)

  Now, applying this to a button by extracting its background as a Drawable ( Button.getBackground()) should work as expected. However, the tint animation doesn't seem to pick the specified color. It turns out that setTintList() doesn't have any effect on RippleDrawables. RippleDrawable being a extension of LayerDrawable should support setTintList(). This is more so because setTintList is defined at Drawable and it would seem that all kind of Drawables would support the feature. So what is different with RippleDrawable? It turns out that RippleDrawable disables layer specific feature of LayerDrawable. It doesn't super any specific constructor of LayerDrawable causing the default one to be invoked.

    LayerDrawable() {
        this((LayerState) null, null);

    }

   setTintList on a LayerDrawable constructed via default constructor is basically a no-op as mLayerState.mNum is 0.

    @Override
    public void setTintList(ColorStateList tint) {
        final ChildDrawable[] array = mLayerState.mChildren;
        final int N = mLayerState.mNum;
        for (int i = 0; i < N; i++) {
            final Drawable dr = array[i].mDrawable;
            if (dr != null) {
                dr.setTintList(tint);
            }
        }
    }

  RippleDrawable doesn't help by not overriding setTintList for ripple specific behavior instead just uses the LayerDrawable's version. This is why setting a ColorStateList on a RippleDrawable via DrawableCompat.setTintList() doesn't have any effect.

   Fortunately, RippleDrawable has another API, setColor(ColorStateList) for the same purpose. This basically means, application developers have to check instance of the Drawable and invoke an appropriate API. This could be fixed in the support library version abstracting the internals.

    public static void setTintList(Drawable drawable, ColorStateList tint) {
        if (drawable instanceof DrawableWrapperLollipop) {
            // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible
            // functionality instead
            DrawableCompatBase.setTintList(drawable, tint);
        } else if ( drawable instanceof RippleDrawable) {
            ((RippleDrawable) drawable).setColor(tint);
        } else
            // Else, we'll use the framework API
            drawable.setTintList(tint);
        }
    }

In any case, all extensions of Drawable including RippleDrawable should support the base contract and override setTintList() for custom logic.

No comments: