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:
<?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:
<span data-es-language="c"></span>lass 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>
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:
<span data-es-language="c"></span>lass 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>
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>
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 viewAppTheme
— describes that this style is for the base AppTheme base themeAwesomeCustomView
— 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 attrAppTheme
— describes that this style is for our AppTheme base themeHeader
— 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>
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.