Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Developing Themes with Style

Developing Themes with Style

The Android Theming system is powerful but easy to misuse. Proper use of it can make themes and styles easier to maintain, make branding updates less scary and make it straightforward to support dark modes.

This talk will start with a crash course in themes and styles. We’ll cover the platform, AppCompat and MaterialComponents functionality that you should know about and build upon. We’ll look at how to support both light and dark modes without going crazy. We’ll show how and when to create your own theme attributes, how to tint drawables, how to theme sub-sections of your screen and how to isolate your theme dependant resources to make them maintainable.

Nick Butcher

July 03, 2019
Tweet

Other Decks in Technology

Transcript

  1. themes Theme Attributes <style name="Plaid" parent="…"> <item name=" z colorPrimary

    ">@color/teal_500!"item> <item name="colorSecondary">@color/pink_200!"item> <item name="android:windowBackground">@color/background!"item> !"style>
  2. Widget.Button Widget.Button.Dark Widget.Button.Pro Widget.Button.Pro.Dark Widget.Switch Widget.Switch.Dark Widget.Switch.Pro Widget.Switch.Pro.Dark Widget.CheckBox Widget.CheckBox.Dark

    Widget.CheckBox.Pro Widget.CheckBox.Pro.Dark Widget.RadioButton Widget.RadioButton.Dark Widget.RadioButton.Pro Widget.RadioButton.Pro.Dark
  3. view attributes theme attributes Configuration specific to a single view

    Semantic variables: provided/varied by theme May be used widely
  4. styles themes Map<view attribute, value> Associated with & applied to

    single view Map<theme attribute, value> Associated with a Context Applied to a hierarchy Overlays ancestors
  5. Theme.AppCompat.Light - Theme.MaterialComponents.Light !% Theme.Owl.Pink themes <style> <item name="colorPrimary"… <item

    name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground"… !!$
  6. Theme.Material.Light - Theme.AppCompat.Light !% Theme.MaterialComponents.Light !!& Theme.Owl.Pink themes <style> <item

    name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground"… <item name="textCursorDrawable"… !!$
  7. eme.Material.Light Theme.AppCompat.Light Theme.MaterialComponents.Light & Theme.Owl.Pink themes Theme.Material.Light - Theme.AppCompat.Light !%

    Theme.MaterialComponents.Light !!& Theme.Owl.Blue tyle> <item name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground"… <item name="textCursorDrawable"… !!$ <style> <item name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground <item name="textCursorDrawable"… !"style>
  8. eme.Material.Light Theme.AppCompat.Light Theme.MaterialComponents.Light & Theme.Owl.Pink themes Theme.Material.Light - Theme.AppCompat.Light !%

    Theme.MaterialComponents.Light !!& Theme.Owl.Blue tyle> <item name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground"… <item name="textCursorDrawable"… !!$ <style> <item name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground <item name="textCursorDrawable"… !"style>
  9. Theme.Material.Light - Theme.AppCompat.Light !% Theme.MaterialComponents.Light !!& Theme.Owl.Pink themes ThemeOverlay.Owl.Blue <style>

    <item name="colorPrimary"… <item name="colorSecondary"… <item name="toolbarStyle"… <item name="materialButtonStyle"… <item name="buttonStyle"… <item name="selectableItemBackground"… <item name="textCursorDrawable"… !"style> <style name="ThemeOverlay.Owl.Blue" parent=""> <item name="colorPrimary"… <item name="colorSecondary"… !"style>
  10. @ColorInt fun Context.getThemeColor( @AttrRes themeAttrId: Int ): Int { return

    obtainStyledAttributes( intArrayOf(themeAttrId) ).use { it.getColor(0, Color.MAGENTA) } } C R A F T Y T I P
  11. val themedContext = ContextThemeWrapper( context, R.style.ThemeOverlay_Owl_Blue ) val inflater =

    LayoutInflater.from(themedContext) val view = inflater.inflate(!!$) C R A F T Y T I P
  12. styles themes Map<view attribute, value> Associated with & applied to

    single view Map<theme attribute, value> Associated with a Context Applied to a hierarchy Overlays ancestors
  13. styles themes Map<view attribute, value> Associated with & applied to

    single view Part of view attribute resolution Map<theme attribute, value> Associated with a Context Applied to a hierarchy Overlays ancestors
  14. public Button(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.buttonStyle); }

    BUTTON <item name="buttonStyle">@style/Widget.Material.Light.Button!"item>
  15. C R A F T Y T I P <style

    name="Theme.App"> <item name="toolbarStyle">@style/Widget.App.Toolbar!"item> !!$ Make styling opt out
  16. C R A F T Y T I P class

    MyTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0, @StyleRes defStyleRes: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr)
  17. C R A F T Y T I P class

    MyTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0, @StyleRes defStyleRes: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr)
  18. C R A F T Y T I P class

    MyTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0, @StyleRes defStyleRes: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr)
  19. C R A F T Y T I P class

    MyTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = android.R.attr.textViewStyle, @StyleRes defStyleRes: Int = 0 ) : AppCompatTextView(context, attrs, defStyleAttr)
  20. styles themes Map<view attribute, value> Associated with & applied to

    single view Part of view attribute resolution Map<theme attribute, value> Associated with a Context Applied to a hierarchy Overlays ancestors
  21. styles themes <style name="Widget.App.Foo"> <item name="android:background"> ?attr/colorSurface !"item> !!$ <style!#

    <style name="Theme.App"> <item name="toolbarStyle"> @style/Widget.App.Toolbar !"item> !!$ <style!#
  22. BUTTON Colors Color state lists Stateful wrapper around colors Use

    same state system as drawables state_pressed = true
  23. Colors Color state lists Stateful wrapper around colors Use same

    state system as drawables BUTTON state_enabled = false
  24. Colors Color state lists <selector> <item android:color="@color/brand_color_bright" android:state_checked="true" /> <item

    android:color="@color/brand_color" /> </selector> res/color/csl_brand_checked.xml
  25. Colors Color state lists Tip: order the items from most-specific

    to least-specific CSL selects the first item which matches the state
  26. Colors Color state lists Tip: order the items from most-specific

    to least-specific CSL selects the first item which matches the state <selector> <item android:color="@color/brand_color_bright" android:state_checked="true" /> <item android:color="@color/brand_color" /> </selector>
  27. Colors Color state lists Tip: order the items from most-specific

    to least-specific CSL selects the first item which matches the state <selector> <item android:color="@color/brand_color" /> <item android:color="@color/brand_color_bright" android:state_checked="true" /> </selector> Will always be selected as it matches all states
  28. <shape android:shape="rectangle" android:tint="@color/btn_colored_background_material"> <corners android:radius="?attr/buttonCornerRadius" /> <solid android:color="@color/white" /> <padding

    android:left="@dimen/button_padding_horizontal_material" android:top="@dimen/button_padding_vertical_material" android:right="@dimen/button_padding_horizontal_material" android:bottom="@dimen/button_padding_vertical_material" /> </shape>
  29. Drawable tinting Added in API 21 (Lollipop) Tints a solid

    color using a specified blending mode Can be stateful if using a ColorStateList Supported < API 21 in VectorDrawableCompat and DrawableCompat
  30. Fill != Tint ADD CLEAR DARKEN DST DST_ATOP DST_IN DST_OUT

    DST_OVER LIGHTEN MULTIPLY OVERLAY SCREEN SRC SRC_ATOP SRC_IN SRC_OUT SRC_OVER XOR
  31. Recommendation For drawables with a single color (e.g. icons, backgrounds)

    
 
 #1 Fill using a solid color (e.g. black) 
 #2 Tint using your chosen color/attribute
  32. <shape android:shape="rectangle" android:tint="@color/btn_colored_background_material"> <solid android:color="@color/white" /> </shape> <selector> <item android:state_enabled="false"

    android:alpha="?attr/disabledAlpha" android:color="?attr/colorButtonNormal" /> <item android:color="?attr/colorAccent" /> </selector>
  33. ColorStateList Or use AppCompat: Alpha modulation added in API 21

    (android:alpha) Can reference theme attributes from API 23 AppCompatResources.getColorStateList()
  34. Single item ColorStateLists Tip <!-- colorOnPrimary * 25% alpha -->

    <color name="color_on_primary_25">...</color> <!-- colorOnPrimary * 50% alpha --> <color name="color_on_primary_50">...</color> Repeat for any colors you need an alpha variant of
  35. Single item ColorStateLists Tip <selector> <item android:alpha="0.25" android:color="?attr/colorOnPrimary" /> </selector>

    res/color/color_on_primary_25.xml <selector> <item android:alpha="0.5" android:color="?attr/colorOnPrimary" /> </selector> res/color/color_on_primary_50.xml
  36. Gotcha <selector> <item android:alpha="0.25" android:color="?attr/colorOnPrimary" /> </selector> <ImageView android:background="@color/on_primary_25" />

    res/color/on_primary_25.xml <ImageView android:background="@color/on_primary_25" /> Crash Using colors as backgrounds
  37. <selector> <item android:alpha="0.25" android:color="?attr/colorOnPrimary" /> </selector> <ImageView android:background="@drawable/rectangle" app:backgroundTint="@color/on_primary_25" />

    res/color/on_primary_25.xml res/drawable/rectangle.xml <shape> <solid android:color="#FFFFFF" /> </shape> Using colors as backgrounds Gotcha
  38. T H E M E S & S T Y

    L E S Organisation
  39. T H E M E S & S T Y

    L E S Organisation Chris' opinionated rules on:
  40. <resources> <color name="color_primary">...</color> <color name="color_primary_dark">...</color> <color name="color_accent">...</color> </style> <style name="Theme.App"

    parent="…"> <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryDark">@color/color_primary_dark</item> <item name="colorAccent">@color/color_accent</item> </style> Nein
  41. #1 Use literal names for resources #2 Use consistent names

    Rule #3 Layer your themes for re-use
  42. <style name="Theme.AppName"> ... <!-- Added in API 27 --> <item

    name="android:windowLightNavigationBar">true</item> </style> values/themes.xml Something something error API level
  43. <style name="Theme.AppName"> <!-- Many many attributes --> <!-- Added in

    API 27 --> <item name="android:windowLightNavigationBar">true</item> </style> <style name="Theme.AppName"> <!-- Many many attributes --> <!-- Added in API 27 --> <item name="android:windowLightNavigationBar">true</item> </style> values/themes.xml
  44. values/themes.xml <style name="Theme.AppName"> <!-- Many many attributes --> <!-- Added

    in API 27 --> <item name="android:windowLightNavigationBar">true</item> </style> values-v27/themes.xml <style name="Theme.AppName"> <!-- Many many attributes --> <!-- Added in API 27 --> <item name="android:windowLightNavigationBar">true</item> </style>
  45. <style name="Theme.AppName"> <!-- Many many attributes --> </style> values/themes.xml <style

    name="Theme.AppName"> <!-- Many many attributes --> <!-- Added in API 27 --> <item name="android:windowLightNavigationBar">true</item> </style> values-v27/themes.xml We now have multiple copies of the same theme
  46. The only tool we really have is inheritance, which means...

    Layering This is how AppCompat is built We want re-use as much as possible
  47. Platform.Theme.MyApp Use only to set API level specific attributes <style

    name="Platform.Theme.AppName" parent="Platform.V14.Theme.AppName" /> values/themes.xml <style name="Platform.V27.Theme.AppName" parent="Platform.V14.Theme.AppName"> <!-- Attributes specific to v27 --> </style> values-v27/themes.xml <style name="Platform.V14.Theme.AppName" parent="Theme.MaterialComponents" /> <style name="Platform.Theme.AppName" parent="Platform.V27.Theme.AppName" />
  48. Platform.Theme.MyApp Use only to set API level specific attributes <style

    name="Platform.Theme.AppName" parent="Platform.V14.Theme.AppName" /> values/themes.xml <style name="Platform.V27.Theme.AppName" parent="Platform.V14.Theme.AppName"> <!-- Attributes specific to v27 --> </style> values-v27/themes.xml <style name="Platform.V14.Theme.AppName" parent="Theme.MaterialComponents" /> <style name="Platform.Theme.AppName" parent="Platform.V27.Theme.AppName" />
  49. Platform.Theme.MyApp Base.Theme.MyApp Everything common across your themes <style name="Base.Theme.AppName" parent="Platform.Theme.AppName">

    <!-- Things common across app --> <item name="textAppearanceHeadline1">...</item> <item name="editTextStyle">...</item> </style> values/themes.xml Usually have one of these
  50. Platform.Theme.MyApp Base.Theme.MyApp Entry point Can have multiple of these, but

    KISS Theme.MyApp <style name="Theme.AppName" parent="Base.Theme.AppName" /> values/themes.xml values-night/themes.xml <style name="Theme.AppName" parent="Base.Theme.AppName"> <!-- Overrides for dark theme --> </style>
  51. #1 Use literal names for resources #2 Use consistent names

    #3 Layer your themes for re-use Rule #4 Split files based on purpose
  52. <resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.MaterialComponents.Light">

    <!-- Customize your theme here. --> <item name="colorPrimary">@color/indigo</item> <item name="colorAccent">@color/indigo</item> <item name="colorControlActivated">?colorPrimary</item> <item name="toolbarStyle">@style/Widget.Toolbar</item> <item name="tabStyle">@style/Widget.IOSched.Tabs</item> <item name="android:textViewStyle">@style/Widget.IOSched.TextView</item> <item name="android:windowBackground">@color/white</item> <item name="android:statusBarColor">@color/status_bar</item> <item name="android:windowLightStatusBar" tools:targetApi="m">true</item> <item name="android:navigationBarColor">@color/indigo_dark</item> <item name="android:textColorTertiary">@color/text_color_tertiary</item> <item name="sessionListKeyline">@dimen/session_keyline</item> <item name="eventFilterViewStyle">@style/Widget.IOSched.EventFilters</item> styles.xml
  53. <style name="AppTheme.Speaker" parent="AppTheme.LightNav"> <item name="android:windowSharedElementEnterTransition">@transition/speaker_shared_enter <item name="sessionListKeyline">@dimen/margin_normal</item> </style> <style name="AppTheme.PopupTheme"

    parent="ThemeOverlay.MaterialComponents.Light" /> <style name="Widget.AppBar" parent="@style/Widget.Design.AppBarLayout"> <item name="popupTheme">@style/AppTheme.PopupTheme</item> <item name="android:background">@color/white</item> <item name="android:elevation">8dp</item> <item name="android:orientation">vertical</item> </style> <style name="Widget.Toolbar" parent="Widget.AppCompat.Toolbar"> <item name="titleTextAppearance">@style/TextAppearance.IOSched.ToolbarTitle</item> <item name="popupTheme">@style/AppTheme.PopupTheme</item> </style> <style name="TextAppearance.IOSched.Body" parent="@style/TextAppearance.AppCompat.Body1"> <item name="android:textColor">?android:textColorSecondary</item> </style> <style name="TextAppearance.IOSched.ToolbarTitle" parent="@style/TextAppearance.Widget.AppCo <item name="android:fontFamily">@font/google_sans</item> styles.xml
  54. </style> <style name="Widget.Toolbar" parent="Widget.AppCompat.Toolbar"> <item name="titleTextAppearance">@style/TextAppearance.IOSched.ToolbarTitle</item> <item name="popupTheme">@style/AppTheme.PopupTheme</item> </style> <style

    name="TextAppearance.IOSched.Body" parent="@style/TextAppearance.AppCompat.Body1"> <item name="android:textColor">?android:textColorSecondary</item> </style> <style name="TextAppearance.IOSched.ToolbarTitle" parent="@style/TextAppearance.Widget.AppCo <item name="android:fontFamily">@font/google_sans</item> </style> <style name="TextAppearance.IOSched.SessionTitle" parent="@style/TextAppearance.AppCompat.Su <item name="android:fontFamily">@font/google_sans</item> <item name="android:textStyle">bold</item> </style> <style name="TextAppearance.IOSched.ListSecondary" parent="@style/TextAppearance.AppCompat.B <item name="android:textColor">?android:textColorSecondary</item> </style> <style name="TextAppearance.IOSched.ReservationStatus" parent="@style/TextAppearance.IOSched styles.xml
  55. They've stuck to the rule of keeping each resource type

    in seperate files styles.xml <style> in styles.xml <dimen> in dimens.xml <string> in strings.xml
  56. styles.xml They've stuck to the rule of keeping each resource

    type in seperate files Not a bad idea, but it quickly leads to huge files
  57. Split files based on purpose themes.xml Themes and ThemeOverlays type.xml

    TextAppearances, text size dimensions styles.xml Widget styles. Only. Easy mode dimens.xml colors.xml strings.xml
  58. Split files based on purpose Hard mode themes.xml type.xml styles.xml

    dimens.xml colors.xml strings.xml shape.xml All resources used for shape appearance sys_ui.xml All booleans, colors used to control system-ui
  59. #1 Use literal names for resources #2 Use consistent names

    #3 Layer your themes for re-use #4 Split files based on purpose
  60. colorOnSurface #000000 colorOnSecondary #000000 colorOnPrimary #ffffff colorError #b00020 colorSurface #ffffff

    colorSecondaryVariant #ffc000 colorSecondary #ffde03 colorPrimaryVariant #0035c9 colorPrimary #0336ff
  61. colorOnSurface #000000 colorSurface #ffffff colorOnSecondary #000000 colorSecondary #ffde03 colorOnPrimary #ffffff

    colorPrimary #0336ff colorError #b00020 colorSecondaryVariant #ffc000 colorPrimaryVariant #0035c9
  62. <item name="textAppearanceHeadline1">@style/TextAppearance.Owl.Headline1!"item> <item name="textAppearanceHeadline2">@style/TextAppearance.Owl.Headline2!"item> <item name="textAppearanceHeadline3">@style/TextAppearance.Owl.Headline3!"item> <item name="textAppearanceHeadline4">@style/TextAppearance.Owl.Headline4!"item> <item name="textAppearanceHeadline5">@style/TextAppearance.Owl.Headline5!"item>

    <item name="textAppearanceHeadline6">@style/TextAppearance.Owl.Headline6!"item> <item name="textAppearanceSubtitle1">@style/TextAppearance.Owl.Subtitle1!"item> <item name="textAppearanceSubtitle2">@style/TextAppearance.Owl.Subtitle2!"item> <item name=" textAppearanceBody1" >@style/TextAppearance.Owl.Body1!"item> <item name="textAppearanceBody2">@style/TextAppearance.Owl.Body2!"item> <item name="textAppearanceButton">@style/TextAppearance.Owl.Button!"item> <item name="textAppearanceCaption">@style/TextAppearance.Owl.Caption!"item> <item name="textAppearanceOverline">@style/TextAppearance.Owl.Overline!"item>
  63. <style name="Widget.MaterialComponents.BottomSheet" parent="!!$"> <item name="shapeAppearance"> ?attr/shapeAppearanceLargeComponent!"item> <item name="shapeAppearanceOverlay"> @style/ShapeAppearanceOverlay.MaterialComponents.BottomSheet!"item> !"style>

    <style name="ShapeAppearanceOverlay.MaterialComponents.BottomSheet" parent=""> <item name="cornerSizeBottomRight">0dp!"item> <item name="cornerSizeBottomLeft">0dp!"item> !"style>
  64. - <item name="colorPrimary">@color/baseline_purple_500!"item> - <item name="colorSecondary">@color/baseline_purple_600!"item> + <item name="colorPrimary">@color/owl_blue_700!"item> +

    <item name="colorSecondary">@color/owl_yellow_500!"item> - <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance. - <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance - <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance. + <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance. + <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance + <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance. - <item name="textAppearanceHeadline1">@style/TextAppearance.Baselin - <item name="textAppearanceHeadline2">@style/TextAppearance.Baselin
  65. - <item name="colorPrimary">@color/baseline_purple_500!"item> - <item name="colorSecondary">@color/baseline_purple_600!"item> + <item name="colorPrimary">@color/owl_blue_700!"item> +

    <item name="colorSecondary">@color/owl_yellow_500!"item> - <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance. - <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance - <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance. + <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance. + <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance + <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance. - <item name="textAppearanceHeadline1">@style/TextAppearance.Baselin - <item name="textAppearanceHeadline2">@style/TextAppearance.Baselin j.mp/material-theme-builder
  66. DayNight First, tell AppCompat what mode to use Can be

    done by calling either: AppCompatDelegate.setDefaultNightMode() getDelegate().setLocalNightMode()
  67. We need something to alias between the colors <color name="color_primary">@color/indigo_500</color>

    values/colors.xml <color name="color_primary">@color/indigo_200</color> values-night/colors.xml
  68. <resources> <attr name="colorPrimarySurface" format="color" /> <attr name="colorOnPrimarySurface" format="color" /> </resources>

    values/attrs.xml <style name="Theme.MyApp" parent="Base.Theme.MyApp"> <item name="colorPrimarySurface">?attr/colorPrimary</item> <item name="colorOnPrimarySurface">?attr/colorOnPrimary</item> </style> values/themes.xml values-night/themes.xml <style name="Theme.MyApp" parent="Base.Theme.MyApp"> <item name="colorPrimarySurface">?attr/colorSurface</item> <item name="colorOnPrimarySurface">?attr/colorOnSurface</item> </style>
  69. TL;DR Understand the difference between themes and styles Prefer theme

    attributes when referencing resources Tint drawables rather than filling Name your resources consistently Layer your themes to easily vary its contents Use Material Theming to express your brand