Tags
actionbar, android, application, custom attributes, custom-ui, dark themes, duplicateParentState, inflate, light themes, menu, mozilla, myth, optimization, performance, pocket, read it later, runtime theme change, theme change, themes, ui xml snapshot, window
It’s a known fact in Android that themes cannot be changed during runtime. If the new theme change has to be reflected in the application, all the views have to be re-inflated, as the theme based values are parsed only once during inflation. However, the beautiful Pocket for Android app switches between black and white themes in a jiffy! How did they achieve something that has been declared by the Android community as impossible? The ActionBar
background, the text colors, the image colors, and the Menu
background have all changed. Since it’s not a single line change, let’s chop it into pieces.
There are clearly two things that make it feel like the theme has changed from Holo Dark to a Light version — the ActionBar and the Menu. The good thing with menu is that it uses a ListView
to show the items. Hence it gets re-created every time it is shown. Now a theme change during runtime will change the menu items for sure. A small theme change code will be like:
<!-- somewhere in themes.xml --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> <!-- light theme --> <style name="AppTheme" parent="@android:style/Theme.Holo.Light"/> <!-- dark theme --> <style name="AppTheme.Dark" parent="@android:style/Theme.Holo"/> </resources>
And we could change the theme in Java as:
// somewhere in the activity private void changeTheme(boolean isLight) { setTheme(isLight ? R.style.AppTheme : R.style.AppTheme_Dark); }
That solves the menu problem! How about ActionBar? This is where Pocket has a very clever approach. The ActionBar is clearly not a custom UI. Hence, a call like getActionBar().getCustomView()
will return null
. If we had such a view, we could easily change the background color, couldn’t we? In this case, ActionBar is fully owned by Android and we don’t have any control over it’s appearance, except that is defined in themes.xml. Also, we cannot re-inflate the ActionBar too — unless we plan to show a custom view.
Let’s take a step back. Where is the actual app content placed on phone? Based on the UI XML Snapshot (that’s so cool!), a typical app will have views like:
<!-- A bunch of FrameLayout as ancestors for core android ...and then --> <!-- Window of the application --> <FrameLayout> <!-- Content of the application --> <RelativeLayout> <!-- ActionBar owned by Android --> <LinearLayout /> <!-- Actual content of the application defined in our layout.xml --> <LinearLayout or RelativeLayout or FrameLayout /> </RelativeLayout> </FrameLayout>
And we can get the Window
of the application — which is a FrameLayout — and can set a background! So, if the views contained by it are translucent, the Window’s background will be seen. Choosing a mid-grey for the translucent layer and changing the background to be black or white could give magical effects.
Did we just have two color themes by just changing the Window background? Changing the window background color is pretty simple.
// somewhere in the activity private void changeTheme(boolean isLight) { getWindow().setBackgroundDrawable(new ColorDrawable(isLight ? Color.WHITE : Color.BLACK)); }
And that’s the magic behind a two-color ActionBar in Pocket. The icons are all translucent, making them feel like they change based on the Holo theme used. The text part is pretty easy. Inflating the views again is unnecessary. Adding a custom state can switch themes in a jiffy. Firefox for Android uses this technique to swiftly change color scheme between normal and private browsing modes. One good trick here is to make the children duplicate their parent state.
<!-- some layout file for the activity --> <LinearLayout ...attributes...> <Button ...attributes... android:duplicateParentState="true"/> <TextView ...attributes... android:duplicateParentState="true"/> </LinearLayout>
In the above layout, changing the custom state, say “state_light_theme_enabled”, of LinearLayout
will propagate down to its children too. So a single call on the root level element can do wonders. And thereby, we can change themes — or make it feel like — within 250ms. This technique is not restricted for monochrome. This works with normal colors too.
Thank you for the article 😉
I still think it is a BAD idea to do things like that (you makes a slower UI without real advantages for the user.
Depends on the nature of the application. Pocket has a good use case to support light vs dark themes.
Wonder analysis .. thanks for writing this …
Great article 🙂 But have an issue with pressed states. Since duplicateParentState is set to true for all elements of the layout, the pressed state is not available to a button or any other layout when I press it. Any suggestions?
What would you want to set it for all elements? Only those that require parent’s state should use it.
Yes. I am working on a GPS Navigation application which requires changing day night theme with the changes in light sensor value. The day theme will have black text color and night will have white text color. Hence I want to set duplicateParentState to all elements which have text.
It’s better to reload the app in that case.
Thank you. But reloading was not a better option in our case as we use opengl to render graphics and it takes some time to load.
Sir, please can you give an example of using this with normal colors ,say i want to switch between red,blue and green.