Slide 1

Slide 1 text

Creative UIs with Compose Chris Horner

Slide 2

Slide 2 text

We ’ re going to analyse and build a complex UI But first…

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

I arrange rectangles. - A designer I worked with

Slide 6

Slide 6 text

- A designer I worked with I arrange rectangles. Sometimes, if I want to get fancy, I round the corners.

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

UI frameworks are built around rectangles

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Column { Box() Row { Box() Box() } }

Slide 13

Slide 13 text

Flat design Easy for a designer to put together Simple for a developer to build Low effort to read Renders quickly Responsive Dark mode

Slide 14

Slide 14 text

Boring 😴 is good

Slide 15

Slide 15 text

Boring 😴 is good Phone calls Customer support Maps / navigation Email But… have we gone too far?

Slide 16

Slide 16 text

Let ’ s go back in time…

Slide 17

Slide 17 text

2009

Slide 18

Slide 18 text

appleinsider.com

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

skins.webamp.org

Slide 24

Slide 24 text

gameuidatabase.com

Slide 25

Slide 25 text

©ATLUS ©SEGA

Slide 26

Slide 26 text

Credit to Miika Laaksonen

Slide 27

Slide 27 text

garystormsongs.com

Slide 28

Slide 28 text

©ATLUS ©SEGA

Slide 29

Slide 29 text

©ATLUS ©SEGA

Slide 30

Slide 30 text

How could we build this with Compose UI in Kotlin?

Slide 31

Slide 31 text

©ATLUS ©SEGA

Slide 32

Slide 32 text

©ATLUS ©SEGA

Slide 33

Slide 33 text

©ATLUS ©SEGA

Slide 34

Slide 34 text

©ATLUS ©SEGA

Slide 35

Slide 35 text

©ATLUS ©SEGA

Slide 36

Slide 36 text

©ATLUS ©SEGA

Slide 37

Slide 37 text

©ATLUS ©SEGA

Slide 38

Slide 38 text

Background

Slide 39

Slide 39 text

©ATLUS ©SEGA

Slide 40

Slide 40 text

val PersonaRed = Color(0xFFC41001) Box( modifier = Modifier .fillMaxSize() .background(color = PersonaRed) ) { } ©ATLUS ©SEGA

Slide 41

Slide 41 text

val PersonaRed = Color(0xFFC41001) Box( modifier = Modifier .fillMaxSize() .background(color = PersonaRed) ) { } ©ATLUS ©SEGA

Slide 42

Slide 42 text

Box( m​ odifier = Modifier .fillMaxSize() .background(color = PersonaRed) ) { I​ mage( painter = painterResource(R.drawable.bg_splatter), c​ ontentDescription = null, contentScale = ContentScale.FillWidth, m​ odifier = M​ odifier.statusBarsPadding() ​)​ } ©ATLUS ©SEGA

Slide 43

Slide 43 text

Box( m​ odifier = Modifier .fillMaxSize() .background(color = PersonaRed) ) { I​ mage( painter = painterResource(R.drawable.bg_splatter), c​ ontentDescription = null, contentScale = ContentScale.FillWidth, m​ odifier = M​ odifier.statusBarsPadding() ​)​ Image( painter = painterResource(R.drawable.logo_im), contentDescription = null, modifier = Modifier .height(100.dp) .statusBarsPadding() .offset(x = 8.dp, y = (-4).dp), ) } ©ATLUS ©SEGA

Slide 44

Slide 44 text

Textbox ©ATLUS ©SEGA

Slide 45

Slide 45 text

©ATLUS ©SEGA

Slide 46

Slide 46 text

©ATLUS ©SEGA

Slide 47

Slide 47 text

Sup? ©ATLUS ©SEGA

Slide 48

Slide 48 text

Sup? ©ATLUS ©SEGA

Slide 49

Slide 49 text

romainguy / v9 ©ATLUS ©SEGA

Slide 50

Slide 50 text

romainguy / v9 ©ATLUS ©SEGA

Slide 51

Slide 51 text

romainguy / v9 ©ATLUS ©SEGA

Slide 52

Slide 52 text

romainguy / v9 ©ATLUS ©SEGA

Slide 53

Slide 53 text

fun outerBox(): Shape = GenericShape { size -> } ©ATLUS ©SEGA

Slide 54

Slide 54 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) } ©ATLUS ©SEGA

Slide 55

Slide 55 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) lineTo(size.width, 0f) } ©ATLUS ©SEGA

Slide 56

Slide 56 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) lineTo(size.width, 0f) lineTo(size.width - 23, size.height) } ©ATLUS ©SEGA

Slide 57

Slide 57 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) lineTo(size.width, 0f) lineTo(size.width - 23, size.height) lineTo(0f, size.height - 8) } ©ATLUS ©SEGA

Slide 58

Slide 58 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) lineTo(size.width, 0f) lineTo(size.width - 23, size.height) lineTo(0f, size.height - 8) close() } ©ATLUS ©SEGA

Slide 59

Slide 59 text

fun outerBox(): Shape = GenericShape { size -> moveTo(31.7, 3.1) lineTo(size.width, 0f) lineTo(size.width - 23, size.height) lineTo(0f, size.height - 8) close() } ©ATLUS ©SEGA

Slide 60

Slide 60 text

fun Density.outerBox(): Shape = GenericShape { s -> moveTo(31.7.dp.toPx(), 3.1.dp.toPx()) lineTo(size.width, 0f) lineTo(size.width - 23.dp.toPx(), size.height) lineTo(0f, size.height - 8.dp.toPx( )​ ) c​ lose() } ©ATLUS ©SEGA

