Skip to content

Instantly share code, notes, and snippets.

@jinqian
Forked from shekibobo/README.md
Created June 22, 2017 11:48
Show Gist options
  • Save jinqian/0a96691186e2063f58e5f0ed33f5fda5 to your computer and use it in GitHub Desktop.
Save jinqian/0a96691186e2063f58e5f0ed33f5fda5 to your computer and use it in GitHub Desktop.
Android: Base Styles for Button (not provided by AppCompat)

How to create custom button styles using Android's AppCompat-v7:21

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:

  1. Provide a @drawable/button_default in both drawable and drawable-v21.
  2. Provide a single @drawable/button_default_shape in drawable that we can share for our button drawables in both versions.
  3. Provide a single @color/default_button_background in color that we can share for our shape solidcolor.
  4. Provide a single @color/button_text_default in color 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.

<?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>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_capsule_default_shape"/>
</selector>
<?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>
<?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>
<?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>
<?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_capsule_default_shape"/>
</ripple>
<?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>
<?xml version="1.0" encoding="utf-8"?>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment