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. 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”)
  2. 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
  3. 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>
  4. 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>
  5. 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>
  6. 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>
  7. 16.

    :-/

  8. 17.

    :-(

  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 47.
  23. 48.
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 70.

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

    materialShapeDrawable.initializeElevationOverlay(context) } override fun setElevation(elevation: Float) { super.setElevation(elevation) materialShapeDrawable.elevation = elevation } } ColorPickerView.kt
  40. 73.

    :-)

  41. 74.

    :-D

  42. 75.

    Final considerations ‣ Add programmatic equivalents of styleable attributes ‣

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