Slide 1

Slide 1 text

Creative UIs with Compose Chris Horner Design Systems at Cash App

Slide 2

Slide 2 text

We ’ re going to analyse and build a complex UI But first… ҰॹʹෳࡶͳUIͷௐࠪͱߏஙΛߦ͍·͢ɻ ·ͣɻɻɻ

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 UI ͷϑϨʔϜϫʔΫ͸࢛֯ܗ͔Βߏங͞Ε͍ͯ·͢ɻ

Slide 11

Slide 11 text

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

Slide 12

Slide 12 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 13

Slide 13 text

Boring 😴 is good ฏຌͳ͍͍͜ͱͰ͢Ͷɻ

Slide 14

Slide 14 text

Boring 😴 is good Phone calls ి࿩ Customer support ΧελϚʔαʔϏε Maps / navigation ஍ਤʗφϏ Email ϝʔϧ But… have we gone too far? ͔͠͠...ߦ͖ա͗ͨͷͩΖ͏͔ʁ ฏຌͳ͍͍͜ͱͰ͢Ͷɻ

Slide 15

Slide 15 text

Let ’ s go back in time… աڈʹ໭Γ·͠ΐ͏

Slide 16

Slide 16 text

2009೥

Slide 17

Slide 17 text

appleinsider.com

Slide 18

Slide 18 text

No content

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

skins.webamp.org

Slide 23

Slide 23 text

gameuidatabase.com

Slide 24

Slide 24 text

©ATLUS ©SEGA

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? ͲͷΑ͏ʹUIߏஙΛ͍͚ͯ͠͹͍͍ͷͰ͠ΐ͏͔ʁ

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

Background

Slide 37

Slide 37 text

©ATLUS ©SEGA

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 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 41

Slide 41 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 42

Slide 42 text

Textbox ©ATLUS ©SEGA

Slide 43

Slide 43 text

©ATLUS ©SEGA

Slide 44

Slide 44 text

Sup? ©ATLUS ©SEGA

Slide 45

Slide 45 text

Sup? ©ATLUS ©SEGA

Slide 46

Slide 46 text

romainguy / v9 ©ATLUS ©SEGA

Slide 47

Slide 47 text

romainguy / v9 ©ATLUS ©SEGA

Slide 48

Slide 48 text

romainguy / v9 ©ATLUS ©SEGA

Slide 49

Slide 49 text

romainguy / v9 ©ATLUS ©SEGA

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 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 55

Slide 55 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 56

Slide 56 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 57

Slide 57 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 58

Slide 58 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 59

Slide 59 text

c5inco / shape-composer-figma

Slide 60

Slide 60 text

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

Slide 61

Slide 61 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 62

Slide 62 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 63

Slide 63 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 64

Slide 64 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 65

Slide 65 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 66

Slide 66 text

©ATLUS ©SEGA

Slide 67

Slide 67 text

Avatar ©ATLUS ©SEGA

Slide 68

Slide 68 text

©ATLUS ©SEGA

Slide 69

Slide 69 text

Row { TextBox() } ©ATLUS ©SEGA

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 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 73

Slide 73 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 74

Slide 74 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 75

Slide 75 text

Message layout ©ATLUS ©SEGA

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

©ATLUS ©SEGA

Slide 79

Slide 79 text

©ATLUS ©SEGA

Slide 80

Slide 80 text

©ATLUS ©SEGA

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Row { Avatar() TextBox( modifier = Modifier.offset(x = (-40).dp) ) } ©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() } ©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 val textConstraints = constraints.copy(maxWidth = textMaxWidth) val textPlaceable = textMeasurable.measure(textConstraints) } ©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) val width = avatarPlaceable.width + textPlaceable.width - 18.dp val height = maxOf(avatarPlaceable.height, textPlaceable.height) } ©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) layout(width, height) { avatarPlaceable.place(0, 0) } } ©ATLUS ©SEGA

Slide 98

Slide 98 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 99

Slide 99 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 100

Slide 100 text

Line ©ATLUS ©SEGA

Slide 101

Slide 101 text

©ATLUS ©SEGA

Slide 102

Slide 102 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 103

Slide 103 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 104

Slide 104 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 105

Slide 105 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 106

Slide 106 text

ViewModel UI state Current list of messages ࠓ·Ͱͷϝοηʔδ͕Ϧετʹ Entries Background line coordinates എܠઢίʔσΟωʔτ Item animation progress ΞΠςϜΞχϝʔγϣϯͷ ਐߦঢ়گ

Slide 107

Slide 107 text

ViewModel UI state LazyColumn

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 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 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 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 115

Slide 115 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 116

Slide 116 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 117

Slide 117 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 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 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 122

Slide 122 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 123

Slide 123 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 124

Slide 124 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 125

Slide 125 text

©ATLUS ©SEGA

Slide 126

Slide 126 text

©ATLUS ©SEGA

Slide 127

Slide 127 text

Total height Overlap height ©ATLUS ©SEGA

Slide 128

Slide 128 text

©ATLUS ©SEGA

Slide 129

Slide 129 text

©ATLUS ©SEGA

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 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 133

Slide 133 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 134

Slide 134 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 135

Slide 135 text

©ATLUS ©SEGA

Slide 136

Slide 136 text

Animations ©ATLUS ©SEGA

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 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 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 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 146

Slide 146 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 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 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 151

Slide 151 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 152

Slide 152 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 ઢ Design property of Atlus Co., Ltd

Slide 153

Slide 153 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 154

Slide 154 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 155

Slide 155 text

©ATLUS ©SEGA

Slide 156

Slide 156 text

Takeaways ςΠΫΞ΢τ Compose UI facilitates creativity Compose UI͕ಠ૑ੑΛߴΊΔ Some UIs should be boring, but not all of them Ұ෦ͷUI͸ୀ۶Ͱ͋Δ΂͖͕ͩɺ͢΂ͯͰ͸ͳ͍ Maybe we lost some magic with all the consistency Ұ؏ੑ͕ແ͘ͳͬͨ͜ͱͰɺϚδοΫ͕ࣦΘΕͯ͠·ͬͨͷ͔΋͠Εͳ͍ɻ Video games can be source of inspiration ήʔϜ͸ൃ૝ͷݯʹͳΔ

Slide 157

Slide 157 text

gameuidatabase.com

Slide 158

Slide 158 text

gameuidatabase.com ©ATLUS ©SEGA

Slide 159

Slide 159 text

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