Slide 61

Slide 61 text

fun Density.outerBox(): Shape = GenericShape { size -> moveTo(31.7.dp.toPx(), 3.1.dp.toPx()) lineTo(size.width, 0f) lineTo(size.width - 23.dp.toPx(), size.height) lineTo(0f, size.height - 8.dp.toPx()) ​c​ lose( )​ } fun Density.innerBox(): Shape = GenericShape { size -> moveTo(33.dp.toPx(), 7.7.dp.toPx()) lineTo(size.width - 13.dp.toPx(), 3.7.dp.toPx()) lineTo(size.width - 25.7.dp.toPx(), size.height - 4.6.dp.toPx( lineTo(20.4.dp.toPx(), size.height - 12.dp.toPx()) close() } ©ATLUS ©SEGA

Slide 62

Slide 62 text

c5inco / shape-composer-figma

Slide 63

Slide 63 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, ) ©ATLUS ©SEGA

Slide 64

Slide 64 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, modifier = Modifier .drawBehind { val outerBox = outerBox() val innerBox = innerBox() } ) ©ATLUS ©SEGA

Slide 65

Slide 65 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, modifier = Modifier .drawBehind { val outerBox = outerBox() val innerBox = innerBox() drawShape(outerBox, color = Color.White) drawShape(innerBox, color = Color.Black) } ) ©ATLUS ©SEGA

Slide 66

Slide 66 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, modifier = Modifier .drawBehind { val outerBox = Outline(outerBox()) val innerBox = Outline(innerBox()) drawOutline(outerBox, color = Color.White) drawOutline(innerBox, color = Color.Black) } ) ©ATLUS ©SEGA

Slide 67

Slide 67 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, modifier = Modifier .drawBehind { val outerBox = Outline(outerBox()) val innerBox = Outline(innerBox()) drawOutline(outerBox, color = Color.White) drawOutline(innerBox, color = Color.Black) } .padding( ... ) ) ©ATLUS ©SEGA

Slide 68

Slide 68 text

Text( text = message.text, style = MaterialTheme.typography.bodyMedium, color = Color.White, fontFamily = OptimaNova, modifier = Modifier .drawBehind { val outerBox = Outline(outerBox()) val innerBox = Outline(innerBox()) drawOutline(outerBox, color = Color.White) drawOutline(innerBox, color = Color.Black) } .padding( ... ) ) ©ATLUS ©SEGA

Slide 69

Slide 69 text

©ATLUS ©SEGA

Slide 70

Slide 70 text

Avatar ©ATLUS ©SEGA

Slide 71

Slide 71 text

©ATLUS ©SEGA

Slide 72

Slide 72 text

Row { TextBox() } ©ATLUS ©SEGA

Slide 73

Slide 73 text

Row { Image( painter = painterResource(R.drawable.ann), contentDescription = null, ) TextBox() } ©ATLUS ©SEGA

Slide 74

Slide 74 text

Box( modifier = Modifier .drawBehind { drawOutline(avatarBlackBox(), Color.Black) drawOutline(avatarWhiteBox(), Color.White) drawOutline(avatarColoredBox(), pink) } ) { Image( painter = painterResource(R.drawable.ann), contentDescription = null, ) } ©ATLUS ©SEGA

Slide 75

Slide 75 text

Box( modifier = Modifier .drawBehind { drawOutline(avatarBlackBox(), Color.Black) drawOutline(avatarWhiteBox(), Color.White) drawOutline(avatarColoredBox(), pink) drawOutline (​ avatarClipBox(), Color.Magenta) } ) { Image( painter = painterResource(R.drawable.ann), contentDescription = null, ) } ©ATLUS ©SEGA

Slide 76

Slide 76 text

Box( modifier = Modifier .drawBehind { drawOutline(avatarBlackBox(), Color.Black) drawOutline(avatarWhiteBox(), Color.White) drawOutline(avatarColoredBox(), pink) } .clip (​ avatarClipBox( )​ ) ) { Image( painter = painterResource(R.drawable.ann), contentDescription = null, ) } ©ATLUS ©SEGA

Slide 77

Slide 77 text

Message layout ©ATLUS ©SEGA

Slide 78

Slide 78 text

Row { Avatar() TextBox() } ©ATLUS ©SEGA

Slide 79

Slide 79 text

Row { Avatar() Stem()? TextBox() } ©ATLUS ©SEGA

Slide 80

Slide 80 text

©ATLUS ©SEGA

Slide 81

Slide 81 text

©ATLUS ©SEGA

Slide 82

Slide 82 text

Row { Avatar() TextBox() } ©ATLUS ©SEGA

Slide 83

Slide 83 text

Row { Avatar() TextBox( modifier = Modifier.offset(x = (-40).dp) ) } ©ATLUS ©SEGA

Slide 84

Slide 84 text

Row { Avatar() TextBox( modifier = Modifier.offset(x = (-40).dp) ) } ©ATLUS ©SEGA

Slide 85

Slide 85 text

Row { Avatar() TextBox() } ©ATLUS ©SEGA

Slide 86

Slide 86 text

Row { Avatar() TextBox() } ©ATLUS ©SEGA

Slide 87

Slide 87 text

