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

Rich UI implementation examples using Jetpack ...

yokomii
September 15, 2023

Rich UI implementation examples using Jetpack Compose (Jetpack Composeを活用した強力なUI表現の実装実例)

アニメーションが再生されません🙇
動画をご参照ください→ https://www.youtube.com/watch?v=S1gYZgT_rAE

yokomii

September 15, 2023
Tweet

Other Decks in Programming

Transcript

  1. Purpose of the Session: In this session, we will introduce

    you to techniques for implementing Rich UI that is "more intuitive👀" and "evokes user emotions🔥" using Jetpack Compose.  4
  2. How do you feel about implementing a rich UI? •

    High maintenance and management costs. • Unclear implementation intent. • Difficulty in implementation (not knowing how to do it).  5
  3. > High maintenance and management costs. Rich UI isn't simply

    implemented and released, and that's the end of it. obligation to maintain the quality consistently.  6
  4. > High maintenance and management costs. Rich UI code is

    generally complex. So, - Which can lead to mental strain for engineers. - Conversely, a loss of interest (often pretending not to notice).  7
  5. > High maintenance and management costs. In Android Views, "layout

    codes (XML)" and for implementing "Rich UI codes (Java, Kotlin)" are managed separately. layout codes (XML) ʴ Rich UI codes (Java, Kotlin)  8
  6. > High maintenance and management costs. In the Layout Preview,

    changes made to Kotlin and Java-based implementations for Rich UI, which are added after defining the layout file, are not reflected. 👉 Lead to a decreased interest in the Rich UI code.  9
  7. > High maintenance and management costs. In Jetpack Compose, layout

    and Rich UI implementation are written in the same Kotlin file. Kotlin Rich UI codes layout codes  10
  8. > High maintenance and management costs. In Jetpack Compose Preview,

    you can validate the UI in a state with applied Rich UI implementation. 👉 Be more conscious of Rich UI implementation.  11
  9. > High maintenance and management costs. If you write screenshot

    tests, you can also save time on visual verification! ref: https://github.com/takahirom/roborazzi  12
  10. > Unclear implementation intent. Jetpack Compose doesn't answer even if

    you ask it about the implementation intent. Let's ask the designer!  13
  11. > Difficulty in implementation (not knowing how to do it).

    Implementation method is not clear. ↓ 
 Development difficulty is judged to be high. ↓ 
 Cost is judged to be high.  14
  12. > Difficulty in implementation (not knowing how to do it).

    Implementation method is not clear. ↓ 
 Development difficulty is judged to be high. ↓ 
 Cost is judged to be high. 👉 How about trying to learn the implementation method?  15
  13. Build AnnotatedString val annotatedString = buildAnnotatedString { append(text = "Congratulations,

    Droider ") append(text = "liked") append(text = " your post!”) check(this is AnnotatedString.Builder) }  21
  14. Styling in AnnotatedString val annotatedString = buildAnnotatedString { append(text =

    "Congratulations, Droider ") withStyle( style = SpanStyle( fontWeight = FontWeight.Bold, color = Color(0xFFF08080) ) ) { append(text = "liked") } append(text = " your post!") // ... Apply text styles.  22
  15. constructor( color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontWeight:

    FontWeight? = null, fontStyle: FontStyle? = null, fontSynthesis: FontSynthesis? = null, fontFamily: FontFamily? = null, fontFeatureSettings: String? = null, letterSpacing: TextUnit = TextUnit.Unspecified, baselineShift: BaselineShift? = null, textGeometricTransform: TextGeometricTransform? = null, localeList: LocaleList? = null, background: Color = Color.Unspecified, textDecoration: TextDecoration? = null, shadow: Shadow? = null, platformStyle: PlatformSpanStyle? = null, drawStyle: DrawStyle? = null //... `androidx.compose.ui.text.SpanStyle.kt`:  23
  16. Insert content into AnnotatedString val annotatedString = buildAnnotatedString { append(text

    = "Congratulations, Droider ") withStyle( /*...*/ ) { append(text = "liked") } appendInlineContent(id = "heart_marks") // ... Content unique id.  26
  17. Create InlineContent val inlineTextContent = InlineTextContent( placeholder = Placeholder( width

    = fontSize * 1.5, height = fontSize, placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter ) ) { TODO() }  27
  18. Create InlineContent val inlineTextContent = InlineTextContent( /*...*/ ) { Row

    { Icon( imageVector = Icons.Default.Favorite, tint = Color(0xFFF08080), modifier = Modifier.weight(weight = 1f) ) Icon( imageVector = Icons.Default.Favorite, tint = Color(0x88F08080), modifier = Modifier.weight(weight = 0.5f) ) } }  28
  19. InlineContent set to Text val annotatedString = buildAnnotatedString { //

    ... appendInlineContent(id = "heart_marks") // ... } 
 
 val inlineTextContent = InlineTextContent( /*...*/ ) { /*...*/ } Text( text = annotatedString, inlineContent = mapOf<String, InlineTextContent>( "heart_marks" to inlineTextContent ) )  29
  20. LinearGradient val colors = listOf(Color(0xFFFF9900), Color(0xFF5B8C83)) val linearGradientBrush: Brush =

    Brush.linearGradient(colors = colors) Box( modifier = Modifier .background(brush = linearGradientBrush) )  35
  21. Default start-end offsets for LinearGradient Offset(x = 0.0f, y =

    0.0f) Offset(x = 1.0F/0.0F, y = 1.0F/0.0F)  36
  22. Reverses the offset of LinearGradient val linearGradientBrush = Brush.linearGradient( colors

    = colors, start = Offset.Infinite, // Offset.Infinite = (x = 1F/0F, y = 1F/0F) end = Offset.Zero // Offset.Zero = (x = 0F, y = 0F) )  37
  23. RadialGradient and SweepGradient val radialGradientBrush = Brush.radialGradient(colors = colors) val

    sweepGradientBrush = Brush.sweepGradient(colors = colors)  39
  24. Shaders used by Gradient brushes • LinearGradient ὎ `android.graphics.LinearGradient.java` •

    RadialGradient ὎ `android.graphics.RadialGradient.java` • SweepGradient ὎ `android.graphics.SweepGradient.java`  41
  25. Composite shader brush example Create Shader A: val size =

    100.dp val sizePx = with(LocalDensity.current) { size.toPx() } val colorsA = listOf(Color(0xFFFF9900), Color(0xFFC0ECFF)) val gradientShadeA = androidx.compose.ui.graphics.LinearGradientShader( from = Offset.Zero, to = Offset(sizePx, sizePx), colors = colorsA, colorStops = null, tileMode = TileMode.Clamp )  42
  26. Composite shader brush example Create Shader B: val colorsB =

    listOf(Color(0xFF5B8C83), Color(0xFFE0DBFF)) val gradientShadeB = LinearGradientShader( from = Offset(sizePx, 0f), to = Offset(0f, sizePx), colors = colorsB, colorStops = null, tileMode = TileMode.Clamp )  43
  27. Composite shader brush example Composite shaders A and B: val

    composeShader = android.graphics.ComposeShader( gradientShadeA, gradientShadeB, PorterDuff.Mode.OVERLAY ) val shaderBrush = ShaderBrush(shader = composeShader) Box( modifier = Modifier .background(brush = shaderBrush) .size(size = size) )  44
  28. SolidColor val color = Color(0xFFFF9900) val solidColor = SolidColor(color) Row

    { Text( text = "SolidColor BG", modifier = Modifier .background(brush = solidColor) ) Text( text = "color BG", modifier = Modifier .background(color = color) ) }  46
  29. Brush: • Drawing gradients - LinearGradient - RadialGradient - SweepGradient

    • Drawing `android.graphics.Shader` - ShaderBrush • Fill with a solid color (less commonly used) - SolidColor  47
  30. Brush use cases Create Painter: val linearGradientBrush = Brush.linearGradient(colors =

    colors) val brushPainter = BrushPainter(brush = linearGradientBrush) Image( painter = brushPainter, contentDescription = "Gradient Image", // ... ) "Gradient Image" (In TalkBack).  48
  31. Brush use cases Apply to TextStyle: val linearGradientBrush = Brush.linearGradient(colors

    = colors) Text( text = "Gradient Text", style = TextStyle(brush = linearGradientBrush) )  49
  32. Modifier.drawWithContent Image( painter = painterResource(id = R.drawable.hamburger), modifier = Modifier.drawWithContent

    { drawRect(brush = linearGradientBrush) drawContent() // Draw source content. } // ... • Draw any object in front of and behind the content. • Draw oneself with `drawContent()`.  51
  33. Modifier.drawWithContent Image( painter = painterResource(id = R.drawable.hamburger), modifier = Modifier.drawWithContent

    { drawContent() drawRect(brush = linearGradientBrush) } // ... Draw in the order of declaration.  52
  34. Modifier.drawWithContent In Android View: class CustomImageView : ImageView { override

    fun onDraw(canvas: Canvas) { canvas.drawRect(0f, 0f, 100f, 100f, paint) super.onDraw(canvas) } }  53
  35. More complex drawing Image( painter = painterResource(id = R.drawable.hamburger), modifier

    = Modifier.drawWithContent { drawContent() withTransform({ rotate(45f) scale(scaleX = 0.2f, scaleY = 2.0f) }) { drawRect(brush = linearGradientBrush) } withTransform({ rotate(-45f) scale(scaleX = 0.2f, scaleY = 2.0f) }) { drawRect(brush = linearGradientBrush) } } // ... V  54
  36. Clip path (Coach Mark-like). Box( modifier = Modifier.drawWithContent { drawContent()

    clipPath( path = clipPath, clipOp = ClipOp.Difference ) { drawRect(color = Color(0x66000000)) } }, ) { Image( // ... For more: https://blog.studysapuri.jp/entry/2023/6/26/compose-spotlight  55
  37. Modifier.drawBehind Image( modifier = Modifier.drawWithContent { drawRect(brush = linearGradientBrush) drawContent()

    } Image( modifier = Modifier.drawBehind { drawRect(brush = linearGradientBrush) } Draw only behind the component.  56
  38. Modifier.drawWithCache modifier = Modifier.drawWithCache { val brush = Brush.linearGradient(colors =

    gradientColors) onDrawWithContent { drawRect(brush = brush) drawContent() } } Persist instance (e.g., Brush) in draw scope as long as the size is consistent or state objects remain unchanged; recreate when area or state changes.  57
  39. Value to change for scale animation 1.25x 1.5x 1.75x 2x

    2.25x 2.5x 2.75x 3x 0ms 1000ms  65
  40. Value to change for alpha animation a^0 a^0.12 a^0.25 a^0.37

    a^0.5 a^0.62 a^0.75 a^0.87 a^1 0ms 1000ms  66
  41. animateʓʓAsState: • ʓʓ is the name of the value to

    be generated. - Float, Int, Color, Size, Offset, etc... • Generate values based on the specified state as arguments. • Values are generated only when the state changes.  69
  42. var isLargeScale by remember { mutableStateOf(false) } val scale: State<Float>

    by animateFloatAsState( targetValue = if (isLargeScale) 4f else 1f ) Create animateFloatAsState  70
  43. Set animation value to Image scale var isLargeScale by remember

    { mutableStateOf(false) } val scale by animateFloatAsState(if (isLargeScale) 4f else 1f) Image( painter = painterResource(id = R.drawable.baby), modifier = Modifier.size(50.dp).scale(scale), // ...  71
  44. Start scale animation var isLargeScale by remember { mutableStateOf(false) }

    val scale by animateFloatAsState(if (isLargeScale) 4f else 1f) Image( painter = painterResource(id = R.drawable.baby), modifier = Modifier.size(50.dp).scale(scale), // ... LaunchdEffect(true) { isBigBaby = true }  72
  45. `androidx.compose.animation.core.AnimationAsState.kt`: @Composable fun animateFloatAsState( targetValue: Float, animationSpec: AnimationSpec<Float> = defaultAnimation,

    visibilityThreshold: Float = 0.01f, label: String = "FloatAnimation", finishedListener: ((Float) -> Unit)? = null ): State<Float> { // ... private val defaultAnimation : SpringSpec<Float> = spring<Float>()  74
  46. AnimationSpec: • Determines the amount of change • Determines the

    duration • Preset physics-based calculations  75
  47. SpringSpec specification that replicates the movement of a spring. fun

    <T> spring( dampingRatio: Float = Spring.DampingRatioNoBouncy, stiffness: Float = Spring.StiffnessMedium, visibilityThreshold: T? = null ): SpringSpec<T> = // ... • dampingRatio: Close to 1 indicates a smaller amount of vibration. • stiffness: Lesser values indicate greater stiffness.  76
  48. • The spring's momentum becomes the animation value. • The

    stretched spring state is the start point, and it ends when it returns to its original shape. • The softer the spring, the greater the momentum (faster). • As it approaches the original shape, the momentum decreases (slows down).  77
  49. • Vibration occurs when the spring reaches the endpoint. •

    Vibration can lead to motion beyond the endpoint or in the opposite direction. • When the damping ratio is 1, the forces trying to stop the spring and the forces causing it to oscillate back to its original shape balance each other out, resulting in no vibration. • The magnitude of vibration also varies with the stiffness of the spring.  78
  50. TweenSpec Duration-based animation spec. class TweenSpec<T>( val durationMillis: Int =

    DefaultDurationMillis, val delay: Int = 0, val easing: Easing = FastOutSlowInEasing ) : DurationBasedAnimationSpec<T> { // ... Easing: Curve representing change in value over time  80
  51. Who is doing the Animation drawing? • Who monitors the

    change in value? • Who is redrawing based on value changes?  83
  52. Difference in Impact Phase Modifier.scale(3f) Modifier.size(size * 3f) • Change

    visual size. • Performance low load (affects Drawing Phase). • Change actual size. • High performance load (affects Layout Phase). ref: https://developer.android.com/jetpack/compose/phases  86
  53. updateTransition Create multiple state values from a single state change.

    var attract by remember { mutableStateOf(false) } val transition : Transition<Boolean> = ɹɹɹɹɹupdateTransition(targetState = attract)  87
  54. updateTransition var attract by remember { mutableStateOf(false) } val transition

    : Transition<Boolean> = updateTransition(targetState = attract) val textColor by transition.animateColor { attract -> if (attract) Color.White else Color.Black } val bgColor by transition.animateColor { attract -> if (attract) Color.Red else Color.White } Transition.animateʓʓ is the name of the value to be generated. - Float, Int, Color, Size, Offset, etc...  88
  55. Set AnimationSpec to the child animation values var attract by

    remember { mutableStateOf(false) } val transition : Transition<Boolean> = updateTransition(targetState = attract) val textColor by transition.animateColor( transitionSpec = { tween(durationMillis = 500, delayMillis = 500) } ) { /*...*/ } val bgColor by transition.animateColor( transitionSpec = { tween(durationMillis = 1000) } ) { /*...*/ }  89
  56. Set values to Component and Setting the initial state var

    attract by remember { mutableStateOf(false) } val transition : Transition<Boolean> = updateTransition(targetState = attract) val textColor by transition.animateColor( /*...*/ ) { /*...*/ } val bgColor by transition.animateColor( /*…*/ ) { /*...*/ } LaunchdEffect(true) { attract = true } Text( color = textColor, modifier = Modifier.background(color = bgColor), // ...  90
  57. updateTransition vs. multiple animateColorAsState. val textColor by transition.animateColor { attract

    -> if (attract) Color.White else Color.Black } val bgColor by transition.animateColor { attract -> if (attract) Color.Red else Color.White } VS val textColor by animateColorAsState( if (attract) Color.White else Color.Black ) val bgColor by animateColorAsState( if (attract) Color.Red else Color.White )  92
  58. updateTransition vs. multiple animateColorAsState Using updateTransition, animations are grouped on

    the Animation Preview, and target state are clearly indicated.  93
  59. rememberInfiniteTransition Generate infinitely repeating animation state. val transition : InfiniteTransition

    = rememberInfiniteTransition() val scale by transition.animateFloat( initialValue = 1f, targetValue = 1f ʣ val color by transition.animateColor( initialValue = Color(0xFFF0AAAA), targetValue = Color(0xFFF06060) )  94
  60. rememberInfiniteTransition val transition = rememberInfiniteTransition() val scale by transition.animateFloat( initialValue

    = 1f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = scaleSpec, repeatMode = RepeatMode.Restart ) ) initial-targetValue: Values before and after the change.  95
  61. rememberInfiniteTransition val scale by transition.animateFloat( initialValue = 1f, targetValue =

    1f, animationSpec = infiniteRepeatable( animation = scaleSpec, repeatMode = RepeatMode.Restart ) ) • RepeatMode: - Restart: When the targetValue is reached, it immediately returns to the initial. - Reverse: When reaching the target value, it reverses and gradually returns to the initial.  96
  62. KeyframesSpec val scaleSpec: KeyframesSpec<Float> = keyframes { durationMillis = 1000

    delayMillis = 1000 1.2f at 250 1.0f at 500 1.2f at 750 } 1.0f 1.2f Value Time initialValue (0) 250 500 750 targetValue (1,000)  97
  63. Set infinite animation values to Component val transition : InfiniteTransition

    = rememberInfiniteTransition() val scale by transition.animateFloat( /*...*/ ) val color by transition.animateColor( /*...*/ ) Icon( imageVector = Icons.Default.NotificationsActive, tint = color, modifier = Modifier.scale(scale) // ... )  98
  64. AnimateVisibility Animate the visibility of components AnimatedVisibility(visible = isVisible) {

    Image( /*...*/ ) } // if (isVisible) { // Image( /*...*/ ) // }  101
  65. Default implementations of AnimateVisibility @Composable fun RowScope.AnimatedVisibility( enter: EnterTransition =

    fadeIn() + expandHorizontally(), exit: ExitTransition = fadeOut() + shrinkHorizontally(), // ... @Composable fun ColumnScope.AnimatedVisibility( enter: EnterTransition = fadeIn() + expandVertically(), exit: ExitTransition = fadeOut() + shrinkVertically(), // ...  102
  66. Enter-ExitTransition AnimatedVisibility( visible = isVisible, enter = fadeIn(animationSpec = tween()),

    exit = fadeOut() + slideOut() ) { // ... • enter(EnterTransition) and exit(ExitTransition) args allow configure the behavior of enter and exit animations. • Transitions can set its own AnimationSpec. • Transitions can combine multiple types.  103
  67. Crossfade When the target state changes, cross-fade animation. Crossfade( targetState

    = day, animationSpec = tween(durationMillis = 1500) ) { day -> when (day) { Day.TODAY -> TodayItem() Day.TOMORROW -> TomorrowItem() } }  105
  68. AnimatedContent AnimatedContent(targetState = contentType) { when (it) { ContentType.WEATHER ->

    WeatherItem() ContentType.SCHEDULE -> ScheduleItem() } } When the target state changes, arbitrary entry/exit animations.  106
  69. Crossfade with AnimatedContent AnimatedContent( targetState = contentType, transitionSpec = {

    fadeIn( animationSpec = tween( durationMillis = 500, delayMillis = 1000 ) ) togetherWith fadeOut( animationSpec = tween( durationMillis = 1500 ) ) using null // Disable sizing animation. } ) { // ...  107
  70. Size animation with AnimatedContent. AnimatedContent( targetState = isExpanded, modifier =

    Modifier.background(Color(0xFFC0ECFF)), ) { when(it) { true -> WeatherItem() false -> Icon( imageVector = Icons.Outlined.WbSunny, // ... ) } // ...  108
  71. Animation features: • Animation value generation features: - animateʓʓAsState -

    updateTransition - rememberInfiniteTransition • Component animation features: - AnimateVisibility - Crossfade - AnimatedContent Delving deeper🏊: https://developer.android.com/jetpack/compose/animation  109
  72. Animation ʹ "Value" that changes over time. a^0 a^0.12 a^0.25

    a^0.37 a^0.5 a^0.62 a^0.75 a^0.87 a^1 0ms 1000ms  111
  73. Find LazyListItemInfo LazyColumn(state = lazyListState) { itemsIndexed(items = newsUiStates) {

    index, news -> val alpha by remember { derivedStateOf { val visibleItemInfo: LazyListItemInfo = lazyListState.layoutInfo.visibleItemsInfo .firstOrNull { it.index == index } ?: return@derivedStateOf 0f } } } // ... derivedStateOf: side effect used to reduce unnecessary recomposition opportunities by deriving one State into another State. ref: https://developer.android.com/jetpack/compose/side-effects#derivedstateof  115
  74. Get item offset and height val alpha by remember {

    derivedStateOf { val visibleItemInfo = /*...*/ val itemHeight = visibleItemInfo.size val itemY = visibleItemInfo.offset val viewPortHeight = lazyListState.layoutInfo.viewportSize.height } }  116
  75. Calculate alpha value from item visible height val alpha by

    remember { derivedStateOf { // ... val itemVisibleHeight = when { itemHeight >= viewPortHeight -> { itemHeight } itemY > 0 -> { viewPortHeight - itemY } else -> { itemHeight - abs(itemY) } } itemVisibleHeight / itemHeight.toFloat() } }  117
  76. Set animate value to item LazyColumn(state = listState) { itemsIndexed(items

    = newsUiStates) { index, news -> val alpha by remember { /*...*/ } NewsItem( news = news, modifier = Modifier.alpha(alpha = alpha) ) } }  118
  77. Create shimmer gradient brush val gradientColors = listOf(Color(0x00FFFFFF), Color(0xCCFFFFFF)) val

    shimmerBrush = Brush.linearGradient(colors = gradientColors)  124
  78. Draw shimmer gradient brush Text( text = "Your license will

    expire soon...", modifier = Modifier.drawWithContent { drawContent() drawRect(brush = shimmerBrush) } // ...  125
  79. Draw shimmer gradient brush // ... modifier = Modifier.drawWithCache {

    val paint = Paint() onDrawWithContent { drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawContent() drawRect( brush = shimmerBrush, blendMode = BlendMode.SrcAtop ) } } } }  126
  80. Draw shimmer gradient brush // ... modifier = Modifier.drawWithCache {

    val paint = Paint() onDrawWithContent { drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawContent() drawRect( brush = shimmerBrush, blendMode = BlendMode.SrcAtop ) } } } } DrawScope.drawIntoCanvas(): directly accessing the canvas and performing drawing.  127
  81. Draw shimmer gradient brush // ... modifier = Modifier.drawWithCache {

    val paint = Paint() onDrawWithContent { drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawContent() drawRect( brush = shimmerBrush, blendMode = BlendMode.SrcAtop ) } } } } Canvas.withSaveLayer(): Create a layer to enable BlendMode.  128
  82. Draw shimmer gradient brush // ... modifier = Modifier.drawWithCache {

    val paint = Paint() onDrawWithContent { drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawContent() drawRect( brush = shimmerBrush, blendMode = BlendMode.SrcAtop ) } } } } BlendMode.SrcAtop: Draw only on overlapping areas. ref: https://developer.android.com/reference/android/graphics/BlendMode  129
  83. Select animation features • Animation value generation features: - animateʓʓAsState

    - updateTransition - rememberInfiniteTransition👈 • Component animation features: - AnimateVisibility - Crossfade - AnimatedContent  131
  84. Create shimmer animation value val transition = rememberInfiniteTransition() val progress

    by transition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 2000), repeatMode = RepeatMode.Restart ) )  132
  85. Calculate shimmer gradient brush offset Text( modifier = Modifier.drawWithCache {

    val simmerBrush = Brush.linearGradient( colors = gradientColors, start = Offset( x = size.width * progress, y = size.height * progress ), end = Offset( x = size.width * progress * 2, y = size.height * progress * 2 ) ) onDrawWithContent { // ... drawContent() drawRect(brush = simmerBrush, blendMode = BlendMode.SrcAtop)  133
  86. Calculate shimmer gradient brush offset Text( modifier = Modifier.drawWithCache {

    val simmerBrush = Brush.linearGradient( colors = gradientColors, start = Offset( x = size.width * progress, y = size.height * progress ), end = Offset( x = size.width * progress * 2, y = size.height * progress * 2 ) ) onDrawWithContent { // ... drawContent() drawRect(brush = simmerBrush, blendMode = BlendMode.SrcAtop) Text Text 0ms 2000ms Text Text Text  134
  87. Create updateTransition val transitionState = remember { MutableTransitionState(false).apply { targetState

    = true } } val transition = updateTransition(transitionState) // LaunchedEffect(true) { transitionState.targetState = true } MutableTransitionState: By setting a state that is different from the initial state, the animation can be started at the first composition.  139
  88. Create animation values val current = 0.75f val colors =

    listOf(Color(0xFF3B91C4), Color(0xFFF84CAD)) val progressTextColor = Color( ColorUtils.blendARGB(colors[0].toArgb(), colors[1].toArgb(), current) ) val textColor by transition.animateColor { if (it) progressTextColor else colors[0] } val progress by transition.animateInt { if (it) (100 * current).toInt() else 0 } val barAngle by transition.animateFloat { if (it) 360 * current else 0f }  140
  89. Create progress Text val progressString = buildAnnotatedString { append(progress.toString()) withStyle(style

    = SpanStyle(fontSize = 40.sp)) { append("%") } } Box(contentAlignment = Alignment.Center) { Text( text = progressString, style = TextStyle(fontSize = 80.sp, color = textColor), ) // ...  141
  90. Draw progress bar background val density = LocalDensity.current Box(modifier =

    Modifier.size(400.dp) .drawWithCache { val paint = Paint() val strokeWidth = with(density) { 80.dp.toPx() } onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle( color = Color.LightGray, radius = size.minDimension / 2 - strokeWidth / 2, style = Stroke(width = strokeWidth) ) } // ...  142
  91. Draw progress bar background val density = LocalDensity.current Box(modifier =

    Modifier.size(400.dp) .drawWithCache { val paint = Paint() val strokeWidth = with(density) { 80.dp.toPx() } onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle( color = Color.LightGray, radius = size.minDimension / 2 - strokeWidth / 2, style = Stroke(width = strokeWidth) ) } // ...  143
  92. Draw progress bar .drawWithCache { // ... val materBrush =

    Brush.sweepGradient(colors = colors) onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle(/* ... */) rotate(degrees = -90f) { drawArc( brush = materBrush, startAngle = 0f, sweepAngle = barAngle, useCenter = true, blendMode = BlendMode.SrcAtop ) } // ...  144
  93. Draw progress bar .drawWithCache { // ... val materBrush =

    Brush.sweepGradient(colors = colors) onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle(/* ... */) rotate(degrees = -90f) { drawArc( brush = materBrush, startAngle = 0f, sweepAngle = barAngle, useCenter = true, blendMode = BlendMode.SrcAtop ) } // ...  145
  94. Draw progress bar .drawWithCache { // ... val materBrush =

    Brush.sweepGradient(colors = colors) onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle(/* ... */) rotate(degrees = -90f) { drawArc( brush = materBrush, startAngle = 0f, sweepAngle = barAngle, useCenter = true, blendMode = BlendMode.SrcAtop ) } // ...  146
  95. Draw progress bar .drawWithCache { // ... val materBrush =

    Brush.sweepGradient(colors = colors) onDrawWithContent { drawContent() drawIntoCanvas { canvas -> canvas.withSaveLayer(bounds = size.toRect(), paint) { drawCircle(/* ... */) rotate(degrees = -90f) { drawArc( brush = materBrush, startAngle = 0f, sweepAngle = barAngle, useCenter = true, blendMode = BlendMode.SrcAtop ) } // ...  147
  96. AGSL • Abbreviation for "Android Graphics Shading Language". • A

    language for writing shaders that operate on the Android graphics rendering system. • Feature added in Android 13, and it's only available on subsequent OS versions.  150
  97. Use AGSL with Jetpack Compose val shader = RuntimeShader(WAVE_COLOR_SHADER) val

    shaderBrush = ShaderBrush(shader = shader) val time by produceState(0f) { while (true) { withFrameMillis { value = it / 1000f } } } shader.setFloatUniform("time", time) Image( painter = painterResource(id = R.drawable.alien), modifier = Modifier .onSizeChanged { size -> shader.setFloatUniform("resolution", size.width.toFloat(), size.height.toFloat()) } .background(brush = shaderBrush) // ... )  152
  98. Use AGSL + RenderEffect with Jetpack Compose Image( painter =

    painterResource(id = R.drawable.alien), modifier = Modifier .graphicsLayer { shader.setFloatUniform("resolution", size.width, size.height) renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "content") .asComposeRenderEffect() } // ... @Language("AGSL") private val WAVE_COLOR_SHADER = """ uniform float time; uniform vec2 resolution; uniform shader content; vec4 main(float2 fragCoord) { vec2 uv = fragCoord.xy / resolution.xy; float offset = cos(time * 3.0 + uv.y * 3.0) * 0.1; vec3 color = vec3(uv.x, uv.y + offset, 0.5 + 0.5 * sin(time)); return mix(vec4(color, 1.0), content.eval(fragCoord), 0.7); } """.trimIndent()  154
  99. Rich UI in the Android View era (in terms of

    cost). ↓ Affordable UI in Jetpack Compose?🤔  157
  100. Let's not keep the benefits of Jetpack Compose limited to

    just engineers; sharing it with the team! Who knows, it might spark new UI implementation ideas?  158
  101. The powerful UI implementation capabilities offered by Jetpack Compose are

    likely to bring positive effects to the team beyond just improving engineers' productivity👍  159