Slide 1

Slide 1 text

Rich UI implementation examples using Jetpack Compose (Jetpack ComposeΛ׆༻ͨ͠ڧྗͳUIදݱͷ࣮૷࣮ྫ) DroidKaigi 2023 Chipmunk - 2023/09/15 14:00-14:40 @yokomii

Slide 2

Slide 2 text

About Me: yokomii Android Engineer at Nikkei inc @iimokoy [email protected]  2

Slide 3

Slide 3 text

 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

> 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

Slide 7

Slide 7 text

> 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

Slide 8

Slide 8 text

> 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

Slide 9

Slide 9 text

> 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

Slide 10

Slide 10 text

> 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

Slide 11

Slide 11 text

> 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

Slide 12

Slide 12 text

> 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

Slide 13

Slide 13 text

> Unclear implementation intent. Jetpack Compose doesn't answer even if you ask it about the implementation intent. Let's ask the designer!  13

Slide 14

Slide 14 text

> 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

Slide 15

Slide 15 text

> 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

Slide 16

Slide 16 text

Let's start implementing a Rich UIʂ  16

Slide 17

Slide 17 text

Session Flow: 1. Features for implementing Rich UI 2. Examples of Rich UI implementations  17

Slide 18

Slide 18 text

Feature 1: AnnotatedString  18

Slide 19

Slide 19 text

AnnotatedString Enrich the following:  19

Slide 20

Slide 20 text

Instantiate AnnotatedString val annotatedString: AnnotatedString = buildAnnotatedString {} In Android View: val spannable = SpannableStringBuilder("")  20

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

AnnotatedString set to Text val annotatedString = buildAnnotatedString { /*...*/ } Text( text = annotatedString )  24

Slide 25

Slide 25 text

Result of AnnotatedString: ▶ Run  25

Slide 26

Slide 26 text

Insert content into AnnotatedString val annotatedString = buildAnnotatedString { append(text = "Congratulations, Droider ") withStyle( /*...*/ ) { append(text = "liked") } appendInlineContent(id = "heart_marks") // ... Content unique id.  26

Slide 27

Slide 27 text

Create InlineContent val inlineTextContent = InlineTextContent( placeholder = Placeholder( width = fontSize * 1.5, height = fontSize, placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter ) ) { TODO() }  27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

InlineContent set to Text val annotatedString = buildAnnotatedString { // ... appendInlineContent(id = "heart_marks") // ... } 
 
 val inlineTextContent = InlineTextContent( /*...*/ ) { /*...*/ } Text( text = annotatedString, inlineContent = mapOf( "heart_marks" to inlineTextContent ) )  29

Slide 30

Slide 30 text

Result of InlineContent: ▶ Run  30

Slide 31

Slide 31 text

Try next 👉  31

Slide 32

Slide 32 text

Feature 2: Brush  32

Slide 33

Slide 33 text

Brush subclasses: ɾLinearGradient ɾRadialGradient ɾSweepGradient ɾShaderBrush ɾSolidColor  33

Slide 34

Slide 34 text

LinearGradient  34

Slide 35

Slide 35 text

LinearGradient val colors = listOf(Color(0xFFFF9900), Color(0xFF5B8C83)) val linearGradientBrush: Brush = Brush.linearGradient(colors = colors) Box( modifier = Modifier .background(brush = linearGradientBrush) )  35

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Other LinearGradient  38

Slide 39

Slide 39 text

RadialGradient and SweepGradient val radialGradientBrush = Brush.radialGradient(colors = colors) val sweepGradientBrush = Brush.sweepGradient(colors = colors)  39

Slide 40

Slide 40 text

ShaderBrush: Drawing `android.graphics.Shader`. android.graphics.Shader: • With Android platform API • Drawing textures, gradients, patterns, etc...  40

Slide 41

Slide 41 text

Shaders used by Gradient brushes • LinearGradient ὎ `android.graphics.LinearGradient.java` • RadialGradient ὎ `android.graphics.RadialGradient.java` • SweepGradient ὎ `android.graphics.SweepGradient.java`  41

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Result of ShaderBlush: ▶ Run  45

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Brush: • Drawing gradients - LinearGradient - RadialGradient - SweepGradient • Drawing `android.graphics.Shader` - ShaderBrush • Fill with a solid color (less commonly used) - SolidColor  47

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Brush use cases Apply to TextStyle: val linearGradientBrush = Brush.linearGradient(colors = colors) Text( text = "Gradient Text", style = TextStyle(brush = linearGradientBrush) )  49

Slide 50

Slide 50 text

Feature 3: Modifier.drawWithContent  50

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Modifier.drawWithContent Image( painter = painterResource(id = R.drawable.hamburger), modifier = Modifier.drawWithContent { drawContent() drawRect(brush = linearGradientBrush) } // ... Draw in the order of declaration.  52

Slide 53

Slide 53 text

Modifier.drawWithContent In Android View: class CustomImageView : ImageView { override fun onDraw(canvas: Canvas) { canvas.drawRect(0f, 0f, 100f, 100f, paint) super.onDraw(canvas) } }  53

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Modifier.drawBehind Image( modifier = Modifier.drawWithContent { drawRect(brush = linearGradientBrush) drawContent() } Image( modifier = Modifier.drawBehind { drawRect(brush = linearGradientBrush) } Draw only behind the component.  56

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Feature 4: Animation  58

Slide 59

Slide 59 text

What is "Animation"?  59

Slide 60

Slide 60 text

Animation ʹ "Value" that changes over time.  60

Slide 61

Slide 61 text

Is this Animation? 3x 0ms 1000ms  61

Slide 62

Slide 62 text

Is this Animation? 3x 0ms 1000ms seisi_baby.gif  62

Slide 63

Slide 63 text

Is this Animation? 3x 0ms 1000ms  63

Slide 64

Slide 64 text

This is Animation 1.25x 1.5x 1.75x 2x 2.25x 2.5x 2.75x 3x 0ms 1000ms  64

Slide 65

Slide 65 text

Value to change for scale animation 1.25x 1.5x 1.75x 2x 2.25x 2.5x 2.75x 3x 0ms 1000ms  65

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Animation ʹ "Value" that changes over time.  67

Slide 68

Slide 68 text

Animation value generation features  68

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

var isLargeScale by remember { mutableStateOf(false) } val scale: State by animateFloatAsState( targetValue = if (isLargeScale) 4f else 1f ) Create animateFloatAsState  70

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

Result of animateFloatAsState (Times 0.2x): ▶ Run  73

Slide 74

Slide 74 text

`androidx.compose.animation.core.AnimationAsState.kt`: @Composable fun animateFloatAsState( targetValue: Float, animationSpec: AnimationSpec = defaultAnimation, visibilityThreshold: Float = 0.01f, label: String = "FloatAnimation", finishedListener: ((Float) -> Unit)? = null ): State { // ... private val defaultAnimation : SpringSpec = spring()  74

Slide 75

Slide 75 text

AnimationSpec: • Determines the amount of change • Determines the duration • Preset physics-based calculations  75

Slide 76

Slide 76 text

SpringSpec specification that replicates the movement of a spring. fun spring( dampingRatio: Float = Spring.DampingRatioNoBouncy, stiffness: Float = Spring.StiffnessMedium, visibilityThreshold: T? = null ): SpringSpec = // ... • dampingRatio: Close to 1 indicates a smaller amount of vibration. • stiffness: Lesser values indicate greater stiffness.  76

Slide 77

Slide 77 text

• 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

Slide 78

Slide 78 text

• 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

Slide 79

Slide 79 text

SpringSpec examples: ▶ Run  79

Slide 80

Slide 80 text

TweenSpec Duration-based animation spec. class TweenSpec( val durationMillis: Int = DefaultDurationMillis, val delay: Int = 0, val easing: Easing = FastOutSlowInEasing ) : DurationBasedAnimationSpec { // ... Easing: Curve representing change in value over time  80

Slide 81

Slide 81 text

Easing Curves From: https://easings.net/  81

Slide 82

Slide 82 text

TweenSpec examples. ▶ Run  82

Slide 83

Slide 83 text

Who is doing the Animation drawing? • Who monitors the change in value? • Who is redrawing based on value changes?  83

Slide 84

Slide 84 text

Who is doing the Animation drawing? Compose Runtime Modifier.scale(1f) Modifier.scale(3f) ᶃ Detect value changes. ᶄRedraw.  84

Slide 85

Slide 85 text

Who is doing the Animation drawing? Modifier.scale(1f) Modifier.scale(1.01f) Modifier.scale(1.02f) Modifier.scale(1.03f) Animation Value State Frame A Frame B Rendering frame  85

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

updateTransition Create multiple state values from a single state change. var attract by remember { mutableStateOf(false) } val transition : Transition = ɹɹɹɹɹupdateTransition(targetState = attract)  87

Slide 88

Slide 88 text

updateTransition var attract by remember { mutableStateOf(false) } val transition : Transition = 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

Slide 89

Slide 89 text

Set AnimationSpec to the child animation values var attract by remember { mutableStateOf(false) } val transition : Transition = updateTransition(targetState = attract) val textColor by transition.animateColor( transitionSpec = { tween(durationMillis = 500, delayMillis = 500) } ) { /*...*/ } val bgColor by transition.animateColor( transitionSpec = { tween(durationMillis = 1000) } ) { /*...*/ }  89

Slide 90

Slide 90 text

Set values to Component and Setting the initial state var attract by remember { mutableStateOf(false) } val transition : Transition = 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

Slide 91

Slide 91 text

Result of updateTransition: ▶ Run transitionupdate.gif  91

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

updateTransition vs. multiple animateColorAsState Using updateTransition, animations are grouped on the Animation Preview, and target state are clearly indicated.  93

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

KeyframesSpec val scaleSpec: KeyframesSpec = 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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

Result of rememberInfiniteTransition: ▶ Run  99

Slide 100

Slide 100 text

Component animation features  100

Slide 101

Slide 101 text

AnimateVisibility Animate the visibility of components AnimatedVisibility(visible = isVisible) { Image( /*...*/ ) } // if (isVisible) { // Image( /*...*/ ) // }  101

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

AnimatedVisibility examples:  104

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

AnimatedContent AnimatedContent(targetState = contentType) { when (it) { ContentType.WEATHER -> WeatherItem() ContentType.SCHEDULE -> ScheduleItem() } } When the target state changes, arbitrary entry/exit animations.  106

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

Size animation with AnimatedContent. AnimatedContent( targetState = isExpanded, modifier = Modifier.background(Color(0xFFC0ECFF)), ) { when(it) { true -> WeatherItem() false -> Icon( imageVector = Icons.Outlined.WbSunny, // ... ) } // ...  108

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

Example 1: Animation by user action  110

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

"Value" that changes over time. ≈ "Value" that changes based on user actions.  112

Slide 113

Slide 113 text

List scroll animation  113

Slide 114

Slide 114 text

Set LazyListState val lazyListState: LazyListState = rememberLazyListState() LazyColumn(state = lazyListState) {}  114

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

Result of list scroll animation: ▶ Run  119

Slide 120

Slide 120 text

Touch action animation  120

Slide 121

Slide 121 text

Example 2: Shimmer Effect  121

Slide 122

Slide 122 text

Shimmer Effect  122

Slide 123

Slide 123 text

Draw gradient💪  123

Slide 124

Slide 124 text

Create shimmer gradient brush val gradientColors = listOf(Color(0x00FFFFFF), Color(0xCCFFFFFF)) val shimmerBrush = Brush.linearGradient(colors = gradientColors)  124

Slide 125

Slide 125 text

Draw shimmer gradient brush Text( text = "Your license will expire soon...", modifier = Modifier.drawWithContent { drawContent() drawRect(brush = shimmerBrush) } // ...  125

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

Animating gradient💪  130

Slide 131

Slide 131 text

Select animation features • Animation value generation features: - animateʓʓAsState - updateTransition - rememberInfiniteTransition👈 • Component animation features: - AnimateVisibility - Crossfade - AnimatedContent  131

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

Result of Shimmer Effect: ▶ Run  135

Slide 136

Slide 136 text

Example 3: Circular Progress Bar  136

Slide 137

Slide 137 text

Circular Progress Bar  137

Slide 138

Slide 138 text

Animation Targets • Text color • Progress text • Progress bar  138

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

✨Circular Progress Bar✨ ▶ Run  148

Slide 149

Slide 149 text

Future of Rich UI🚀  149

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

AGSL code (in .Kt file)  151

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

Result of AGSL with Jetpack Compose: ▶ Run  153

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

Result of Use AGSL + RenderEffect with Jetpack Compose: ▶ Run agsl2.gif  155

Slide 156

Slide 156 text

Closing  156

Slide 157

Slide 157 text

Rich UI in the Android View era (in terms of cost). ↓ Affordable UI in Jetpack Compose?🤔  157

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

The powerful UI implementation capabilities offered by Jetpack Compose are likely to bring positive effects to the team beyond just improving engineers' productivity👍  159

Slide 160

Slide 160 text

Have a nice Jetpack Compose life👋  160