Layout( content = { // Composables ... } ) { measurables, constraints -> }

Slide 88

Slide 88 text

Layout( content = { // Composables ... } ) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } }

Slide 89

Slide 89 text

Layout( content = { // Composables ... } ) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } layout(width, height) { placeables.forEach { it.place(x, y) } } }

Slide 90

Slide 90 text

Layout( content = { // Composables ... } ) { measurables, constraints -> val placeables = measurables.map { it.measure(constraints) } layout(width, height) { placeables.forEach { it.place(x, y) } } } ©ATLUS ©SEGA

Slide 91

Slide 91 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> } ©ATLUS ©SEGA

Slide 92

Slide 92 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> } ©ATLUS ©SEGA

Slide 93

Slide 93 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) } ©ATLUS ©SEGA

Slide 94

Slide 94 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp } ©ATLUS ©SEGA

Slide 95

Slide 95 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp } ©ATLUS ©SEGA

Slide 96

Slide 96 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) } ©ATLUS ©SEGA

Slide 97

Slide 97 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) val width = avatarPlaceable.width + textPlaceable.width - 18.dp val height = maxOf(avatarPlaceable.height, textPlaceable.height) } ©ATLUS ©SEGA

Slide 98

Slide 98 text

Layout( content = { Avatar() TextBox() } ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) val width = avatarPlaceable.width + textPlaceable.width - 18.dp val height = maxOf(avatarPlaceable.height, textPlaceable.height) layout(width, height) { avatarPlaceable.place(0, 0) } } ©ATLUS ©SEGA

Slide 99

Slide 99 text

} ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) val width = avatarPlaceable.width + textPlaceable.width - 18.dp val height = maxOf(avatarPlaceable.height, textPlaceable.height) layout(width, height) { avatarPlaceable.place(0, 0) val textBoxX = avatarPlaceable.width - textBoxOverlap val textBoxY = if (textPlaceable.height > avatarPlaceable.height) 0 else height - textPlaceable.height textPlaceable.place(textBoxX, textBoxY) } } ©ATLUS ©SEGA

Slide 100

Slide 100 text

} ) { (avatarMeasurable, textMeasurable), constraints -> val avatarPlaceable = avatarMeasurable.measure(constraints) val textMaxWidth = constraints.maxWidth - avatarPlaceable.width + 18.dp val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) val width = avatarPlaceable.width + textPlaceable.width - 18.dp val height = maxOf(avatarPlaceable.height, textPlaceable.height) layout(width, height) { avatarPlaceable.place(0, 0) val textBoxX = avatarPlaceable.width - textBoxOverlap val textBoxY = if (textPlaceable.height > avatarPlaceable.height) 0 else height - textPlaceable.height textPlaceable.place(textBoxX, textBoxY) } } ©ATLUS ©SEGA

Slide 101

Slide 101 text

LazyColumn( verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = WindowInsets.systemBars .add(WindowInsets(top = 100.dp, bottom = 100.dp)) .asPaddingValues(), modifier = Modifier.fillMaxSize(), ) ©ATLUS ©SEGA

Slide 102

Slide 102 text

Line ©ATLUS ©SEGA

Slide 103

Slide 103 text

©ATLUS ©SEGA

Slide 104

Slide 104 text

Centres itself to the first item ©ATLUS ©SEGA

Slide 105

Slide 105 text

Centres itself to the first item Alternates left and right by a random offset ©ATLUS ©SEGA

Slide 106

Slide 106 text

Centres itself to the first item Alternates left and right by a random offset Centres itself to the last item ©ATLUS ©SEGA

Slide 107

Slide 107 text

Centres itself to the first item Alternates left and right by a random offset Centres itself to the last item Jumps to a final position before animating ©ATLUS ©SEGA

Slide 108

Slide 108 text

Modelling data Modelling data ©ATLUS ©SEGA

Slide 109

Slide 109 text

Modelling data enum class Sender( @DrawableRes val image: Int, val color: Color, ) { Kasumi(R.drawable.kasumi, Color(0xFFD53359)), Ryuji(R.drawable.ryuji, Color(0xFFF0EA40)), Ann(R.drawable.ann, Color(0xFFFE93C9)), } Modelling data ©ATLUS ©SEGA

Slide 110

Slide 110 text

Modelling data Modelling data data class Message( val sender: Sender, val text: String, ) enum class Sender( @DrawableRes val image: Int, val color: Color, ) { Kasumi(R.drawable.kasumi, Color(0xFFD53359)), Ryuji(R.drawable.ryuji, Color(0xFFF0EA40)), Ann(R.drawable.ann, Color(0xFFFE93C9)), } ©ATLUS ©SEGA

Slide 111

Slide 111 text

Modelling data Modelling data data class LineCoordinates( val leftPoint: Offset, val rightPoint: Offset, ) ©ATLUS ©SEGA

Slide 112

Slide 112 text

Modelling data Modelling data data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) data class LineCoordinates( val leftPoint: Offset, val rightPoint: Offset, ) ©ATLUS ©SEGA

Slide 113

Slide 113 text

Where to store these data? ViewModel is one option UI state is another option ©ATLUS ©SEGA

Slide 114

Slide 114 text

Where to store these data? ViewModel is one option UI state is another option val scrollState = rememberScrollState() val pagerState = rememberPagerState() val textState = rememberTextFieldState() ©ATLUS ©SEGA

Slide 115

Slide 115 text

ViewModel UI state Current list of messages Entries Background line coordinates Item animation progress

Slide 116

Slide 116 text

ViewModel UI state LazyColumn

Slide 117

Slide 117 text

@Composable fun rememberTranscriptState(): TranscriptState { val density = LocalDensity.current val coroutineScope = rememberCoroutineScope() return remember(density) { TranscriptState(density, coroutineScope) } }

Slide 118

Slide 118 text

@Composable fun rememberTranscriptState(): TranscriptState { val density = LocalDensity.current val coroutineScope = rememberCoroutineScope() return remember(density) { TranscriptState(density, coroutineScope) } }

Slide 119

Slide 119 text

@Stable class TranscriptState internal constructor( private val density: Density, private val coroutineScope: CoroutineScope, ) { private val _entries = mutableStateOf> (emptyList()) val entries: List by _entries }

Slide 120

Slide 120 text

messageText [ ] val entries: List by _entries ©ATLUS ©SEGA

Slide 121

Slide 121 text

messageText [ ] messageText messageText [ ] , val entries: List by _entries ©ATLUS ©SEGA

Slide 122

Slide 122 text

messageText [ ] messageText messageText [ ] , messageText messageText messageText [ ] , , val entries: List by _entries ©ATLUS ©SEGA

Slide 123

Slide 123 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) ©ATLUS ©SEGA

Slide 124

Slide 124 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) ©ATLUS ©SEGA

Slide 125

Slide 125 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) ©ATLUS ©SEGA

Slide 126

Slide 126 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) ©ATLUS ©SEGA

Slide 127

Slide 127 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) ©ATLUS ©SEGA

Slide 128

Slide 128 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX, y), rightPoint = Offset(rightX, y), ) ©ATLUS ©SEGA

Slide 129

Slide 129 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX, y), rightPoint = Offset(rightX, y), ) ©ATLUS ©SEGA

Slide 130

Slide 130 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX, y), rightPoint = Offset(rightX, y), ) ©ATLUS ©SEGA

Slide 131

Slide 131 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val direction = if (message.index % 2 = = 0) 1f else -1f ©ATLUS ©SEGA

Slide 132

Slide 132 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val direction = if (message.index % 2 = = 0) 1f else -1f val horizontalShift = randomBetween(MinShift, MaxShift) * direction ©ATLUS ©SEGA

Slide 133

Slide 133 text

val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val direction = if (message.index % 2 = = 0) 1f else -1f val horizontalShift = randomBetween(MinShift, MaxShift) * direction val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX + horizontalShift, y), rightPoint = Offset(leftX + horizontalShift, y), ) ©ATLUS ©SEGA

Slide 134

Slide 134 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f val direction = if (message.index % 2 = = 0) 1f else -1f val horizontalShift = randomBetween(MinShift, MaxShift) * direction val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX + horizontalShift, y), rightPoint = Offset(leftX + horizontalShift, y), ) ©ATLUS ©SEGA

Slide 135

Slide 135 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f val direction = if (message.index % 2 = = 0) 1f else -1f val horizontalShift = randomBetween(MinShift, MaxShift) * direction val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX + horizontalShift, y), rightPoint = Offset(leftX + horizontalShift, y), ) ©ATLUS ©SEGA

Slide 136

Slide 136 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) val leftX = (AvatarSize.width / 2f) - (lineWidth / 2f) val rightX = leftX + lineWidth val y = AvatarSize.height / 2f val direction = if (message.index % 2 = = 0) 1f else -1f val horizontalShift = randomBetween(MinShift, MaxShift) * direction val lineCoordinates = LineCoordinates( leftPoint = Offset(leftX + horizontalShift, y), rightPoint = Offset(leftX + horizontalShift, y), ) ©ATLUS ©SEGA

Slide 137

Slide 137 text

©ATLUS ©SEGA

Slide 138

Slide 138 text

val listState = rememberLazyListState() LazyColumn(state = listState) ©ATLUS ©SEGA

Slide 139

Slide 139 text

val listState = rememberLazyListState() val transcriptState = rememberTranscriptState() val entries = transcriptState.entries LazyColumn(state = listState) ©ATLUS ©SEGA

Slide 140

Slide 140 text

val listState = rememberLazyListState() val transcriptState = rememberTranscriptState() val entries = transcriptState.entries LazyColumn(state = listState) { items(entries) { entry -> Entry(entry) } } ©ATLUS ©SEGA

Slide 141

Slide 141 text

val listState = rememberLazyListState() val transcriptState = rememberTranscriptState() val entries = transcriptState.entries BackgroundLine(listState, entries) LazyColumn(state = listState) { items(entries) { entry -> Entry(entry) } } ©ATLUS ©SEGA

Slide 142

Slide 142 text

@Composable private fun BackgroundLine( entries: List, listState: LazyListState, ) { val visibleItemInfos = listState.layoutInfo.visibleItemsInfo } ©ATLUS ©SEGA

Slide 143

Slide 143 text

@Composable private fun BackgroundLine( entries: List, listState: LazyListState, ) { val visibleItemInfos = listState.layoutInfo.visibleItemsInfo } interface LazyListItemInfo { /** * The main axis offset of the item in pixels. * It is relative to the start of the lazy list container. */ val offset: Int } ©ATLUS ©SEGA

Slide 144

Slide 144 text

@Composable private fun BackgroundLine( entries: List, listState: LazyListState, ) { val visibleItemInfos = listState.layoutInfo.visibleItemsInfo } interface LazyListItemInfo { /** * The main axis offset of the item in pixels. * It is relative to the start of the lazy list container. */ val offset: Int } ©ATLUS ©SEGA

Slide 145

Slide 145 text

