What is best practice to organise styling of buttons of a professional android application? Assume a larger contemporary application (SDK 26+, min SDK 21).
This question is answerable, as both the sources of Material Design
and the setup of Android Studio
give enough clues and examples of the patterns of the intended professional usage. Surely the user is not limited to this patterns, but following them, makes the application play well together with the sources of Material Design
and provides best maintainability.
I find several ingredients related to the styling of buttons.
@style/Widget.AppCompat.Button
@style/Widget.AppCompat.Button.Colored
@style/TextAppearance.AppCompat.Button
@style/TextAppearance.AppCompat.Widget.Button
@color/foreground_material_dark
?colorAccent
?textColorPrimary
?android:colorForeground
?textAppearanceButton
There may be more.
You can look up the sources. However, even knowing all details does not give the full picture of the intended usage. This question is asking to draw the picture.
(Min SDK 21)
I think it an enough fine-grained approach to separate the text appearance from the backgrounds. This gives the option to combine different backgrounds with different text appearances. It also matches the two style settings provided by Button
and the organisation of Material Design. Hence it addresses the question, how it is intended to be used.
The price is, that each Button
needs both settings:
android:textAppearance
style
To even lower this price styles_material.xml
in fact takes an advanced approach. Each button style
already includes a default text appearance. So in the normal case I only have to apply the button style
.
<Button
style="?defaultButtonStyle"
I follow this pattern for my own button styling, as the question is for the intended usage. If I want to modify the default, I add an alternative text appearance by setting it to android:textAppearance
.
<Button
style="?defaultButtonStyle"
android:textAppearance="?smallButtonTextAppearance"
For very special buttons I still can adjust the styling on the level of the layout file. This is the lowest level of granularity.
Hint: Be aware that
android:textAppearance
has a very low precedence. If you set a text attribute somewhere in the theme (or style), you will overwrite the same attribute in all ofandroid:textAppearance
. It works with a similar force like the "!important" annotation in CSS, which can be a pretty pitfall.
If I don't plan to use different themes, I can set the styles directly into the layouts.
<Button
style="@style/My.DefaultButtonStyle"
android:textAppearance="@style/My.SmallButtonTextAppearance"
...
If a want to be able to exchange themes, I map all types of styles to attributes first. Then I set the styles indirectly by using the attributes. This gives me the option to connect other styles for other themes, without the need to duplicate layouts.
<Button
style="?defaultButtonStyle"
android:textAppearance="?smallButtonTextAppearance"
...
I personally prefer not to use or mix given attributes, but to fully define my own set of attributes addressing my design. So the levels of the onion stay cleanly separated.
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<!-- button text appearance -->
<attr name="defaultButtonTextAppearance" format="reference" />
<attr name="smallButtonTextAppearance" format="reference" />
<!-- button backgrounds -->
<attr name="defaultButtonStyle" format="reference" />
<attr name="alarmButtonStyle" format="reference" />
In the themes the attributes are mapped to theme specific styles.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="OtherTheme" parent="Theme.AppCompat">
<!-- button text appearance -->
<item name="defaultButtonTextAppearance">@style/OtherTheme.DefaultButtonTextAppearance</item>
...
<!-- button backgrounds -->
<item name="defaultButtonStyle">@style/OtherTheme.DefaultButtonStyle</item>
...
If I track down the styles to the sources I come to a file data/res/values/styles_material.xml
, which defines the root of all button text appearances. TextAppearance.Material.Button
inherits from TextAppearance.Material
, but the four relevant attributes for buttons are overwritten.
<style name="TextAppearance.Material">
<item name="textColor">?attr/textColorPrimary</item>
<item name="textColorHint">?attr/textColorHint</item>
<item name="textColorHighlight">?attr/textColorHighlight</item>
<item name="textColorLink">?attr/textColorLink</item>
<item name="textSize">@dimen/text_size_body_1_material</item>
<item name="fontFamily">@string/font_family_body_1_material</item>
<item name="lineSpacingMultiplier">@dimen/text_line_spacing_multiplier_material</item>
</style>
<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>
It can be overwritten by my own inherited styles. It also shows, that it would be easy to write my own text appearance style without using inheritance at all.
Understanding Androids text color management system is them most confusing part, because the system is quite powerful. Here comes some enlightenment.
In the above TextAppearance.Material.Button
I find text color is specified by the attribute ?textColorPrimary
. This attribute again is based on the attribute ?android:colorForeground
.
The attribute ?android:colorForeground
is the central switch to set the text colors. Be default all text colors are calculated based on this setting, also those of the buttons. For example different grades of greyed-out or opaque variants are calculated for disabled buttons, for body text, etc.
Instead of touching dozens of different places it is a good idea to set the common default text color here and rely on the default Android color calculating system as far as useful. Tweak it in details.
<item name="android:colorForeground">@color/orange_700</item>
This setting defaults to @color/foreground_material_dark
.
Hint 1: If you edit the setting by use of the
Android Studio Theme Editor
, it will possibly change the value of@color/foreground_material_dark
. To me it does not feel like a good idea to change a value of material dark because it is not my realm. Better use a reference like shown before.Hint 2: The
Theme Editor
is an appropriate tool to discover the relations of the color attributes system. This relations reveal, when you experimentally try to edit the different attributes with the editor.
If I want a button text color that varies from the overall text color, I set it on the level of the text appearance style.
Hint 3: Using
?android:colorForeground
does not work out of the
box below API 26. For a workaround see here.
The text size is the factor of the text appearance, that I typically want to directly adjust to my own design within my customised text appearance styles.
<style name="My.SmallButtonTextAppearance" parent="My.DefaultButtonTextAppearance">
<item name="android:textSize">16sp</item>
</style>
TextAppearance.Material.Button
takes the text size default from the resource @dimen/text_size_button_material
. There is no system with a central text size setting comparable to the text color setting system.
The root style TextAppearance.Material.Button
set's all caps to true
. There is not even a resource, the value is taken from. It's just hard coded.
<item name="textAllCaps">true</item>
There is a high chance, I want to set it to false
in my customised button styles.
<item name="android:textAllCaps">false</item>
As with the text colors the font family typically is a system with a common central nature. How is it managed for the buttons? The root style TextAppearance.Material.Button
makes use a the string resource @string/font_family_button_material
.
<item name="fontFamily">@string/font_family_button_material</item>
In the file data/res/values/donttranslate_material.xml
this is set to sans-serif-medium
, while in the file data/res/values-watch/donttranslate_material.xml
is is set to sans-serif-condensed
.
<string name="font_family_button_material">sans-serif-medium</string>
<string name="font_family_button_material">sans-serif-condensed</string>
This sans-serif
settings are mapped to my chosen font family within the fonts setup. Typically sans-serif
is fine for button text. For further customisation of the fonts I point to this question.
Apart from using a color for the background, a xml resource file can be applied to specify the background with fancy corners, color gradients or other graphical effects, also supporting different backgrounds for different states of the button.
This part is strongly influenced by my design. I will typically use my own background.
On the other hand there is a rich system of predefined button backgrounds resource files in material design. I would like to give a short overview here, but that's beyond my skills and seems so large to be worth a topic of it's own.
The style
for the background should not contain settings for width
, height
or margins
, as this belongs into the surrounding layout. On the other hand the padding
belongs into the background style
.
In the file data/res/values/styles_material.xml
I find nine button styles I may inherit from. If I write my very own, a should not forget to set a default text appearance.
The root element is Widget.Material.Button
. It set's the default text appearance to ?textAppearanceButton
. Hence, setting this attribute is an option to directly use the material design button styles without inheritance and yet have your customised default text appearance.
<!-- 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>
The attribute ?colorAccent
is used, to set the color of Widget.AppCompat.Button.Colored
. See the Android Studio Theme Editor
. See @drawable/btn_colored_material
.
Note that the default text appearance of Widget.AppCompat.Button.Colored
varies and is not set by a customisable attribute.
<!-- Colored bordered ink button -->
<style name="Widget.Material.Button.Colored">
<item name="background">@drawable/btn_colored_material</item>
<item name="textAppearance">@style/TextAppearance.Material.Widget.Button.Colored</item>
</style>
<!-- Small bordered ink button -->
<style name="Widget.Material.Button.Small">
<item name="minHeight">48dip</item>
<item name="minWidth">48dip</item>
</style>
<!-- Borderless ink button -->
<style name="Widget.Material.Button.Borderless">
<item name="background">@drawable/btn_borderless_material</item>
<item name="stateListAnimator">@null</item>
</style>
Note that the default text appearance of Widget.Material.Button.Borderless.Colored
varies and is not set by a customisable attribute.
<!-- Colored borderless ink button -->
<style name="Widget.Material.Button.Borderless.Colored">
<item name="textAppearance">@style/TextAppearance.Material.Widget.Button.Borderless.Colored</item>
</style>
Note that Widget.Material.Button.ButtonBar.AlertDialog
inherits from Widget.Material.Button.Borderless.Colored
. Same limitations of the default text appearance apply.
<!-- Alert dialog button bar button -->
<style name="Widget.Material.Button.ButtonBar.AlertDialog" parent="Widget.Material.Button.Borderless.Colored">
<item name="minWidth">64dp</item>
<item name="minHeight">@dimen/alert_dialog_button_bar_height</item>
</style>
<!-- Small borderless ink button -->
<style name="Widget.Material.Button.Borderless.Small">
<item name="minHeight">48dip</item>
<item name="minWidth">48dip</item>
</style>
<style name="Widget.Material.Button.Inset">
<item name="background">@drawable/button_inset</item>
</style>
<style name="Widget.Material.Button.Toggle">
<item name="background">@drawable/btn_toggle_material</item>
<item name="textOn">@string/capital_on</item>
<item name="textOff">@string/capital_off</item>
</style>
Personally I would either use one of this predefined button styles or inherit my own from Widget.Material.Button
. This keeps the hierarchy of inheritance low and the code easily readable. It saves me at most three lines of code if I inherit from another style, while the code becomes less maintainable.
There are exceptions to this rule of thumb. For example @drawable/btn_borderless_material
is private. So I either have to inherit from Widget.Material.Button.Colored
or create a copy of the file.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With