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

Android UI: Patterns, Practices, Pitfalls

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

Chris Horner

November 09, 2019
Tweet

More Decks by Chris Horner

Other Decks in Technology

Transcript

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

    Int, bottom: Int ) ViewGroup override fun onMeasure( widthSpec: Int, heightSpec: Int )\
  2. <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>
  3. Text label <LinearLayout android:orientation=“horizontal" > This tag and its children

    can be replaced by one <TextView/> and a compound drawable
  4. Animation Complexity view.animate() .translationX(100f) .alpha(0.5f) .rotation(45f) Easy Pretty tricky android:animateLayoutChanges="true"

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

    ObjectAnimator.ofFloat(...)\ <MotionScene> </MotionScene> TransitionManager .beginDelayedTransition(vie
  6. 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
  7. Transition Tips 1. Make them quick, debug them slow 2.

    Choreograph customisations with Kotlin
  8. <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>
  9. <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>
  10. 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) }
  11. 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() } }
  12. Transition Tips 1. Make them quick, debug them slow 2.

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

    addTransition(ChangeBounds()) addTarget(viewA) interpolator = FastOutSlowInInterpolator() }/ addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(TextResize()) addTarget(viewB) interpolator = FastOutLinearInInterpolator() }/ }/
  14. 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() }/ }/
  15. 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
  16. 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
  17. 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
  18. 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
  19. <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" />
  20. 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) }
  21. <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>
  22. <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>
  23. <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>
  24. <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>
  25. 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
  26. 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
  27. 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
  28. 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