@Composable private fun BackgroundLine( entries: List, listState: LazyListState, ) { val visibleItemInfos = listState.layoutInfo.visibleItemsInfo } interface LazyListItemInfo { /** * The main axis offset of the item in pixels. * It is relative to the start of the lazy list container. */ v​ al offset: Int } ©ATLUS ©SEGA

Slide 146

Slide 146 text

@Composable private fun BackgroundLine( entries: List, listState: LazyListState, ) { val visibleItemInfos = listState.layoutInfo.visibleItemsInfo Canvas(modifier = Modifier.fillMaxSize()) { for (info in visibleItemInfos) { drawPath(getPoints(info, entries[info.index]), Color.Black) } } } interface LazyListItemInfo { * * The main axis offset of the item in pixels. * It is relative to the start of the lazy list container. / v​ } ©ATLUS ©SEGA

Slide 147

Slide 147 text

©ATLUS ©SEGA

Slide 148

Slide 148 text

Total height Overlap height ©ATLUS ©SEGA

Slide 149

Slide 149 text

©ATLUS ©SEGA

Slide 150

Slide 150 text

©ATLUS ©SEGA

Slide 151

Slide 151 text

fun Modifier.drawConnectingLine(entry1: Entry, entry2: Entry?): Modifier { } ©ATLUS ©SEGA

Slide 152

Slide 152 text

fun Modifier.drawConnectingLine(entry1: Entry, entry2: Entry?): Modifier { if (entry2 == null) return this return drawWithCache { onDrawBehind { } } } ©ATLUS ©SEGA

Slide 153

Slide 153 text

fun Modifier.drawConnectingLine(entry1: Entry, entry2: Entry?): Modifier { if (entry2 == null) return this return drawWithCache { val linePath = Path() val topLeft = entry1.lineCoordinates.leftPoint val topRight = entry1.lineCoordinates.rightPoint val bottomLeft = entry2.lineCoordinates.leftPoint + bottomOffset val bottomRight = entry2.lineCoordinates.rightPoint + bottomOffset onDrawBehind { } } } ©ATLUS ©SEGA

Slide 154

Slide 154 text

fun Modifier.drawConnectingLine(entry1: Entry, entry2: Entry?): Modifier { if (entry2 == null) return this return drawWithCache { val linePath = Path() val topLeft = entry1.lineCoordinates.leftPoint val topRight = entry1.lineCoordinates.rightPoint val bottomLeft = entry2.lineCoordinates.leftPoint + bottomOffset val bottomRight = entry2.lineCoordinates.rightPoint + bottomOffset onDrawBehind { with(linePath) { rewind() moveTo(topLeft.x, topLeft.y) lineTo(topRight.x, topRight.y) lineTo(bottomRight.x, bottomRight.y) lineTo(bottomLeft.x, bottomLeft.y) close() } drawPath(linePath, Color.Black) } } ©ATLUS ©SEGA

Slide 155

Slide 155 text

val bottomRight = entry2.lineCoordinates.rightPoint + bottomOffset val shadowPaint = Paint().apply { color = Color.Black alpha = 0.5f asFrameworkPaint().maskFilter = BlurMaskFilter(4.dp.toPx(), NORMAL) } onDrawBehind { with(linePath) { rewind() moveTo(topLeft.x, topLeft.y) lineTo(topRight.x, topRight.y) lineTo(bottomRight.x, bottomRight.y) lineTo(bottomLeft.x, bottomLeft.y) close() } translate(top = 16.dp.toPx()) { drawIntoCanvas { it.drawPath(linePath, shadowPaint) } } drawPath(linePath, Color.Black) } } ©ATLUS ©SEGA

Slide 156

Slide 156 text

©ATLUS ©SEGA

Slide 157

Slide 157 text

©ATLUS ©SEGA Portraits

Slide 158

Slide 158 text

©ATLUS ©SEGA

Slide 159

Slide 159 text

©ATLUS ©SEGA

Slide 160

Slide 160 text

©ATLUS ©SEGA

Slide 161

Slide 161 text

©ATLUS ©SEGA

Slide 162

Slide 162 text

©ATLUS ©SEGA

Slide 163

Slide 163 text

©ATLUS ©SEGA

Slide 164

Slide 164 text

©ATLUS ©SEGA

Slide 165

Slide 165 text

©ATLUS ©SEGA

Slide 166

Slide 166 text

©ATLUS ©SEGA

Slide 167

Slide 167 text

©ATLUS ©SEGA

Slide 168

Slide 168 text

©ATLUS ©SEGA @Composable fun Portraits(senders: List) { Canvas { } }

Slide 169

Slide 169 text

©ATLUS ©SEGA @Composable fun Portraits(senders: List) { val displayModels = remember(senders) { } Canvas { } }

Slide 170

Slide 170 text

