Slide 1

Slide 1 text

Incorporating Material Theming into Custom Views Making custom components responsive to Material Theming

Slide 2

Slide 2 text

Nick Rout @ricknout

Slide 3

Slide 3 text

What is Material Theming?

Slide 4

Slide 4 text

ABCDEFGHI JKLMNOPQR STUVWXYZa bcdefghij klmnopqrs

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

‣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”)

Slide 7

Slide 7 text

Playground

Slide 8

Slide 8 text

ColorPickerView

Slide 9

Slide 9 text

ColorPickerView

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Custom theme <!-- 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>

Slide 12

Slide 12 text

Custom theme <!-- 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>

Slide 13

Slide 13 text

Custom theme <!-- 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>

Slide 14

Slide 14 text

Custom theme <!-- 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>

Slide 15

Slide 15 text

Custom theme

Slide 16

Slide 16 text

:-/

Slide 17

Slide 17 text

:-(

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

“OK” Button

Slide 20

Slide 20 text

“OK” Button 1

Slide 21

Slide 21 text

Before view_color_picker.xml

Slide 22

Slide 22 text

After view_color_picker.xml

Slide 23

Slide 23 text

Fixed “OK” Button

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Title & subtitle

Slide 26

Slide 26 text

Title & subtitle 2

Slide 27

Slide 27 text

Before view_color_picker.xml

Slide 28

Slide 28 text

After view_color_picker.xml

Slide 29

Slide 29 text

Fixed title & subtitle

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Background

Slide 32

Slide 32 text

Background 3

Slide 33

Slide 33 text

Before bg_color_picker.xml class ColorPickerView ... { init { inflate(context, R.layout.view_color_picker, this) ... setBackgroundResource(R.drawable.bg_color_picker) } } ColorPickerView.kt

Slide 34

Slide 34 text

Add a styleable attrs.xml

Slide 35

Slide 35 text

Add a default style ... <!-- ColorPickerView widget attribute --> <item name=“colorPickerStyle”>@style/Widget.App.ColorPickerView</item> <item name="shapeAppearance">?attr/shapeAppearanceLargeComponent</item> <item name="backgroundTint">?attr/colorSurface</item> <item name="android:elevation">4dp</item> styles.xml

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Fixed background

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Title & subtitle color

Slide 42

Slide 42 text

Title & subtitle color 4

Slide 43

Slide 43 text

Add new styleable attrs ... attrs.xml ... <item name="titleTextColor">@color/material_on_surface_emphasis_high_type</item> <item name="subtitleTextColor">@color/material_on_surface_emphasis_medium</item> styles.xml

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Fixed text colors

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Items

Slide 48

Slide 48 text

Items 5

Slide 49

Slide 49 text

Before 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

Slide 50

Slide 50 text

Add item styleable attrs ... attrs.xml ... <item name=“itemShapeAppearance”>?attr/shapeAppearanceSmallComponent</item> styles.xml

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Fixed items

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Item ripples

Slide 58

Slide 58 text

Item ripples 6

Slide 59

Slide 59 text

Add item styleable attrs ... attrs.xml ... <item name=“itemRippleColor”>@color/ripple_color_color_picker</item> styles.xml

Slide 60

Slide 60 text

Handle all color states ripple_color_color_picker.xml

Slide 61

Slide 61 text

Handle all color states ripple_color_color_picker.xml

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Item ripples Unselected Selected

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Support dark theme

Slide 69

Slide 69 text

Support dark theme 7

Slide 70

Slide 70 text

Use elevation overlay class ColorPickerView ... { init { ... materialShapeDrawable.initializeElevationOverlay(context) } override fun setElevation(elevation: Float) { super.setElevation(elevation) materialShapeDrawable.elevation = elevation } } ColorPickerView.kt

Slide 71

Slide 71 text

Various elevations 2dp 16dp 32dp

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

:-)

Slide 74

Slide 74 text

:-D

Slide 75

Slide 75 text

Final considerations ‣ Add programmatic equivalents of styleable attributes ‣ See this commit ‣ Retain custom View state on configuration change ‣ eg. Selected color

Slide 76

Slide 76 text

No content