Introduction
AppCompat is an Android support library to provide backwards-compatible functionality for Material design patterns. It currently comes bundled with a set of styles in the Theme.AppCompat
and Widget.AppCompat
namespaces. However, there is a critical component missing which I would have thought essential to provide the a default from which we could inherit our styles: Widget.AppCompat.Button
. Sure, there's Widget.AppCompat.Light.ActionButton
, but that doesn't actually inherit from Widget.ActionButton
, which does not inherit from Widget.Button
, so we might get some unexpected behavior using that as our base button style, mainly because Widget.ActionButton
strictly belongs in the ActionBar
.
So, if we want to have a decently normal default button style related to AppCompat, we need to make it ourselves. Let's start by digging into the Android SDK to see how it's doing default styles.
Digging In
From res/values/styles_material.xml
provided in the android-sdk/platforms/android-21
directory of the Android SDK, we can find Widget.Material.Button
:
<!-- Bordered ink button -->
<style name="Widget.Material.Button">
<item name="background">@drawable/btn_default_material</item>
<item name="textAppearance">?attr/textAppearanceButton</item>
<item name="minHeight">48dip</item>
<item name="minWidth">88dip</item>
<item name="stateListAnimator">@anim/button_state_list_anim_material</item>
<item name="focusable">true</item>
<item name="clickable">true</item>
<item name="gravity">center_vertical|center_horizontal</item>
</style>
We can keep most of the defaults, but remove stateListAnimator
since that's not available below Lollipop. We also need to provide our own default values for background
and textAppearance
for our theming purposes, since neither of those values will work if we just steal them. Let's make a couple styles based on this:
Our Styles
<?xml version="1.0" encoding="utf-8"?>
<!-- values/styles.xml -->
<resources>
<style name="AppTheme.Widget" />
<style name="AppTheme.Widget.Button">
<item name="android:background">@drawable/button_default</item>
<item name="android:textColor">@color/button_text_default</item>
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Button</item>
<item name="android:minHeight">48dip</item>
<item name="android:minWidth">88dip</item>
<item name="android:focusable">true</item>
<item name="android:clickable">true</item>
<item name="android:gravity">center_vertical|center_horizontal</item>
</style>
<style name="AppTheme.Widget.Button.Capsule" parent="AppTheme.Widget.Button">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">@dimen/button_capsule_default_height</item>
<item name="android:background">@drawable/button_capsule_default</item>
</style>
</resources>
As you can see, we created a couple new styles, one for a default rectangular button and one for a capsule-shaped button. We provide our own drawables for the background, and we inherit from the TextAppearance.AppCompat.Button
for our textAppearance. Let's take a look at that theme just to see what it gives us by default.
In styles_material
:
<style name="TextAppearance.Material.Button">
<item name="textSize">@dimen/text_size_button_material</item>
<item name="fontFamily">@string/font_family_button_material</item>
<item name="textAllCaps">true</item>
<item name="textColor">?attr/textColorPrimary</item>
</style>
and appcompat
:
<style name="Base.TextAppearance.AppCompat.Button">
<item name="android:textSize">@dimen/abc_text_size_button_material</item>
<item name="textAllCaps">true</item>
<item name="android:textColor">?android:textColorPrimary</item>
</style>
Depending on your needs you may want to override these styles in your own theme, but they'll do for our purposes right now.
Note: If overriding TextAppearance.AppCompat.Button
, my experience shows that android:textColor
should be changed in the button theme, not in the TextAppearance
style.
Strategy
So we want a unified theme for buttons - pressed, disabled, and enabled should be consistent for our default buttons. But we also want to take advantage of the new ripple
effect in Lollipop. When I started retheming, I had a number of different drawable state lists and color state lists and shape state lists. It was getting messy. I figured there had to be a better way. So through lots of experimentation and research trying to grok how to efficiently build button themes, I came up with the following pattern:
- Provide a
@drawable/button_default
in bothdrawable
anddrawable-v21
. - Provide a single
@drawable/button_default_shape
indrawable
that we can share for our button drawables in both versions. - Provide a single
@color/default_button_background
incolor
that we can share for our shapesolid
color. - Provide a single
@color/button_text_default
incolor
that we can share for the default button styles.
There's a problem with #2 and #3, though. Android's older XML parsers can't apply a ColorStateList
as a Solid
's drawable or color attribute (see Stack Overflow). Since I wrote it already, I'm keeping that section to show how it could work.
To support older versions, we'll simply expand #2 a bit, and change #3:
2.1. Provide a single @drawable/button_default_shape_selector
in drawable
that we can share for our button drawables in both versions. This will define the drawable we will use for each state of the button.
2.2. Provide one @drawable/button_default_shape_<state>
for each supported button state. Each of these will use a different color for it's solid
element. (Technically you could combine these into the shape selector file in 2.1, but this way you can preview what each state will look like in Android Studio.)
3.0. Create a @color/default_button_background_<state>
color instance for each state in values/colors.xml
.
Button Background Drawable
Since we want to take advantage of the ripple effect on Lollipop, we'll need to provide two drawables of the same name in version-qualified drawable directories.
For the default version (below 21), we'll just make a shape that uses our custom color state list for its solid color:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/button_default_background"/>
</shape>
Just a note, I tried just making it a <selector>
with the color as its <item>
, but it turns out the item must use android:drawable
, which must come from the drawables
directory. Thus, we make a basic rectangular shape.
For the lollipop version, we simply wrap the <shape>
element inside of a <ripple>
element:
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_default_background"/>
</shape>
</item>
</ripple>
Button Background Shape
You'll notice that we're using the same shape for both drawables, so we can extract that into its own drawable @drawable/button_default_shape
:
<!-- drawable/button_default_shape.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/button_default_background"/>
</shape>
<!-- drawable/button_default.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_default_shape"/>
</selector>
<!-- drawable-v21/button_default.xml -->
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item android:drawable="@drawable/button_default_shape"/>
</ripple>
Button Background Colors
And of course, we need a @color/button_default_background
state list:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_pressed="true"
android:color="@color/branding_color_primary_dark"/>
<item
android:state_enabled="false"
android:alpha="@dimen/disabled_alpha_default"
android:color="@color/branding_color_primary_dark"/>
<item
android:state_enabled="true"
android:color="@color/branding_color_primary"/>
</selector>
I'll leave the @color/button_text_default
as an exercize for the reader, but that's pretty much it! We now have a standard button style we can use throughout the app! To make that quick and easy, put it in your theme:
Applying the Style
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:buttonStyle">@style/AppTheme.Widget.Button</item>
</style>
</resources>
And Beyond
To add a capsule button, you can follow the exact same steps as above, but change your button_default_shape
to have a <corners>
element defining whatever works for your theme. To match the previous theme, just use the @color/button_default_background
that we created for our rectangular buttons. For different colored buttons, you'll still need to create a set of drawables, shapes, and color lists, but hopefully following this pattern, you'll be able to keep everything nicely organized, and follow a recognizable pattern throughout your codebase.