©ATLUS ©SEGA @Composable fun Portraits(senders: List) { val displayModels = remember(senders) { val darkAvatarIndex = Random.nextInt(senders.size) senders.shuffled().mapIndexed { index, sender -> sender.getDisplayModel( darkAvatar = darkAvatarIndex == index, ) } } Canvas {

Slide 171

Slide 171 text

©ATLUS ©SEGA data class PortraitDisplayModel( val image: ImageBitmap, val middlePath: Path, val innerPath: Path, val imageOffset: Offset, val outerRotation: Float, val middleRotation: Float, val innerRotation: Float, val horizontalOffset: Float, val verticalOffset: Float, val darkAvatar: Boolean, )

Slide 172

Slide 172 text

©ATLUS ©SEGA data class PortraitDisplayModel( val image: ImageBitmap, val middlePath: Path, val innerPath: Path, val imageOffset: Offset, val outerRotation: Float, val middleRotation: Float, val innerRotation: Float, val horizontalOffset: Float, val verticalOffset: Float, val darkAvatar: Boolean, )

Slide 173

Slide 173 text

©ATLUS ©SEGA data class PortraitDisplayModel( val image: ImageBitmap, val middlePath: Path, val innerPath: Path, val imageOffset: Offset, val outerRotation: Float, val middleRotation: Float, val innerRotation: Float, val horizontalOffset: Float, val verticalOffset: Float, val darkAvatar: Boolean, )

Slide 174

Slide 174 text

©ATLUS ©SEGA data class PortraitDisplayModel( val image: ImageBitmap, val middlePath: Path, val innerPath: Path, val imageOffset: Offset, val outerRotation: Float, val middleRotation: Float, val innerRotation: Float, val horizontalOffset: Float, val verticalOffset: Float, val darkAvatar: Boolean, )

Slide 175

Slide 175 text

©ATLUS ©SEGA Canvas { }

Slide 176

Slide 176 text

©ATLUS ©SEGA Canvas { }

Slide 177

Slide 177 text

Canvas { var stride = 0f val rotationPivot = Offset( x = PortraitSize.width.toPx(), y = PortraitSize.height.toPx(), ) for (model in portraitDisplayModels) { } } ©ATLUS ©SEGA

Slide 178

Slide 178 text

Canvas { var stride = 0f val rotationPivot = Offset( x = PortraitSize.width.toPx(), y = PortraitSize.height.toPx(), ) for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(degrees = model.outerRotation, pivot = rotationPivot) } ) { drawRect(Color.Black, size = PortraitSize.toSize()) } } ©ATLUS ©SEGA

Slide 179

Slide 179 text

Canvas { var stride = 0f val rotationPivot = Offset( x = PortraitSize.width.toPx(), y = PortraitSize.height.toPx(), ) for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(degrees = model.outerRotation, pivot = rotationPivot) } ) { drawRect(Color.Black, size = PortraitSize.toSize()) } stride += PortraitSize.width.toPx() } ©ATLUS ©SEGA

Slide 180

Slide 180 text

Canvas { var stride = 0f val rotationPivot = Offset( x = PortraitSize.width.toPx(), y = PortraitSize.height.toPx(), ) for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(degrees = model.outerRotation, pivot = rotationPivot) } ) { drawRect(Color.Black, size = PortraitSize.toSize()) } stride += PortraitSize.width.toPx() } stride = 0f for (model in portraitDisplayModels) { } ©ATLUS ©SEGA

Slide 181

Slide 181 text

withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(degrees = model.outerRotation, pivot = rotationPivot) } ) { drawRect(Color.Black, size = PortraitSize.toSize()) } stride += PortraitSize.width.toPx() } stride = 0f for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(model.middleRotation, pivot = rotationPivot) } ) { drawPath(model.middlePath, Color.White) } } ©ATLUS ©SEGA

Slide 182

Slide 182 text

} stride = 0f for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(model.middleRotation, pivot = rotationPivot) } ) { drawPath(model.middlePath, Color.White) withTransform( transformBlock = { rotate(model.innerRotation, rotationPivot) clipPath(model.innerPath) }, ) { if (model.darkAvatar) { drawRect(Color.Black) } } } } ©ATLUS ©SEGA

Slide 183

Slide 183 text

for (model in portraitDisplayModels) { stride += model.horizontalOffset withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(model.middleRotation, pivot = rotationPivot) } ) { drawPath(model.middlePath, Color.White) withTransform( transformBlock = { rotate(model.innerRotation, rotationPivot) clipPath(model.innerPath) }, ) { if (model.darkAvatar) { drawRect(Color.Black) } translate(left = model.imageOffset.x, top = model.imageOffset.y) { drawImage(model.image) } } } } ©ATLUS ©SEGA

Slide 184

Slide 184 text

withTransform( transformBlock = { translate(stride + model.horizontalOffset, model.verticalOffset) rotate(model.middleRotation, pivot = rotationPivot) } ) { drawPath(model.middlePath, Color.White) withTransform( transformBlock = { rotate(model.innerRotation, rotationPivot) clipPath(model.innerPath) }, ) { if (model.darkAvatar) { drawRect(Color.Black) } translate(left = model.imageOffset.x, top = model.imageOffset.y) { drawImage(model.image) } } } stride += PortraitSize.width.toPx() } ©ATLUS ©SEGA

Slide 185

Slide 185 text

Animations ©ATLUS ©SEGA

Slide 186

Slide 186 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, ) ©ATLUS ©SEGA

Slide 187

Slide 187 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, val lineProgress: State, ) ©ATLUS ©SEGA

Slide 188

Slide 188 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, val lineProgress: State, val avatarBackgroundScale: State, val avatarForegroundScale: State, ) ©ATLUS ©SEGA

Slide 189

Slide 189 text

data class Entry( val message: Message, val lineCoordinates: LineCoordinates, val lineProgress: State, val avatarBackgroundScale: State, val avatarForegroundScale: State, val messageHorizontalScale: State, val messageVerticalScale: State, val messageTextAlpha: State, ) ©ATLUS ©SEGA

Slide 190

Slide 190 text

avatarBackgroundScale = Animatable(initialValue = 0.6f) ©ATLUS ©SEGA

Slide 191

Slide 191 text

avatarBackgroundScale = Animatable(initialValue = 0.6f) .apply { coroutineScope.launch { animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 300, easing = EaseOutBack, ), ) } } ©ATLUS ©SEGA

Slide 192

Slide 192 text

avatarBackgroundScale: Animatable = Animatable(initialValue = 0.6f) .apply { coroutineScope.launch { animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 300, easing = EaseOutBack, ), ) } } ©ATLUS ©SEGA

Slide 193

Slide 193 text

avatarBackgroundScale: State = Animatable(initialValue = 0.6f) .apply { coroutineScope.launch { animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 300, easing = EaseOutBack, ), ) } } .asState() ©ATLUS ©SEGA

Slide 194

Slide 194 text

avatarBackgroundScale = Animatable(initialValue = 0.6f) .apply { // . .. } ©ATLUS ©SEGA

Slide 195

Slide 195 text

avatarBackgroundScale = Animatable(initialValue = 0.6f) .apply { // . .. } avatarForegroundScale = Animatable(initialValue = 0.0f) .apply { coroutineScope.launch { delay(160L) snapTo(0.8f) animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 150, easing = EaseOutBack, ), ) } } .asState() ©ATLUS ©SEGA

Slide 196

Slide 196 text

avatarBackgroundScale = Animatable(initialValue = 0.6f) .apply { // . .. } avatarForegroundScale = Animatable(initialValue = 0.0f) .apply { coroutineScope.launch { delay(160L) snapTo(0.8f) animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 150, easing = EaseOutBack, ), ) } } .asState() ©ATLUS ©SEGA

Slide 197

Slide 197 text

Box( modifier = Modifier .size(AvatarSize) .drawBehind { // Previous drawing code } ) { } ©ATLUS ©SEGA

Slide 198

Slide 198 text

Box( modifier = Modifier .size(AvatarSize) .scale(entry.avatarBackgroundScale.value) .drawBehind { // Previous drawing code } ) { } ©ATLUS ©SEGA

Slide 199

Slide 199 text

Box( modifier = Modifier .size(AvatarSize) .scale(entry.avatarBackgroundScale.value) .drawBehind { // Previous drawing code } ) { Image( painter = painterResource(entry.message.sender.image), ) } ©ATLUS ©SEGA

Slide 200

Slide 200 text

Box( modifier = Modifier .size(AvatarSize) .scale(entry.avatarBackgroundScale.value) .drawBehind { // Previous drawing code } ) { Image( painter = painterResource(entry.message.sender.image), modifier = Modifier.graphicsLayer { transformOrigin = TransformOrigin( pivotFractionX = 0.5f, pivotFractionY = 1.15f, ), scaleX = entry.avatarForegroundScale.value scaleY = entry.avatarForegroundScale.value } ) } ©ATLUS ©SEGA

Slide 201

Slide 201 text

Box( modifier = Modifier .size(AvatarSize) .scale(entry.avatarBackgroundScale.value) .drawBehind { // Previous drawing code } ) { Image( painter = painterResource(entry.message.sender.image), modifier = Modifier.graphicsLayer { transformOrigin = TransformOrigin( pivotFractionX = 0.5f, pivotFractionY = 1.15f, ), scaleX = entry.avatarForegroundScale.value scaleY = entry.avatarForegroundScale.value } ) } ©ATLUS ©SEGA

Slide 202

Slide 202 text

onDrawBehind { with(linePath) { rewind() moveTo(topLeft.x, topLeft.y) lineTo(topRight.x, topRight.y) lineTo (​ bottomRight.x, bottomRight.y) lineTo (​ bottomLeft.x, bottomLeft.y) close() } drawPath(linePath, Color.Black) } v​ al b​ ottomLeft = entry2.lineCoordinates.leftPoint v​ al b​ ottomRight = entry2.lineCoordinates.rightPoint The line ©ATLUS ©SEGA

Slide 203

Slide 203 text

onDrawBehind { val currentBottomLeft = lerp( start = topLeft, stop = bottomLeft, fraction = entry1.lineProgress.value, ) val currentBottomRight = lerp( start = topRight, stop = bottomRight, fraction = entry1.lineProgress.value, ) with(linePath) { rewind() moveTo(topLeft.x, topLeft.y) lineTo(topRight.x, topRight.y) lineTo (​ currentBottomRight.x, currentBottomRight.y) lineTo (​ currentBottomLeft.x, currentBottomLeft.y) close() } drawPath(linePath, Color.Black) } ©ATLUS ©SEGA

Slide 204

Slide 204 text

onDrawBehind { val currentBottomLeft: Offset = lerp( start = topLeft, stop = bottomLeft, fraction = entry1.lineProgress.value, ) val currentBottomRight: Offset = lerp( start = topRight, stop = bottomRight, fraction = entry1.lineProgress.value, ) with(linePath) { rewind() moveTo(topLeft.x, topLeft.y) lineTo(topRight.x, topRight.y) lineTo (​ currentBottomRight.x, currentBottomRight.y) lineTo (​ currentBottomLeft.x, currentBottomLeft.y) close() } drawPath(linePath, Color.Black) } ©ATLUS ©SEGA

Slide 205

Slide 205 text

coroutineScope.launch { while (true) { delay(300) dotVisible1.value = true delay(300) dotVisible2.value = true delay(300) dotVisible3.value = true delay(400) dotVisible1.value = false delay(100) dotVisible2.value = false delay(100) dotVisible3.value = false } } ©ATLUS ©SEGA

Slide 206

Slide 206 text

©ATLUS ©SEGA

Slide 207

Slide 207 text

@Composable fun BackgroundParticles(season: Season, modifier: Modifier = Modifier) { val state = remember { ParticlesState() } state.season = season if (season == Season.NONE) return val frameTimeMillis by rememberFrameTimeMillis() Canvas(modifier = modifier.fillMaxSize()) { state.update(frameTimeMillis) state.particles.fastForEach { particle -> // Draw particle ... } } }

Slide 208

Slide 208 text

@Composable fun BackgroundParticles(season: Season, modifier: Modifier = Modifier) { val state = remember { ParticlesState() } state.season = season if (season == Season.NONE) return val frameTimeMillis by rememberFrameTimeMillis() Canvas(modifier = modifier.fillMaxSize()) { state.update(frameTimeMillis) state.particles.fastForEach { particle -> // Draw particle ... } } }

Slide 209

Slide 209 text

@Composable private fun rememberFrameTimeMillis(): LongState { val millisState = remember { mutableLongStateOf(0L) } LaunchedEffect(Unit) { val startTime = withFrameMillis { it } while (true) { withFrameMillis { frameTime -> millisState.longValue = frameTime - startTime } } } return millisState }

Slide 210

Slide 210 text

@Composable private fun rememberFrameTimeMillis(): LongState { val millisState = remember { mutableLongStateOf(0L) } LaunchedEffect(Unit) { val startTime = withFrameMillis { it } while (true) { withFrameMillis { frameTime -> millisState.longValue = frameTime - startTime } } } return millisState }

Slide 211

Slide 211 text

class ParticlesState { }

Slide 212

Slide 212 text

class ParticlesState { private val active = ArrayList(MAX_PARTICLE_COUNT) private val pool = ArrayDeque(MAX_PARTICLE_COUNT).apply { repeat(MAX_PARTICLE_COUNT) { add(Particle()) } } }

Slide 213

Slide 213 text

class ParticlesState { private val active = ArrayList(MAX_PARTICLE_COUNT) private val pool = ArrayDeque(MAX_PARTICLE_COUNT).apply { repeat(MAX_PARTICLE_COUNT) { add(Particle()) } } val particles: List get() = active fun update(time: Long, worldSize: DpSize) { } }

Slide 214

Slide 214 text

class ParticlesState { private val active = ArrayList(MAX_PARTICLE_COUNT) private val pool = ArrayDeque(MAX_PARTICLE_COUNT).apply { repeat(MAX_PARTICLE_COUNT) { add(Particle()) } } val particles: List get() = active private var lastUpdateTime = 0L fun update(time: Long, worldSize: DpSize) { if (time > nextSpawnTime && pool.isNotEmpty()) { // Move a particle from pool to active. } val deltaSeconds = (time - lastUpdateTime) / 1000f lastUpdateTime = time } }

Slide 215

Slide 215 text

private var lastUpdateTime = 0L fun update(time: Long, worldSize: DpSize) { if (time > nextSpawnTime && pool.isNotEmpty()) { // Move a particle from pool to active. } val deltaSeconds = (time - lastUpdateTime) / 1000f for (index in active.indices.reversed()) { val particle = active[index] if ( particle.x < -DESPAWN_BUFFER_SIZE || particle.y > worldSize.height + DESPAWN_BUFFER_SIZE ) { active.remove(particle) pool.add(particle) } } lastUpdateTime = time } }

Slide 216

Slide 216 text

if (time > nextSpawnTime && pool.isNotEmpty()) { // Move a particle from pool to active. } val deltaSeconds = (time - lastUpdateTime) / 1000f for (index in active.indices.reversed()) { val particle = active[index] if ( particle.x < -DESPAWN_BUFFER_SIZE || particle.y > worldSize.height + DESPAWN_BUFFER_SIZE ) { active.remove(particle) pool.add(particle) } else { with(particle) { rotation += rotationSpeed * deltaSeconds x += xSpeed * deltaSeconds y += (period * sin(amplitude * x.value) + (ySpeed.value * deltaSeconds)).dp } } } lastUpdateTime = time } }

Slide 217

Slide 217 text

if (time > nextSpawnTime && pool.isNotEmpty()) { // Move a particle from pool to active. } val deltaSeconds = (time - lastUpdateTime) / 1000f for (index in active.indices.reversed()) { val particle = active[index] if ( particle.x < -DESPAWN_BUFFER_SIZE || particle.y > worldSize.height + DESPAWN_BUFFER_SIZE ) { active.remove(particle) pool.add(particle) } else { with(particle) { rotation += rotationSpeed * deltaSeconds x += xSpeed * deltaSeconds y += (period * sin(amplitude * x.value) + (ySpeed.value * deltaSecon } } } lastUpdateTime = time } }

Slide 218

Slide 218 text

Takeaways Compose UI facilitates creativity Some UIs should be boring, but not all of them Maybe we lost some magic with all the consistency Video games can be source of inspiration

Slide 219

Slide 219 text

No content

Slide 220

Slide 220 text

No content

Slide 221

Slide 221 text

No content

Slide 222

Slide 222 text

gameuidatabase.com

Slide 223

Slide 223 text

gameuidatabase.com ©ATLUS ©SEGA

Slide 224

Slide 224 text

Thank you, and don’t forget to vote

Slide 225

Slide 225 text

Creative UIs with Compose github.com/chris-horner/persona-im All art and character designs in this presentation are the property of Atlus Co., Ltd. Material is used for reference and educational purposes only. © @[email protected]