Incorporating Material Theming into Custom Views

Incorporating Material Theming into Custom Views

Material Theming is a means of systematically customizing Material Design to better reflect your product’s brand. It allows for easy customization of core widgets in terms of three main subsystems - color, typography and shape - when using the Material Theme Editor and Material Design Components for Android. However, how would these subsystems be incorporated into custom components?

Custom Views can (and should) feel at home amongst the core components of Material Design. This presentation covers approaches to making Views responsive to Material Theming. With the aid of a practical example, this includes an intro to the capabilities of the Material Theming and MDC-Android, basic theme/style attribute support in Custom Views and a deep dive into key MDC-Android classes such as MaterialShapeDrawable, RippleUtils, ElevationOverlayProvider and more.

This was presented at mDevCamp 2019:
https://mdevcamp.eu/schedule.html#nick-rout-modal

It was also presented at Droidcon Berlin 2019:
https://www.de.droidcon.com/speaker/Nick-Rout

84ed0218b7b6eec8a5ac5af51c342c0c?s=128

Nick Rout

May 31, 2019
Tweet

Transcript

  1. Incorporating Material Theming into Custom Views Making custom components responsive

    to Material Theming
  2. Nick Rout @ricknout

  3. What is Material Theming?

  4. ABCDEFGHI JKLMNOPQR STUVWXYZa bcdefghij klmnopqrs

  5. implementation ”com.android.support:design:$support_version” implementation ”com.google.android.material:material:$material_version” Material Components for Android

  6. ‣Material Design, Develop & Tools ‣material.io ‣“Material Theming: Build Expressively

    with Material Components” (Google I/O 2019) ‣“Setting up a Material Components theme for Android” & “Hands on with Material Components for Android” series ‣medium.com/@ricknout ‣MDC-Android repository (including “Build a Material Theme”)
  7. Playground

  8. ColorPickerView

  9. ColorPickerView

  10. Custom theme Primary #2962FF Primary Variant #0039CB Secondary #FFC107 Secondary

    Variant #C79100 Error #F44336 Surface #E7E9FF Background #FFFFFF Primary (Dark) #768FFF Primary Variant (Dark) #3C62CB Secondary (Dark) #FFF350 Secondary Variant (Dark) #C9C10C Error (Dark) #FF7961 Surface (Dark) #0001C0 Background (Dark) #121212 8dp Small 8dp Medium 8dp Large Font Family: Roboto Mono
  11. Custom theme <style name=“Theme.App.Custom” parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Global color attributes -->

    <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <item name="colorOnSecondary">@color/color_on_secondary</item> <item name="colorError">@color/color_error</item> <item name="colorOnError">@color/color_on_error</item> <item name="colorSurface">@color/color_surface</item> <item name="colorOnSurface">@color/color_on_surface</item> <item name="android:colorBackground">@color/color_background</item> <item name="colorOnBackground">@color/color_on_background</item> <!-- Global type attributes --> <item name="fontFamily">@font/roboto_mono</item> <item name="android:fontFamily">@font/roboto_mono</item> <!-- Global shape attributes --> <item name=“shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item> <item name=“shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item> <item name=“shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item> </style>
  12. Custom theme <style name=“Theme.App.Custom” parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Global color attributes -->

    <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <item name="colorOnSecondary">@color/color_on_secondary</item> <item name="colorError">@color/color_error</item> <item name="colorOnError">@color/color_on_error</item> <item name="colorSurface">@color/color_surface</item> <item name="colorOnSurface">@color/color_on_surface</item> <item name="android:colorBackground">@color/color_background</item> <item name="colorOnBackground">@color/color_on_background</item> <!-- Global type attributes --> <item name="fontFamily">@font/roboto_mono</item> <item name="android:fontFamily">@font/roboto_mono</item> <!-- Global shape attributes --> <item name=“shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item> <item name=“shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item> <item name=“shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item> </style>
  13. Custom theme <style name=“Theme.App.Custom” parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Global color attributes -->

    <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <item name="colorOnSecondary">@color/color_on_secondary</item> <item name="colorError">@color/color_error</item> <item name="colorOnError">@color/color_on_error</item> <item name="colorSurface">@color/color_surface</item> <item name="colorOnSurface">@color/color_on_surface</item> <item name="android:colorBackground">@color/color_background</item> <item name="colorOnBackground">@color/color_on_background</item> <!-- Global type attributes --> <item name="fontFamily">@font/roboto_mono</item> <item name="android:fontFamily">@font/roboto_mono</item> <!-- Global shape attributes --> <item name=“shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item> <item name=“shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item> <item name=“shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item> </style>
  14. Custom theme <style name=“Theme.App.Custom” parent="Theme.MaterialComponents.DayNight.NoActionBar"> <!-- Global color attributes -->

    <item name="colorPrimary">@color/color_primary</item> <item name="colorPrimaryVariant">@color/color_primary_variant</item> <item name="colorOnPrimary">@color/color_on_primary</item> <item name="colorSecondary">@color/color_secondary</item> <item name="colorSecondaryVariant">@color/color_secondary_variant</item> <item name="colorOnSecondary">@color/color_on_secondary</item> <item name="colorError">@color/color_error</item> <item name="colorOnError">@color/color_on_error</item> <item name="colorSurface">@color/color_surface</item> <item name="colorOnSurface">@color/color_on_surface</item> <item name="android:colorBackground">@color/color_background</item> <item name="colorOnBackground">@color/color_on_background</item> <!-- Global type attributes --> <item name="fontFamily">@font/roboto_mono</item> <item name="android:fontFamily">@font/roboto_mono</item> <!-- Global shape attributes --> <item name=“shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item> <item name=“shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item> <item name=“shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item> </style>
  15. Custom theme

  16. :-/

  17. :-(

  18. Let’s fix this... 5/6 2/4 3 1 7

  19. “OK” Button

  20. “OK” Button 1

  21. Before <merge ...> <androidx.appcompat.widget.AppCompatButton android:id="@+id/okButton" ... android:textColor="#FFFFFF" android:backgroundTint=“#6200EE" /> </merge>

    view_color_picker.xml
  22. After <merge ...> <com.google.android.material.button.MaterialButton android:id="@+id/okButton" ... /> </merge> view_color_picker.xml

  23. Fixed “OK” Button

  24. Lessons learnt ‣Use standard MDC-Android components wherever possible (eg. MaterialButton)

    ‣Certain components auto-inflated with MaterialComponentsViewInflater ‣Avoid hardcoding component attributes ‣Standard components should “just work” i.t.o responding to Material Theming ‣github.com/ricknout/android-mdc-custom-views/ pull/1
  25. Title & subtitle

  26. Title & subtitle 2

  27. Before <merge ...> <TextView android:id="@+id/titleTextView" ... android:textSize="20sp" android:fontFamily=“sans-serif-medium" /> <TextView

    android:id="@+id/subtitleTextView" ... android:textSize=“16sp" /> </merge> view_color_picker.xml
  28. After <merge ...> <TextView android:id="@+id/titleTextView" ... android:textAppearance="?attr/textAppearanceHeadline6" /> <TextView android:id="@+id/subtitleTextView"

    ... android:textAppearance="?attr/textAppearanceSubtitle1" /> </merge> view_color_picker.xml
  29. Fixed title & subtitle

  30. Lessons learnt ‣ Use the android:textAppearance attribute to style TextViews

    ‣ Use MDC-Android attributes that map to the Material type system scale (eg. textAppearaceHeadline6) ‣ material.io/design/typography/the-type- system.html ‣ github.com/ricknout/android-mdc-custom- views/pull/2
  31. Background

  32. Background 3

  33. Before <shape ... android:shape="rectangle"> <solid android:color="#FFFFFF" /> <corners android:radius="4dp" />

    </shape> bg_color_picker.xml class ColorPickerView ... { init { inflate(context, R.layout.view_color_picker, this) ... setBackgroundResource(R.drawable.bg_color_picker) } } ColorPickerView.kt
  34. Add a styleable <resources> <attr name="colorPickerStyle" format="reference" /> <declare-styleable name="ColorPickerView">

    <attr name="shapeAppearance" /> <attr name="backgroundTint" /> <attr name="android:elevation" /> </declare-styleable> </resources> attrs.xml
  35. Add a default style <resources> <style name=“Theme.App.Custom”> ... <!-- ColorPickerView

    widget attribute --> <item name=“colorPickerStyle”>@style/Widget.App.ColorPickerView</item> </style> <style name=“Widget.App.ColorPickerView”> <item name="shapeAppearance">?attr/shapeAppearanceLargeComponent</item> <item name="backgroundTint">?attr/colorSurface</item> <item name="android:elevation">4dp</item> </style> </resources> styles.xml
  36. Parse background attrs class ColorPickerView ... { private val materialShapeDrawable

    = MaterialShapeDrawable( context, attrs, R.attr.colorPickerStyle, R.style.Widget_App_ColorPickerView ) init { inflate(context, R.layout.view_color_picker, this) ... context.withStyledAttributes( attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { val backgroundTint = getColorStateListOrThrow(R.styleable.ColorPickerView_backgroundTint) val elevation = getDimensionOrThrow(R.styleable.ColorPickerView_android_elevation) background = materialShapeDrawable backgroundTintList = backgroundTint setElevation(elevation) } } } ColorPickerView.kt
  37. Parse background attrs class ColorPickerView ... { private val materialShapeDrawable

    = MaterialShapeDrawable( context, attrs, R.attr.colorPickerStyle, R.style.Widget_App_ColorPickerView ) init { inflate(context, R.layout.view_color_picker, this) ... context.withStyledAttributes( attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { val backgroundTint = getColorStateListOrThrow(R.styleable.ColorPickerView_backgroundTint) val elevation = getDimensionOrThrow(R.styleable.ColorPickerView_android_elevation) background = materialShapeDrawable backgroundTintList = backgroundTint setElevation(elevation) } } } ColorPickerView.kt
  38. Apply background attrs class ColorPickerView ... { private val materialShapeDrawable

    = MaterialShapeDrawable( context, attrs, R.attr.colorPickerStyle, R.style.Widget_App_ColorPickerView ) init { inflate(context, R.layout.view_color_picker, this) ... context.withStyledAttributes( attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { val backgroundTint = getColorStateListOrThrow(R.styleable.ColorPickerView_backgroundTint) val elevation = getDimensionOrThrow(R.styleable.ColorPickerView_android_elevation) background = materialShapeDrawable backgroundTintList = backgroundTint setElevation(elevation) } } } ColorPickerView.kt
  39. Fixed background

  40. Lessons learnt ‣ It’s a good idea to create a

    styleable and default style(s) for any Custom View ‣ Use existing MDC-Android attribute names - shapeAppearance, backgroundTint, etc. ‣ Always use MaterialShapeDrawable for backgrounds (no other way to achieve shape theming, amongst other things) ‣ github.com/ricknout/android-mdc-custom- views/pull/3
  41. Title & subtitle color

  42. Title & subtitle color 4

  43. Add new styleable attrs <resources> <declare-styleable name="ColorPickerView"> ... <attr name="titleTextColor"

    /> <attr name="subtitleTextColor" /> </declare-styleable> </resources> attrs.xml <resources> <style name=“Widget.App.ColorPickerView”> ... <item name="titleTextColor">@color/material_on_surface_emphasis_high_type</item> <item name="subtitleTextColor">@color/material_on_surface_emphasis_medium</item> </style> </resources> styles.xml
  44. Parse attrs class ColorPickerView ... { init { ... context.withStyledAttributes(

    attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { … val titleTextColor = getColorStateListOrThrow(R.styleable.ColorPickerView_titleTextColor) val subtitleTextColor = getColorStateListOrThrow(R.styleable.ColorPickerView_subtitleTextColor) ... titleTextView.setTextColor(titleTextColor) subtitleTextView.setTextColor(subtitleTextColor) } } } ColorPickerView.kt
  45. Fixed text colors

  46. Lessons learnt ‣We got to reuse the styleable we made

    ‣Consider the color behind the text and use an appropriate “on” color ‣Existing MDC-Android text color resources exist for emphasis (eg. @color/ material_on_surface_emphasis_medium) ‣github.com/ricknout/android-mdc-custom-views/ pull/4 ‣Updated for MDC 1.1.0-alpha07
  47. Items

  48. Items 5

  49. Before <shape ... android:shape="oval"> <solid android:color="#000000" /> </shape> bg_color.xml class

    ColorPickerView ... { inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { itemView.colorView.setBackgroundResource(R.drawable.bg_color) ... } } } ColorPickerView.kt
  50. Add item styleable attrs <resources> <declare-styleable name="ColorPickerView"> ... <attr name=“itemShapeAppearance"

    /> </declare-styleable> </resources> attrs.xml <resources> <style name=“Widget.App.ColorPickerView”> ... <item name=“itemShapeAppearance”>?attr/shapeAppearanceSmallComponent</item> </style> </resources> styles.xml
  51. Parse shape appearance class ColorPickerView ... { private lateinit var

    itemShapeAppearanceModel: ShapeAppearanceModel init { ... context.withStyledAttributes( attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { ... val itemShapeAppearanceResId = getResourceIdOrThrow(R.styleable.ColorPickerView_itemShapeAppearance) itemShapeAppearanceModel = ShapeAppearanceModel .builder(context, itemShapeAppearanceResId, 0).build() } } ... } ColorPickerView.kt
  52. Apply shape appearance class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val materialShapeDrawable = MaterialShapeDrawable(itemShapeAppearanceModel) itemView.colorView.background = materialShapeDrawable materialShapeDrawable.setCornerSize(ShapeAppearanceModel.PILL) ... } } } ColorPickerView.kt
  53. Apply shape appearance class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val materialShapeDrawable = MaterialShapeDrawable(itemShapeAppearanceModel) itemView.colorView.background = materialShapeDrawable materialShapeDrawable.setCornerSize(ShapeAppearanceModel.PILL) ... } } } ColorPickerView.kt
  54. Apply shape appearance class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val materialShapeDrawable = MaterialShapeDrawable(itemShapeAppearanceModel) itemView.colorView.background = materialShapeDrawable materialShapeDrawable.setCornerSize(ShapeAppearanceModel.PILL) ... } } } ColorPickerView.kt
  55. Fixed items

  56. Lessons learnt ‣ Use a common ShapeAppearanceModel for each MaterialShapeDrawable

    item background ‣ For certain components, adjusting the shape corner size makes sense ‣ github.com/ricknout/android-mdc-custom- views/pull/5 ‣ Updated for MDC 1.1.0-alpha07
  57. Item ripples

  58. Item ripples 6

  59. Add item styleable attrs <resources> <declare-styleable name="ColorPickerView"> ... <attr name=“itemRippleColor"

    /> </declare-styleable> </resources> attrs.xml <resources> <style name=“Widget.App.ColorPickerView”> ... <item name=“itemRippleColor”>@color/ripple_color_color_picker</item> </style> </resources> styles.xml
  60. Handle all color states <selector ...> <!-- Selected --> <item

    android:alpha="0.08" android:color="?attr/colorPrimary" android:state_pressed="true" android:state_selected="true" /> <item android:alpha="0.16" android:color="?attr/colorPrimary" android:state_focused="true" android:state_hovered="true" android:state_selected="true" /> <item android:alpha="0.12" android:color="?attr/colorPrimary" android:state_focused="true" android:state_selected="true" /> <item android:alpha="0.04" android:color="?attr/colorPrimary" android:state_hovered="true" android:state_selected="true" /> <item android:alpha="0.00" android:color="?attr/colorPrimary" android:state_selected="true" /> <!-- Unselected --> <item android:alpha="0.08" android:color="?attr/colorOnSurface" android:state_pressed="true" /> <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_focused="true" android:state_hovered="true" /> <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_focused="true" /> <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_hovered="true" /> <item android:alpha="0.00" android:color="?attr/colorOnSurface" /> </selector> ripple_color_color_picker.xml
  61. Handle all color states <selector ...> <!-- Selected --> <item

    android:alpha="0.08" android:color="?attr/colorPrimary" android:state_pressed="true" android:state_selected="true" /> <item android:alpha="0.16" android:color="?attr/colorPrimary" android:state_focused="true" android:state_hovered="true" android:state_selected="true" /> <item android:alpha="0.12" android:color="?attr/colorPrimary" android:state_focused="true" android:state_selected="true" /> <item android:alpha="0.04" android:color="?attr/colorPrimary" android:state_hovered="true" android:state_selected="true" /> <item android:alpha="0.00" android:color="?attr/colorPrimary" android:state_selected="true" /> <!-- Unselected --> <item android:alpha="0.08" android:color="?attr/colorOnSurface" android:state_pressed="true" /> <item android:alpha="0.16" android:color="?attr/colorOnSurface" android:state_focused="true" android:state_hovered="true" /> <item android:alpha="0.12" android:color="?attr/colorOnSurface" android:state_focused="true" /> <item android:alpha="0.04" android:color="?attr/colorOnSurface" android:state_hovered="true" /> <item android:alpha="0.00" android:color="?attr/colorOnSurface" /> </selector> ripple_color_color_picker.xml
  62. Parse ripple color class ColorPickerView ... { private lateinit var

    itemRippleColor: ColorStateList init { ... context.withStyledAttributes( attrs, R.styleable.ColorPickerView, defStyleAttr, R.style.Widget_App_ColorPickerView ) { ... itemRippleColor = getColorStateListOrThrow(R.styleable.ColorPickerView_itemRippleColor) ... } } ... } ColorPickerView.kt
  63. Apply ripple color class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val rippleColor = RippleUtils.convertToRippleDrawableColor(itemRippleColor) val maskDrawable = GradientDrawable().apply { setColor(Color.WHITE) } val rippleDrawable = RippleDrawable(rippleColor, null, maskDrawable) itemView.background = rippleDrawable itemView.isSelected = colorItem.selected ... } } } ColorPickerView.kt
  64. Apply ripple color class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val rippleColor = RippleUtils.convertToRippleDrawableColor(itemRippleColor) val maskDrawable = GradientDrawable().apply { setColor(Color.WHITE) } val rippleDrawable = RippleDrawable(rippleColor, null, maskDrawable) itemView.background = rippleDrawable itemView.isSelected = colorItem.selected ... } } } ColorPickerView.kt
  65. Apply ripple color class ColorPickerView ... { ... inner class

    ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(colorItem: ColorItem) { val rippleColor = RippleUtils.convertToRippleDrawableColor(itemRippleColor) val maskDrawable = GradientDrawable().apply { setColor(Color.WHITE) } val rippleDrawable = RippleDrawable(rippleColor, null, maskDrawable) itemView.background = rippleDrawable itemView.isSelected = colorItem.selected ... } } } ColorPickerView.kt
  66. Item ripples Unselected Selected

  67. Lessons learnt ‣ Consider the color behind the ripple and

    use an appropriate “on” color ‣ Specify a default ColorStateList, convert using RippleUtils and use RippleDrawable ‣ github.com/ricknout/android-mdc-custom- views/pull/7 ‣ Updated for MDC 1.1.0-alpha07
  68. Support dark theme

  69. Support dark theme 7

  70. Use elevation overlay class ColorPickerView ... { init { ...

    materialShapeDrawable.initializeElevationOverlay(context) } override fun setElevation(elevation: Float) { super.setElevation(elevation) materialShapeDrawable.elevation = elevation } } ColorPickerView.kt
  71. Various elevations 2dp 16dp 32dp

  72. Lessons learnt ‣ MaterialShapeDrawable supports ElevationOverlayProvider out-of-the-box ‣ github.com/ricknout/android-mdc-custom- views/pull/6

  73. :-)

  74. :-D

  75. Final considerations ‣ Add programmatic equivalents of styleable attributes ‣

    See this commit ‣ Retain custom View state on configuration change ‣ eg. Selected color
  76. None