Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android AppCompat 23.1.0 Tint Compound Drawable

I was using the method below to properly tint compound drawables with android.support.design 23.0.1 . Now that they released 23.1.0 it doesn't work anymore on api LVL16, all my drawables are black.

Anyone has a suggestion ?

  private void setCompoundColor(TextView view) {
    Drawable drawable = view.getCompoundDrawables()[0];
    Drawable wrap = DrawableCompat.wrap(drawable);
    DrawableCompat.setTint(wrap, ContextCompat.getColor(this, R.color.primaryLighter2));
    DrawableCompat.setTintMode(wrap, PorterDuff.Mode.SRC_IN);
    wrap = wrap.mutate();
    view.setCompoundDrawablesRelativeWithIntrinsicBounds(wrap, null, null, null);
  }

Thanks.

like image 939
Philippe David Avatar asked Oct 19 '15 18:10

Philippe David


2 Answers

I faced the same problem last week, and it turns out in the AppCompatTextView v23.1.0, compound drawables are automatically tinted.

Here is the solution I found, with more explications on why I did this below. Its not very clean but at least it enables you to tint your compound drawables !

SOLUTION

Put this code in a helper class or in your custom TextView/Button :

/**
 * The app compat text view automatically sets the compound drawable tints for a static array of drawables ids.
 * If the drawable id is not in the list, the lib apply a null tint, removing the custom tint set before.
 * There is no way to change this (private attributes/classes, only set in the constructor...)
 *
 * @param object the object on which to disable default tinting.
 */
public static void removeDefaultTinting(Object object) {
    try {
        // Get the text helper field.
        Field mTextHelperField = object.getClass().getSuperclass().getDeclaredField("mTextHelper");
        mTextHelperField.setAccessible(true);
        // Get the text helper object instance.
        final Object mTextHelper = mTextHelperField.get(object);
        if (mTextHelper != null) {
            // Apply tint to all private attributes. See AppCompat source code for usage of theses attributes.
            setObjectFieldToNull(mTextHelper, "mDrawableStartTint");
            setObjectFieldToNull(mTextHelper, "mDrawableEndTint");
            setObjectFieldToNull(mTextHelper, "mDrawableLeftTint");
            setObjectFieldToNull(mTextHelper, "mDrawableTopTint");
            setObjectFieldToNull(mTextHelper, "mDrawableRightTint");
            setObjectFieldToNull(mTextHelper, "mDrawableBottomTint");
        }
    } catch (NoSuchFieldException e) {
        // If it doesn't work, we can do nothing else. The icons will be white, we will see it.
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // If it doesn't work, we can do nothing else. The icons will be white, we will see it.
        e.printStackTrace();
    }
}

/**
 * Set the field of an object to null.
 *
 * @param object    the TextHelper object (class is not accessible...).
 * @param fieldName the name of the tint field.
 */
private static void setObjectFieldToNull(Object object, String fieldName) {
    try {
        Field tintField;
        // Try to get field from class or super class (depends on the implementation).
        try {
            tintField = object.getClass().getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            tintField = object.getClass().getSuperclass().getDeclaredField(fieldName);
        }
        tintField.setAccessible(true);
        tintField.set(object, null);

    } catch (NoSuchFieldException e) {
        // If it doesn't work, we can do nothing else. The icons will be white, we will see it.
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        // If it doesn't work, we can do nothing else. The icons will be white, we will see it.
        e.printStackTrace();
    }
}

Then you can call removeDefaultTinting(this); on each constructor of your class extending AppCompatTextView or AppCompatButton. For example :

public MyCustomTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    removeDefaultTinting(this);
}

With this, code working with v23.0.1 should work on v23.1.0.

I am not satisfied by the use of reflection to change attributes in the AppCompat lib, but this is the only way I found to use tinting on compound drawables with v23.1.0. Hopefully someone will find a better solution, or compound drawable tinting will be added to the AppCompat public methods.

UPDATE

I found another simpler solution : this bug occurs only if you set compound drawables using xml. Do not set them in xml, then set them in your code and it will work. The faulty code being in the constructor, setting drawables after it has been called is not affected.

EXPLICATIONS

In AppCompatTextView constructor, a text helper is initialized :

mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();

In the TextHelper loadFromAttributes function, a tint list is created for each compound drawable. As you can see, mDrawableXXXTint.mHasTintList is always set to true. mDrawableXXXTint.mTintList is the tint color that will be applied, and is only get from hardcoded values of AppCompat. For your custom drawables, it will always be null. So you end up with a tint having a null "tint list".

TypedArray a = context.obtainStyledAttributes(attrs, VIEW_ATTRS, defStyleAttr, 0);
    final int ap = a.getResourceId(0, -1);

    // Now read the compound drawable and grab any tints
    if (a.hasValue(1)) {
        mDrawableLeftTint = new TintInfo();
        mDrawableLeftTint.mHasTintList = true;
        mDrawableLeftTint.mTintList = tintManager.getTintList(a.getResourceId(1, 0));
    }
    if (a.hasValue(2)) {
        mDrawableTopTint = new TintInfo();
        mDrawableTopTint.mHasTintList = true;
        mDrawableTopTint.mTintList = tintManager.getTintList(a.getResourceId(2, 0));
    }

...

The problem is that this tint is applied in the constructor, and each time a drawable is set or changed :

 @Override
protected void drawableStateChanged() {
    super.drawableStateChanged();
    if (mBackgroundTintHelper != null) {
        mBackgroundTintHelper.applySupportBackgroundTint();
    }
    if (mTextHelper != null) {
        mTextHelper.applyCompoundDrawablesTints();
    }
}

So if you apply a tint to a compound drawable, and then call a super method such as view.setCompoundDrawablesRelativeWithIntrinsicBounds, the text helper will apply its null tint to your drawable, removing everything you've done...

Finally, here is the function applying the tint :

final void applyCompoundDrawableTint(Drawable drawable, TintInfo info) {
    if (drawable != null && info != null) {
        TintManager.tintDrawable(drawable, info, mView.getDrawableState());
    }
}

The TintInfo in parameters is the mDrawableXXXTint attribute of the texthelper class. As you can see, if it is null, no tint is applied. Setting all drawable tint attributes to null prevents AppCompat from applying its tint, and enables you to do wathever you want with the drawables.

I didn't find a clean way of blocking this behavior or getting it to apply the tint I want. All attributes are private, with no getters.

like image 158
David Lericolais Avatar answered Sep 28 '22 03:09

David Lericolais


You can try something like this

ContextCompat.getDrawable(context, R.drawable.cool_icon)?.apply {
    setTint(ContextCompat.getColor(context, R.color.red))
}
like image 30
0wl Avatar answered Sep 28 '22 03:09

0wl