Tintable Toolbar Things

21 Jun 2019

A few weeks ago, I merged a pull request that updates our app’s theme to Material Components from the Bridge version.

I have been referencing the MDC Catalog app a lot during this exercise; and I thought it would be a good idea to pattern how we organise and apply our theming and styling based on that app. (Hey they must know what they’re doing, right?)

We have two kinds of Toolbars in our app – the first one with our standard primary colour, and a “clean” variant.

When we initially set up our theming, we made this theme overlay (read about theme overlays here) to style our toolbar and give it that green arrow:

<style name="ThemeOverlay.Toolbar.Inverse" parent="ThemeOverlay.AppCompat.Light">
    <item name="colorControlNormal">?colorAccent</item>
    <item name="android:background">@color/white</item>
</style>

We then set this in our Toolbars as android:theme:

<androidx.appcompat.widget.Toolbar 
    ...
    android:theme="@style/ThemeOverlay.Toolbar.Inverse" />

The MDC Catalog app uses styles instead of themes, so I set about copying their technique. (Side note: Anita Singh has an amazing talk on styles, themes, and material design)

We immediately run into one problem: in our theme-based approach, we can set the colorControlNormal attribute in our theme overlay, but we cannot do that anymore with our style-based approach.

We definitely want our green arrow though, and we can look at the MDC source code to get some clues. It’s always a good idea to start off with what we want to be our parent style:

<style name="Widget.MaterialComponents.Toolbar.Surface">
    <item name="android:background">?attr/colorSurface</item>
    <item name="titleTextColor">?attr/colorOnSurfaceEmphasisHighType</item>
    <item name="subtitleTextColor">?attr/colorOnSurfaceEmphasisMedium</item>
    <!-- Note: this theme overlay will only work if the style is applied directly to a Toolbar. -->
    <item name="android:theme">@style/ThemeOverlay.MaterialComponents.Toolbar.Surface</item>
</style>

:thinking: Interesting note there, we should totally remember that.

Taking a peek at the referenced theme overlay, we see the attribute we want :tada: :

<style name="ThemeOverlay.MaterialComponents.Toolbar.Surface" parent="">
    <item name="colorControlNormal">?attr/colorOnSurfaceEmphasisMedium</item>
    <item name="actionMenuTextColor">?attr/colorOnSurfaceEmphasisMedium</item>
  </style>

Adapting this to our code, we now have:

<style name="Widget.Toolbar.Inverse" parent="Widget.MaterialComponents.Toolbar.Surface">
    <item name="android:background">@color/white</item>
    <item name="android:theme">@style/ThemeOverlay.ToolbarTint</item>
</style>

<style name="ThemeOverlay.ToolbarTint" parent="ThemeOverlay.MaterialComponents.Toolbar.Surface">
    <item name="colorControlNormal">?colorSecondary</item>
</style>

And applying this to our Toolbar (remember it is now a style !):

 <androidx.appcompat.widget.Toolbar
    ...
    style="@style/Widget.Toolbar.Inverse"
    app:navigationIcon="?homeAsUpIndicator" />

And this works really well, until….


The close button is now invisible :scream:

The only difference between this screen and the others is that we provide a vector drawable for the navigation icon:

<androidx.appcompat.widget.Toolbar
    ...
    style="@style/Widget.Toolbar.Inverse"
    app:navigationContentDescription="@string/close"
    app:navigationIcon="@drawable/ic_close" />

So it should work, right? MDC Catalog has a style with a close button like we have, the vector they use also has a fillColor hardcoded in it (I even tried making their fill colour red :joy:), but it is always always getting tinted correctly.

I always find theming and styling really hard to debug, so I gave up on this for a while and conceded my defeat. The next day, I decided to timebox myself to a couple of hours – if I can’t figure it out by then I will just stick to how it was before. I re-read all the blog posts, re-reviewed the source code, and gave StackOverflow another shot.

And there… in one random comment exchange:

What’s the difference between using ?attr/colorControlNormal at android:tint vs android:fillColor attributes? Thanks! – Thomas Vos Jun 20 ‘17 at 15:46 @SuperThomasLab The example is from Chris Banes’s blog where it illustrates replacing a tinted image with a tinted vector. Note that the fillColor was effectively hard-coded in the source image. I do not know if other color representations are now supported in AppCompat. – Joe Bowbeer Jun 22 ‘17 at 20:06

And sure enough, our vector didn’t have android:tint defined in it! :woman_facepalming: Adding it into our vector makes everything work perfectly :ok_hand:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24"
    android:tint="?colorControlNormal">
  <path
      android:fillColor="#FFFFFFFF"
      android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

I know it may be obvious to some people (of course you need a tint to tint something, duh???), but it wasn’t super obvious to me.

So, TL;DR: Make sure your vectors actually support tinting before trying to tint it. :rainbow: