Theming: Default Styles on Android
April 25, 2020
Yesterday, I came across this video by @ataul where he describes setting up default styles on Android and uses a custom view as an example.
I thought the video was great and I decided to create an example of using default styles of my own.
Why Default Styles?
Setting up a default style is useful when you need a widget (custom view, android view) styled consistently within your application. This way, you can define a view like so:
<com.example.defaultstyles.CustomCircleView android:layout_width="wrap_content" android:layout_height="wrap_content" />
Instead of referencing some style:
<com.example.defaultstyles.CustomCircleView style="@style/Widget.DefaultStyles.CustomCircleView" android:layout_width="wrap_content" android:layout_height="wrap_content" />
Not only does it reduce boilerplate code but also helps ensure that your views are consistent throughout the app. You don’t need to remember to apply a default style every time you use a widget in your layout.
To accomplish this, we need a way to associate our view with its default style. In the example above, we need to associate our view com.example.defaultstyles.CustomCircleView
with style Widget.DefaultStyles.CustomCircleView
.
Adding support for default styles
Let’s build a view that draws a gray circle without a border by default but make it configurable so the caller can add a border, if desired .
The custom view above is rendered 3 times. The second time it’s rendered (the middle circle) it adds a border by using app:hasBorder="true"
Layout
Here is the layout xml for the above:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <com.example.defaultstyles.CustomCircleView android:id="@+id/circle1" android:layout_width="@dimen/circle_size" android:layout_height="@dimen/circle_size" android:layout_margin="10dp" /> <com.example.defaultstyles.CustomCircleView android:id="@+id/circle2" android:layout_width="@dimen/circle_size" android:layout_height="@dimen/circle_size" android:layout_margin="10dp" app:hasBorder="true" /> <com.example.defaultstyles.CustomCircleView android:id="@+id/circle3" android:layout_width="@dimen/circle_size" android:layout_height="@dimen/circle_size" android:layout_margin="10dp" /> </LinearLayout>
Custom view
Here is the code for the custom view:
package com.example.defaultstyles import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.util.AttributeSet import android.view.View private const val DEFAULT_STYLE_ATTR = R.attr.circleViewStyle private const val DEFAULT_STYLE_RES = R.style.Widget_DefaultStyles_CustomCircleView private const val DEFAULT_HAS_BORDER = false class CustomCircleView : View { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, DEFAULT_STYLE_ATTR) constructor( context: Context, attrs: AttributeSet?, defStyleAttr: Int ) : this(context, attrs, defStyleAttr, DEFAULT_STYLE_ATTR) constructor( context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) { val ta = context.obtainStyledAttributes( attrs, R.styleable.CustomCircleView, DEFAULT_STYLE_ATTR, DEFAULT_STYLE_RES ) hasBorder = ta.getBoolean(R.styleable.CustomCircleView_hasBorder, DEFAULT_HAS_BORDER) ta.recycle() } private var hasBorder: Boolean = DEFAULT_HAS_BORDER private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.LTGRAY style = Paint.Style.FILL } private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.BLACK style = Paint.Style.STROKE // circle border width in pixels strokeWidth = 10.0f } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val size = width // draw the circle val radius = size / 2f canvas.drawCircle(size / 2f, size / 2f, radius, paint) if (hasBorder) { // draw the border around the circle canvas.drawCircle( size / 2f, size / 2f, radius - borderPaint.strokeWidth / 2f, borderPaint ) } } }
Styles
Here is our styles.xml
<resources> <!-- Base application theme. --> <style name="Theme.DefaultStyles" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="circleViewStyle">@style/Widget.DefaultStyles.CustomCircleView</item> </style> <style name="Widget.DefaultStyles.CustomCircleView" parent="android:Widget"> <item name="hasBorder">false</item> </style> </resources>
Custom attributes
This is the contents of the attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="DefaultStylesTheme"> <attr name="circleViewStyle" format="reference" /> </declare-styleable> <declare-styleable name="CustomCircleView"> <attr name="hasBorder" format="boolean" /> </declare-styleable> </resources>
Having circleViewStyle
defined and referenced by the theme allows us to easily have a default style for all circles in our app and every time we inflate this view in layout xml, we don’t need to reference the style explicitly.
Attribute resolution precedence
Here is how obtainStyledAttributes()
derives the value of our custom hasBorder attribute (top to bottom in the order of precedence)
Values passed in layout xml which will be available in
AttributeSet
.Style resource referenced in layout xml (for instance,
style=@style/foo
). These will also be available in theAttributeSet
.Default style attribute set by
defStyleAttr
.Default style resource referenced by
defStyleRes
ONLY IF there was nodefStyleAttr
specified. In the above custom view code, defStyleRes will not be used sincedefStyleAttr
was specified.Any matching values you define in the theme itself (personally, I don’t anticipate using this).
Full source code can be found here