Android AppCompat 23.1.0 Tint Compound Drawable
Asked Answered
P

2

8

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.

Photomicroscope answered 19/10, 2015 at 18:18 Comment(2)
check this answer for update.Populace
The code from Philippe David works, but from my experience you should write wrap = wrap.mutate(); before DrawableCompat.setTint(). Otherwise it will not work properly as the original drawable will be modified.Dispersant
C
9

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.

Corenda answered 20/10, 2015 at 10:47 Comment(8)
Wow. Not the kind of answer I thought I would get ! Will try your solution, but this kind of work around is a shame to Android ... do you think it would be wise to open a bug for google to check out ? Thanks a lot :)Photomicroscope
You're welcome ! I spent half a day using the debugger to understand why my drawables were white, so when I saw your question I felt obliged to create an account and post what I found :) It's probably a good idea to open a bug, I just didn't take the time. This is a temporary solution that will probably stop working next time they modify AppCompat. The code they use to tint the compound drawables is not too long or complicated, its just completely innaccessible from an external class. A simple setter for the tint of each compound drawables, and it's fixed...Corenda
Done for the issue. Since yesterday, I found out this lib also breaks TransitionDrawable on some device :) code.google.com/p/android/issues/detail?id=191111Corenda
mTextHelper does not exist for AppCompatAutoCompleteTextView ?Photomicroscope
Using the method of not putting them in xml works perfectly !Photomicroscope
Glad it worked ! mTextHelper exists in AppCompatAutoCompleteTextView so it should work if your class directly extends it, unless you use TransitionDrawables as compound drawables. In this case, not putting them in xml seems to be the only solution !Corenda
hmm, what is TintManager? and what exactly is TintInfo, I don't understand your description of it and they don't existBaxter
This was a hack to make AppCompat 23.1.0 work on some specific cases. Its no longer necessary with the last version (23.4.0). "TintManager" and "TintInfo" are objects used internally by the AppCompat library, they are not visible outside of the library.Corenda
C
1

You can try something like this

ContextCompat.getDrawable(context, R.drawable.cool_icon)?.apply {
    setTint(ContextCompat.getColor(context, R.color.red))
}
Cletacleti answered 1/11, 2018 at 19:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.