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

DroidKaigi: Android UI: Patterns, Practices, Pitfalls

Chris Horner
February 21, 2020

DroidKaigi: Android UI: Patterns, Practices, Pitfalls

Nearing the end of its lifespan, Android's UI framework is now chock-full of useful tools for building UIs. Sometimes it seems like there's a hundred ways to achieve something. Other times you have no idea where to even begin.

This talk outlines when and when not to apply certain UI components. It discusses tips and tricks Android developers can apply when building their UIs, as well as outlines some of the common gotchas to avoid. Topics discussed will include:

Animations and Transitions
Layout hierarchies
Not using ConstraintLayout for everything
Drawables
Custom Views/ViewGroups
Threading and timing
Themes and styles

Chris Horner

February 21, 2020
Tweet

More Decks by Chris Horner

Other Decks in Programming

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. 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) }
  10. 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() } }
  11. Transition Tips 1. Make them quick, debug them slow 2.

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

    addTransition(ChangeBounds()) addTarget(viewA) interpolator = FastOutSlowInInterpolator() }/ addSet { addTransition(ChangeBounds()) { pathMotion = ArcMotion() } addTransition(TextResize()) addTarget(viewB) interpolator = FastOutLinearInInterpolator() }/ }/
  13. 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() }/ }/
  14. 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
  15. 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
  16. 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
  17. 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
  18. <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" />
  19. 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) }
  20. <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>
  21. <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>
  22. <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>
  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. 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
  25. 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
  26. 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
  27. class CustomLayout : LinearLayout { fun display(items: List<Item>) { for

    (index in items.indices) { val view: View = inflate(R.layout.item) if (index != 0) { val params = view.layoutParams params.topMargin = dpToPx(16) } addView(view) } } }
  28. class CustomLayout : LinearLayout { fun display(items: List<Item>) { for

    (index in items.indices) { val view: View = inflate(R.layout.item) if (index != 0) { val space = Space(context) space.layoutParams.height = dpToPx(16) addView(space) } addView(view) } } }
  29. val scope = MainScope() val scope = viewModelScope ContextScope(SupervisorJob() +

    Dispatchers.Main) SupervisorJob() + Dispatchers.Main.immediate
  30. 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