Android UI: Patterns, Practices, Pitfalls

7b1e567c19126de48554fe8e5e767395?s=47 Chris Horner
November 09, 2019

Android UI: Patterns, Practices, Pitfalls

Building UI's on Android can be tricky business. Sometimes it seems like there's a hundred ways to achieve something. Other times you have no idea where to even begin.
This talk demonstrates tips and tricks Android developers can apply when building their UIs, as well as outlines some of the common gotchas to avoid. It touches topics around:

Animations and Transitions
Layout hierarchies
Threading and timing
Themes and styles

7b1e567c19126de48554fe8e5e767395?s=128

Chris Horner

November 09, 2019
Tweet

Transcript

  1. Android UI Patterns, Practices, Pitfalls @chris_h_codes

  2. None
  3. None
  4. https:/ /github.com/aosp-mirror/.../View.java

  5. CSS IS AWESOME

  6. ViewGroup View

  7. ViewGroup override fun onMeasure( widthSpec: Int, heightSpec: Int )\

  8. override fun onLayout( changed: Boolean, left: Int, top: Int, right:

    Int, bottom: Int ) ViewGroup override fun onMeasure( widthSpec: Int, heightSpec: Int )\
  9. FrameLayout AbsoluteLayout LinearLayout RelativeLayout ListView

  10. GridLayout ConstraintLayout CoordinatorLayout NestedScrollView RecyclerView

  11. ConstraintLayout

  12. <ConstraintLayout> <TextView android:id="@+id/first" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/second"

    android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@idfirst" /> </ConstraintLayout>
  13. <LinearLayout android:orientation="vertical" > <TextView android:id="@+id/first" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/second"

    android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
  14. None
  15. <View android:background="pink" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:background="orange" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"

    app:layout_constraintBottom_toBottomOf="parent" /> <View android:background="green" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
  16. <View android:background="pink" /> <View android:background="orange" android:layout_gravity="center" /> <View android:background="green" android:layout_width="wrap_content"

    android:layout_height="wrap_content" android:layout_gravity="bottom|end" /> <FrameLayout> </FrameLayout>
  17. Text label <LinearLayout android:orientation=“horizontal" > This tag and its children

    can be replaced by one <TextView/> and a compound drawable
  18. override fun onMeasure( widthSpec: Int, heightSpec: Int )/ 1x 1x

    1x 1x 1x 1x 1x 1x
  19. override fun onMeasure( widthSpec: Int, heightSpec: Int )/ 1x 2x

    1x 2x 2x 1x 1x 1x
  20. override fun onMeasure( widthSpec: Int, heightSpec: Int )/ 2x 4x

    2x 4x 4x 2x 2x 2x
  21. <RelativeLayout> <LinearLayout> <RelativeLayout> <FrameLayout> <RelativeLayout> <RelativeLayout> <LinearLayout>

  22. <ConstraintLayout>

  23. ConstraintLayout

  24. ConstraintLayout ConstraintLayout

  25. ConstraintLayout ConstraintLayout

  26. Main container Screen container Screen root Recycler List children

  27. Be mindful of your ConstraintLayouts

  28. Be mindful of your MotionLayouts

  29. Animation Complexity

  30. Animation Complexity Easy Pretty tricky

  31. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky

  32. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

  33. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

    viewGroup .layoutTransition .enableTransitionType(LayoutTransition.CHANGING)
  34. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky ObjectAnimator.ofFloat(0f,

    1f)\ android:animateLayoutChanges="true"
  35. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky ObjectAnimator.ofFloat(object

    : Property<View, Float> ...)\ android:animateLayoutChanges="true"
  36. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene>
  37. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene> TransitionManager .beginDelayedTransition(viewGroup)
  38. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene> TransitionManager .beginDelayedTransition(vie
  39. MotionLayout Easy to use, declarative syntax Seekable Can be driven

    by touch
  40. MotionLayout Easy to use, declarative syntax Seekable Can be driven

    by touch All animated views have to be direct children Can’t easily perform shared element transitions
  41. Transition Tips

  42. Transition Tips 1. Make them quick, debug them slow

  43. None
  44. None
  45. None
  46. None
  47. None
  48. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin
  49. class SomeFragment : Fragment() { sharedElementEnterTransition = TransitionInflater .from(context) .inflateTransition(android.R.transition.move)

    }
  50. sharedElementEnterTransition = TransitionInflater .from(context) .inflateTransition(android.R.transition.move) <transitionSet> <changeBounds/> <changeTransform/> <changeClipBounds/> <changeImageTransform/>

    </transitionSet>
  51. Transitions can… Target specific views Exclude specific views Have their

    duration, interpolation, and pathing modified
  52. <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="200" > <transitionSet android:interpolator="@android:interpolator/fast_out_slow_in" android:matchOrder="@string/transitionName_currentFeelingButton" > <changeBounds />

    <changeTransform /> </transitionSet> <transitionSet android:interpolator="@android:interpolator/fast_out_linear_in" android:matchOrder="@string/transitionName_feelingTitle" > <arcMotion android:maximumAngle="150f"/> <changeBounds/> <transition class="codes.chrishorner.tumtracker.animation.TextResize"/> </transitionSet> </transitionSet>
  53. <transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:duration="200" > <transitionSet android:interpolator="@android:interpolator/fast_out_slow android:matchOrder="@string/transitionName_currentFeeling > <changeBounds />

    <changeTransform /> </transitionSet> <transitionSet android:interpolator="@android:interpolator/fast_out_linear
  54. <changeBounds /> <changeTransform /> </transitionSet> <transitionSet android:interpolator="@android:interpolator/fast_out_linear android:matchOrder="@string/transitionName_feelingTitle" > <arcMotion

    android:maximumAngle="150f"/> <changeBounds/> <transition class="codes.chrishorner.tumtracker.animation.T </transitionSet> </transitionSet>
  55. None
  56. inline fun transitionSet( block: TransitionSet.() -> Unit ): TransitionSet {

    return TransitionSet().apply(block) } inline fun TransitionSet.addSet( block: TransitionSet.() -> Unit ) { addTransition(TransitionSet().apply(block)) } inline fun TransitionSet.addTransition( transition: Transition, block: Transition.() -> Unit ) { transition.apply(block) addTransition(transition) }
  57. val transition = transitionSet { duration = 250 addSet {

    addTransition(ChangeBounds()) addTransition(ChangeTransform()) addTarget(currentFeelingTransitionName) interpolator = FastOutSlowInInterpolator() } addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(ChangeTransform()) addTransition(TextResize()) addTarget(titleTransitionName) interpolator = FastOutLinearInInterpolator() } }
  58. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin 3. Respect your parents
  59. val transition = transitionSet { duration = 250 addSet {

    addTransition(ChangeBounds()) addTarget(viewA) interpolator = FastOutSlowInInterpolator() }/ addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(TextResize()) addTarget(viewB) interpolator = FastOutLinearInInterpolator() }/ }/
  60. val transition = transitionSet { duration = 250 addSet {

    addTransition(ChangeBounds()) addTransition(ChangeTransform()) addTarget(viewA) interpolator = FastOutSlowInInterpolator() }/ addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(ChangeTransform()) addTransition(TextResize()) addTarget(viewB) interpolator = FastOutLinearInInterpolator() }/ }/
  61. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" > <!-- Child views to transition

    --> </FrameLayout>
  62. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" android:transitionGroup="false" > <!-- Child views to

    transition --> </FrameLayout>
  63. <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/viewBackground" android:transitionGroup="false" android:clipChildren="false" > <!-- Child views

    to transition --> </FrameLayout>
  64. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin 3. Respect your parents 4. Remember that arc motion is a thing
  65. None
  66. addTransition(ChangeBounds()) { pathMotion = ArcMotion() }/

  67. addTransition(ChangeBounds()) { pathMotion = ArcMotionPlus() }/

  68. None
  69. https:/ /github.com/neild001/ArcMotionPlus Using the ArcMotionPlus Using the ArcMotionPlus is straightforward.

    There are a just two settings • Arc Angle • Reflect Neil Davies neild001
  70. https:/ /gist.github.com/chris-horner/80837fd3f3ac54052b766648d80ddbd2 class ArcMotionPlus(private val arcAngle: Float = 90f, private

    val reflected: Boolean = false) : PathMotion() { override fun getPath(startX: Float, startY: Float, endX: Float, endY: Float): Path { val start = PointF(startX, startY) val end = PointF(endX, endY) require(!(start.x == end.x && start.y == end.y)) { "Start and end points cannot be the same." } require(arcAngle in 1.0..179.0) { "Arc angle must be between 1 and 179 degrees." } val angleRadians: Double = Math.toRadians(arcAngle.toDouble()) val deltaX: Float = start.x - end.x val deltaY: Float = start.y - end.y val halfChordLength: Float = sqrt((deltaX * deltaX + deltaY * deltaY).toDouble()).toFloat() / 2f val radius: Float = halfChordLength / sin(angleRadians / 2f).toFloat() // The length of the line from the start or end point to the control point. chris-horner / ArcMotionPlus.kt
  71. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin 3. Respect your parents 4. Remember that arc motion is a thing
  72. ScrollView Shenanigans

  73. ScrollView Shenanigans <ScrollView android:fillViewport="true" >

  74. None
  75. None
  76. None
  77. <ScrollView> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <View android:background="@color/pink" /> <View android:background="@color/orange"

    /> <View android:background="@color/green" /> </LinearLayout> </ScrollView>
  78. <ScrollView> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <View android:background="@color/pink" /> <View android:background="@color/orange"

    /> <View android:background="@color/green" /> </LinearLayout> </ScrollView>
  79. <View android:background="@color/pink" /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <View android:background="@color/orange"

    /> <Space android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <View android:background="@color/green" />
  80. <ScrollView android:fillViewport="true" > <LinearLayout> <View android:background="@color/pink" /> <Space android:layout_width="match_parent" android:layout_height="0dp"

    android:layout_weight="1" /> <View android:background="@color/orange" />
  81. Forget ActionBar

  82. <application android:theme="@style/Theme.MaterialComponents.DayNight" />

  83. <application android:theme="@style/Theme.MaterialComponents.DayNight"\ />

  84. <application android:theme="@style/Theme.MaterialComponents.DayNight.DarkActionBar"\ />

  85. <application android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"\ />

  86. <application android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"\ /> AndroidManifest.xml <LinearLayout android:orientation="vertical" > <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" />

    </LinearLayout> activity_main.xml MainActivity.kt setSupportActionBar(toolbar)
  87. Toolbar Fragment A

  88. Title A Fragment A Fragment B

  89. Title B Fragment A Fragment B

  90. Title B Fragment B override fun onViewCreated() { setHasOptionsMenu(true) }

    override fun onCreateOptionsMenu() { super.onCreateOptionsMenu(menu, inflater) menu.clear() inflater.inflate(R.menu.screen_b, menu) }
  91. Title B

  92. None
  93. Title B

  94. None
  95. Don’t

  96. <androidx.appcompat.widget.Toolbar app:menu="@menu/menu" app:title="@string/title" />

  97. transitionSet { addTransition(ChangeBounds()) { addTarget(toolbar) } addTransition(Slide()) { addTarget(fromView) addTarget(toView)

    } }
  98. Styling Toolbars

  99. <LinearLayout android:orientation="vertical" > <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" /> <ScrollView

    android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>
  100. <LinearLayout android:orientation="vertical" > <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?attr/colorPrimary" app:title="My toolbar" />

    <ScrollView android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout>
  101. <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" > <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" />

    </com.google.android.material.appbar.AppBarLayout>
  102. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface" />

  103. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface" /> <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar"

    > <item name="colorPrimary">@color/blue_700</item> <item name="colorPrimaryVariant">@color/blue_700_variant</item> <item name="colorPrimaryDark">@color/blue_700_variant</item> <item name="colorOnPrimary">@android:color/white</item> <item name="colorOnPrimarySurface">@android:color/white</item> </style>
  104. <androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="wrap_content" app:title="My toolbar" style="@style/Widget.MaterialComponents.Toolbar.PrimarySurface" /> <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar"

    > <item name="colorPrimary">@color/blue_700</item> <item name="colorPrimaryVariant">@color/blue_700_variant</item> <item name="colorPrimaryDark">@color/blue_700_variant</item> <item name="colorOnPrimary">@android:color/white</item> <item name="colorOnPrimarySurface">@android:color/white</item> </style>
  105. <androidx.appcompat.widget.Toolbar app:titleTextColor="@android:color/white" android:background="@color/background" /> Basically, never do this

  106. Respect Themes

  107. android:background="@color/backgroundPrimary"\

  108. android:background="?attr/colorPrimary"\

  109. <vector android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0" android:tint="?attr/colorControlNormal" >

  110. <com.google.android.material.card.MaterialCardView android:theme="@style/Theme.MyApp.Dark" > <TextView android:textAppearance="?attr/textAppearanceBody1" /> </com.google.android.material.card.MaterialCardView> <com.google.android.material.card.MaterialCardView android:theme="@style/Theme.MyApp" >

    <TextView android:textAppearance="?attr/textAppearanceBody1" /> </com.google.android.material.card.MaterialCardView>
  111. Required Watching https://www.youtube.com/watch?v=Owkf8DhAOSo

  112. Inset Issues

  113. Inset Issues https://www.youtube.com/watch?v=_mGDMVRO3iE BECOMING A MASTER WINDOW FITTER @chrisbanes

  114. Is one of my parent ViewGroups “insets aware”? How far

    down do I need to put fitsSystemWindows="true"? How do I cope with cumulative padding? Questions I Always Had
  115. Give up

  116. Always render edge-to-edge Give up

  117. override fun onCreate(savedInstanceState: Bundle?) { // Render under the status

    and navigation bars. window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE } Always render edge-to-edge
  118. None
  119. toolbar.updatePaddingWithInsets(top = true) bottomNav.updatePaddingWithInsets(bottom = true)

  120. toolbar.updatePaddingWithInsets(top = true) bottomNav.updatePaddingWithInsets(bottom = true)

  121. https:/ /gist.github.com/chris-horner/4718402eb6ebd4d89b63491245a359ff fun View.updatePaddingWithInsets(left: Boolean = false, top: Boolean =

    false, right: Boolean = false, bottom: Boolean = false) { doOnApplyWindowInsets { insets, padding -> updatePadding(left = if (left) padding.left + insets.systemWindowInsetLeft else padding.left, top = if (top) padding.top + insets.systemWindowInsetTop else padding.top, right = if (right) padding.right + insets.systemWindowInsetRight else padding.right, bottom = if (bottom) padding.bottom + insets.systemWindowInsetBottom else padding.bottom) } } inline fun View.doOnApplyWindowInsets(crossinline block: (insets: WindowInsets, padding: Rect) -> Unit) { // Create a snapshot of padding. val initialPadding = Rect(paddingLeft, paddingTop, paddingRight, paddingBottom) // Set an actual OnApplyWindowInsetsListener which proxies to the given lambda, also passing in the original padding. chris-horner / InsetUtils.kt
  122. https:/ /github.com/chrisbanes/insetter

  123. Beware the Shadow Barber

  124. None
  125. None
  126. None
  127. None
  128. None
  129. <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:orientation="vertical" >

  130. <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:clipToPadding="false" android:orientation="vertical" >

  131. Recycler Wrongdoings

  132. None
  133. None
  134. None
  135. Generally, as long as you give views an ID they’ll

    try and restore their state
  136. You’re in a race!

  137. fun onRestoreInstanceState(state: Parcelable) You’re in a race!

  138. dataStream .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { items -> adapter.setItems(items) }\

  139. dataStream .replay(1) .refCount() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { items -> adapter.setItems(items)

    }\
  140. dataStream .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .replay(1) .refCount() .subscribe { items -> adapter.setItems(items)

    }\
  141. Attach and put data in your adapter right after your

    views are inflated
  142. What Takeaways?

  143. Takeaways ConstraintLayout and MotionLayout are great! But remember there are

    alternatives Widgets sometimes have surprising properties Handle Toolbars and window insets manually Understand and get the most out of the theming system Watch out for gotchas when it comes to state restoration and rendering
  144. chris_h_codes chris-horner chrishorner.codes Android UI Patterns, Practices, Pitfalls