Skip to content

Instantly share code, notes, and snippets.

@shekibobo
Last active March 2, 2020 11:04
Show Gist options
  • Star 79 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • Save shekibobo/46e2f0b7b7e83f60b500 to your computer and use it in GitHub Desktop.
Save shekibobo/46e2f0b7b7e83f60b500 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>
@mx1up
Copy link

mx1up commented Sep 15, 2015

thanks A LOT!!

@chrisjenx
Copy link

Does this alleviate the disabled button bug on 5.0/5.1?

@CrandellWS
Copy link

Great job on putting this together, I appreciate it alot.

@suclike
Copy link

suclike commented Dec 14, 2015

Awesome post but I the thing is that it isn't correct to use implicit and explicit style imports together.

name="AppTheme.Widget.Button.Capsule" && parent="AppTheme.Widget.Button"

It wont work that way) Simply remove style parent and it is set.

Copy link

ghost commented Dec 15, 2015

Can you provide a default colors, dimens for missing

Copy link

ghost commented Dec 15, 2015

my buttons are a light white color, it isn't until I tap the button where I see my color I chose for the button background.

@vkatdare
Copy link

Thank you. Great job with this.

@balachandarlinks
Copy link

balachandarlinks commented Jun 30, 2016

Helpful post. Just a suggestion. We can add a foreground item ( ?selectableItemBackground ) to the button style to get the ripple effect on lollipop and above. So that there won't be any need to create drawables with ripple.

@kenyee
Copy link

kenyee commented Aug 18, 2016

For some reason statelists change the size of a button too. Just FYI...

@fredgrott
Copy link

dims is missing

@farukcankaya
Copy link

Thanks for sharing, I've faced with same issue with styling. I've learned that this problem is fixed in Support Library rev.22 (Fri March 13, 2015)..

We can use material design for pre-lolipop devices with using AppCompat classes from support library. Support library 22.1 supports following views:

  • AppCompatAutoCompleteTextView
  • AppCompatButton
  • AppCompatCheckBox
  • AppCompatCheckedTextView
  • AppCompatEditText
  • AppCompatMultiAutoCompleteTextView
  • AppCompatRadioButton
  • AppCompatRatingBar
  • AppCompatSpinner
  • AppCompatTextView

and we can use AppCompat styles with these views. Here is the my answer in so: http://stackoverflow.com/a/42413396/3963197

@aweck
Copy link

aweck commented Dec 6, 2017

This is super useful, thank you!

@EngrAhsan
Copy link

how we can set background color of button on click listenr...i knw it can be done by using touchListener but how we get back every them color in button release ?
any one knw plzz help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment