Have you ever supported multiple themes in your application? You made a new theme, added new colors, made a new build and everything looked shiny – except your custom view? Learn how to prepare a custom view that won't fall apart.

The glue that keeps it all in one place

Your custom view should support theming, so that it does not break the theme once you change it. Let’s go over a simple example.

Let’s say you want to make a custom view that will have an icon, title, and a description. Here's how to create a layout for that custom view:

Layout for the custom view



<?xml version="1.0" encoding="utf-8"?> <merge tools:parentTag= "androidx.constraintlayout.widget.ConstraintLayout" ... > <ImageView android:id= "@+id/image" ... /> <TextView android:id= "@+id/title" ... /> <TextView android:id= "@+id/description" ... /> </merge>



The next step is to create a class for that custom view:

class AwesomeCustomView constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { init { View.inflate(context, R.layout.layout_awesome_custom_view, this) } }



You shouldn't set drawable for ImageView nor the text size, line height, and the color for your TextViews directly in the layout of the custom view.

Stylable

Let's make the custom view styleable, so that different themes can have different style values. To do that, add the following code in attrs.xml file:

<resources> <declare-styleable name="AwesomeCustomView"> <!-- todo add custom attributes --> </declare-styleable> </resources>



Now, add image and imageTintColor attributes, which will be used to set image drawable and tint color:

<resources> <declare-styleable name="AwesomeCustomView"> <attr name="awesomeImage" format="reference"/> <attr name="awesomeImageTintColor" format="color"/> </declare-styleable> </resources>



You can use those attributes when adding AwesomeCustomView in fragment or activity layout. For example, if you want to add it in fragment_example.xml :

<androidx.constraintlayout.widget.ConstraintLayout ... > <co.infinum.styles.AwesomeCustomView ... app:awesomeImage="@drawable/ic_info" app:awesomeImageTintColor="@color/green" /> </androidx.constraintlayout.widget.ConstraintLayout>

Note: When you use custom attribute, you should use app: prefix instead of android: prefix.

To support those attributes, you should get values for those attributes in AwesomeCustomView class. You can do that from attrs , using obtainStyledAttributes() function:

class AwesomeCustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { init { View.inflate(context, R.layout.layout_awesome_custom_view, this) attrs?.let { val typedArray = context.obtainStyledAttributes(it, R.styleable.AwesomeCustomView) //todo get value for attrs typedArray.recycle() } } }



Once you're finished with typedArray , you will need to recycle it.

To set tint, do the following:

val imageTintColorResource = typedArray.getResourceId(R.styleable.AwesomeCustomView_awesomeImageTintColor, android.R.color.white) val imageTintColor = ContextCompat.getColor(context, imageTintColorResource) ImageViewCompat.setImageTintList(image, ColorStateList.valueOf(imageTintColor))



Unless you specify tint color, it will fall back to white color. You can do a similar thing for awesomeImage .

Style attribute

Should you set the awesomeImageTintColor and awesomeImage attributes wherever you use AwesomeCustomView with the desired tint color? No .

There is a better way to handle it. In the styles.xml , let's make a new style :

<resources> <style name="Widget.AppTheme.AwesomeCustomView" parent=""> <item name="awesomeImageTintColor">@color/green</item> </style> </resources>

Note: When you use a custom attribute in style, don't use the android or app prefix.

For example, instead of having android:awesomeImageTintColor you should just have awesomeImageTintColor .

Apply it in activity_acv_example.xml :

<androidx.constraintlayout.widget.ConstraintLayout ... > <co.infinum.styles.AwesomeCustomView ... style="@style/Widget.AppTheme.AwesomeCustomView" /> </androidx.constraintlayout.widget.ConstraintLayout>



Custom view with applied style

At this point, you should only use one line in all the places where you want this style for your view. If you want to have a different style, you can do it with ease:

<resources> <style name="Widget.AppTheme.AwesomeCustomView.Warning" parent=""> <item name="awesomeImageTintColor">@color/sunset</item> </style> </resources>



To use this in layout, just change one line:

<androidx.constraintlayout.widget.ConstraintLayout ... > <co.infinum.styles.AwesomeCustomView ... style="@style/Widget.AppTheme.AwesomeCustomView.Warning" /> </androidx.constraintlayout.widget.ConstraintLayout>



Custom view when Warning Theme is applied

How did you end up with the name Widget.AppTheme.AwesomeCustomView ?

Let's break it down to parts:

Widget — describes that there is a defined style for a view

— describes that there is a defined style for a view AppTheme — describes that this style is for the base AppTheme base theme

— describes that this style is for the base AppTheme base theme AwesomeCustomView — describes which view the style is for

Instead of having lines of code for each custom attribute, now you can just use the style attribute.

TextAppearance attributes

You should do a similar thing for our TextViews . Instead of making attributes for text size, text line height, text color, font, and other characteristics for each TextView , you should make a custom textAppearance attr.

What is the textAppearance attr? It is the same as style attribute but for the text. Instead of supporting all the attributes, as style does, textAppearance supports only some attributes, like textColor , textSize , typeface , fontFamily etc. However, it does not support ellipsize or background for example.

In the attr.xml file, you should add the new attribute:

<resources> <declare-styleable name="AwesomeCustomView"> ... <attr name="awesomeTitleTextAppearance" format="reference"/> </declare-styleable> </resources>



To support it, you will need to make the following changes in the AwesomeCustomView class:

val titleStyleRes = typedArray.getResourceId(R.styleable.AwesomeCustomView_awesomeTitleTextAppearance, R.style.TextAppearance_AppCompat_Title) TextViewCompat.setTextAppearance(title, titleStyleRes)

If you don’t specify text appearance, it will fallback to AppCompat’s Title TextAppearance.

Now, you can get down to adding the special TextAppearance style for the title in the styles.xml file.

Let’s call that style Header :

<style name="TextAppearance.AppTheme.Header" parent="android:TextAppearance"> <item name="android:lineSpacingMultiplier">0</item> <item name="android:textSize">32sp</item> <item name="android:lineSpacingExtra">40sp</item> <item name="android:textStyle">bold</item> </style>



Let’s explain the naming for TextAppearance.AppTheme.Header :

TextAppearance — describes that we have a style for textAppearance attr

— describes that we have a style for textAppearance attr AppTheme — describes that this style is for our AppTheme base theme

— describes that this style is for our AppTheme base theme Header — describes our text style

You'll do a similar code for description. To use them in the Widget.AppTheme.AwesomeCustomView style, add the following:

<style name="Widget.AppTheme.AwesomeCustomView" parent=""> ... <item name="awesomeTitleTextAppearance">@style/TextAppearance.AppTheme.Header</item> <item name="awesomeDescriptionTextAppearance">@style/TextAppearance.AppTheme.Body</item> </style>

Theme

Let’s say you want to use the same style for your AwesomeCustomView everywhere where the base AppTheme is used. In that case, you should declare AppTheme styleable and add a special attribute in attrs.xml :

<declare-styleable name="AppTheme"> <attr name="awesomeCustomViewStyle" format="reference"/> </declare-styleable>



Next, you should set value for awesomeCustomViewStyle in your AppTheme :

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="awesomeCustomViewStyle">@style/Widget.AppTheme.AwesomeCustomView</item> </style>



Now, you should end up with different parameters for obtainStyledAttributes function:

val typedArray = context.obtainStyledAttributes( it, R.styleable.AwesomeCustomView, R.attr.awesomeCustomViewStyle, R.style.Widget_AppTheme_AwesomeCustomView )

During the initialization of the AwesomeCustomView , Android will get the style of your view from R.attr.awesomeCustomViewStyle . If you use another theme, in which there is no set value for awesomeCustomViewStyle attribute, it will fall back to R.style.Widget_AppTheme_AwesomeCustomView .

That means you don’t have to use this line anymore:

style="@style/Widget.AppTheme.AwesomeCustomView"



Having done all that, you should now support themes for your custom view. If you want to use it in another theme, you can easily do it:

<style name="AnotherTheme" parent="AppTheme"> <item name="awesomeCustomViewStyle">@style/Widget.AnotherTheme.AwesomeCustomView</item> </style>

Dark theme

Once you have a default custom view style per theme, it is really easy to support dark theme for your custom view. Your AppTheme should extend Theme.AppCompat.DayNight in res/values/themes.xml :

<style name="AppTheme" parent="Theme.AppCompat.DayNight"> <item name="awesomeCustomViewStyle">@style/Widget.AppTheme.AwesomeCustomView</item> </style>



And in res/values-night/themes.xml , just define the dark resources:

<style name="AppTheme" parent="Theme.AppCompat.DayNight"> <item name="awesomeCustomViewStyle">@style/Widget.AppTheme.Dark.AwesomeCustomView</item> </style>



Custom view when dark theme is applied

The checklist

To conclude, if you want to avoid problems with custom views when you theme your app, your custom views should support custom style attributes for each child view style, and custom textAppearance attributes for each child TextView.

Furthermore, it should have defined styles for each different usage in each theme, as well as a default style for each theme.

Designer Dubravko Tuksar is responsible for painting the screen red.