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

Jetpack Composeで作る楽しい金額入力画面! / Creating a fun r...

yume
October 11, 2024

Jetpack Composeで作る楽しい金額入力画面! / Creating a fun register screen with jetpack compose!

Jetpack Composeで作る楽しい金額入力画面!

STORES Tech Conf 2024 "New Engineering" 登壇資料です。
https://storesinc.tech/conf/2024

yume

October 11, 2024
Tweet

More Decks by yume

Other Decks in Programming

Transcript

  1. 自己紹介 2 chuka モバイル開発本部 決済グループ 2024年入社 STORES 決済 Android の開発

    普段は北海道からリモートワーク おいしいちゅうかがたべたい 𝕏:@YkOxc
  2. Jetpack Composeとは 18 STORES 決済 Android も、 Jetpack Compose移行に取り組んでいます! Androidの新しいUIツールキット

    2021年7月に安定版リリース ・宣言的UIで直感的にわかりやすい ・Kotlinで記述できてコード量も削減 ・プレビュー機能などで開発速度UP
  3. 26 @Composable
 fun Key() {
 Box(
 modifier = Modifier
 .size(120.dp)


    .background(Color.LightGray),
 contentAlignment = Alignment.Center
 ) {
 Text(
 text = "1",
 style =
 TextStyle(
 fontSize = 28.sp,
 color = Color.DarkGray,
 ),
 )
 }
 }
 キーを1個つくる
  4. 27 @Composable
 fun Key() {
 Box(
 modifier = Modifier
 .size(120.dp)


    .background(Color.LightGray),
 contentAlignment = Alignment.Center
 ) {
 Text(
 text = "1",
 style =
 TextStyle(
 fontSize = 28.sp,
 color = Color.DarkGray,
 ),
 )
 }
 }
 キーを1個つくる
  5. 28 @Composable
 fun Key() {
 Box(
 modifier = Modifier
 .size(120.dp)


    .background(Color.LightGray),
 contentAlignment = Alignment.Center
 ) {
 Text(
 text = "1",
 style =
 TextStyle(
 fontSize = 28.sp,
 color = Color.DarkGray,
 ),
 )
 }
 }
 キーを1個つくる
  6. 29 @Composable
 fun Key() {
 Box(
 modifier = Modifier
 .size(120.dp)


    .background(Color.LightGray),
 contentAlignment = Alignment.Center
 ) {
 Text(
 text = "1",
 style =
 TextStyle(
 fontSize = 28.sp,
 color = Color.DarkGray,
 ),
 )
 }
 }
 キーを1個つくる
  7. 押下時のアニメーションの実装① - 色と文字サイズ - 33 val transition = updateTransition(isSelectedState, label

    = "selected state")
 
 val fontSize by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 0.85f else 1f
 },
 label = "",
 )
 
 val fontColor by transition.animateColor(
 targetValueByState = { isSelected ->
 if (isSelected) Color.Blue else Color.DarkGray
 },
 label = "",
 )

  8. 押下時のアニメーションの実装① - 色と文字サイズ - 34 val transition = updateTransition(isSelectedState, label

    = "selected state")
 
 val fontSize by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 0.85f else 1f
 },
 label = "",
 )
 
 val fontColor by transition.animateColor(
 targetValueByState = { isSelected ->
 if (isSelected) Color.Blue else Color.DarkGray
 },
 label = "",
 )
 キー押下時(isSelectedがtrueのとき) 文字サイズを0.85倍 それ以外は1倍
  9. 押下時のアニメーションの実装① - 色と文字サイズ - 35 val transition = updateTransition(isSelectedState, label

    = "selected state")
 
 val fontSize by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 0.85f else 1f
 },
 label = "",
 )
 
 val fontColor by transition.animateColor(
 targetValueByState = { isSelected ->
 if (isSelected) Color.Blue else Color.DarkGray
 },
 label = "",
 )
 キー押下時(isSelectedがtrueのとき) 文字色をBlue それ以外はDarkGray
  10. 押下時のアニメーションの実装① - 色と文字サイズ - 36 Text(
 text = "1",
 modifier

    = Modifier.graphicsLayer {
 scaleX = fontSize
 scaleY = fontSize
 transformOrigin = TransformOrigin.Center
 },
 style =
 TextStyle(
 textMotion = TextMotion.Animated,
 fontSize = 28.sp,
 color = fontColor,
 ),
 )

  11. 押下時のアニメーションの実装① - 色と文字サイズ - 37 Text(
 text = "1",
 modifier

    = Modifier.graphicsLayer {
 scaleX = fontSize
 scaleY = fontSize
 transformOrigin = TransformOrigin.Center
 },
 style =
 TextStyle(
 textMotion = TextMotion.Animated,
 fontSize = 28.sp,
 color = fontColor,
 ),
 )

  12. 押下時のアニメーションの実装① - 色と文字サイズ - 38 Text(
 text = "1",
 modifier

    = Modifier.graphicsLayer {
 scaleX = fontSize
 scaleY = fontSize
 transformOrigin = TransformOrigin.Center
 },
 style =
 TextStyle(
 textMotion = TextMotion.Animated,
 fontSize = 28.sp,
 color = fontColor,
 ),
 )

  13. 押下時のアニメーションの実装② - 円形エフェクト - 40 val circleSize by transition.animateFloat(
 targetValueByState

    = { isSelected ->
 if (isSelected) 100f else 120f
 },
 label = "",
 )
 val circleWidth by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 8f else 0f
 },
 label = "",
 )

  14. 押下時のアニメーションの実装② - 円形エフェクト - 41 val circleSize by transition.animateFloat(
 targetValueByState

    = { isSelected ->
 if (isSelected) 100f else 120f
 },
 label = "",
 )
 val circleWidth by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 8f else 0f
 },
 label = "",
 )
 キー押下時(isSelectedがtrueのとき) 円の大きさを100 離したとき120
  15. 押下時のアニメーションの実装② - 円形エフェクト - 42 val circleSize by transition.animateFloat(
 targetValueByState

    = { isSelected ->
 if (isSelected) 100f else 120f
 },
 label = "",
 )
 val circleWidth by transition.animateFloat(
 targetValueByState = { isSelected ->
 if (isSelected) 8f else 0f
 },
 label = "",
 )
 キー押下時(isSelectedがtrueのとき) 円の太さを8 離したとき0
  16. 押下時のアニメーションの実装② - 円形エフェクト - 43 transition.AnimatedVisibility(
 visible = { targetValue

    -> targetValue },
 enter = fadeIn(),
 exit = fadeOut(),
 ) {
 Box(
 modifier =
 Modifier
 .size(circleSize.dp)
 .border(
 width = circleWidth.dp,
 color = Color.Blue,
 shape = CircleShape,
 ),
 )
 }

  17. 押下時のアニメーションの実装② - 円形エフェクト - 44 transition.AnimatedVisibility(
 visible = { targetValue

    -> targetValue },
 enter = fadeIn(),
 exit = fadeOut(),
 ) {
 Box(
 modifier =
 Modifier
 .size(circleSize.dp)
 .border(
 width = circleWidth.dp,
 color = Color.Blue,
 shape = CircleShape,
 ),
 )
 }
 Modifier.border(shape = CircleShape) で 円形の枠線を描画
  18. 押下時のアニメーションの実装② - 円形エフェクト - 45 transition.AnimatedVisibility(
 visible = { targetValue

    -> targetValue },
 enter = fadeIn(),
 exit = fadeOut(),
 ) {
 Box(
 modifier =
 Modifier
 .size(circleSize.dp)
 .border(
 width = circleWidth.dp,
 color = Color.Blue,
 shape = CircleShape,
 ),
 )
 }
 targetValue(isSelected)に応じて アニメーションで表示・非表示
  19. 押下時のアニメーションの実装③ - ジェスチャーの判定 - 46 Box(
 // 背景色などの設定は省略 
 modifier

    =
 Modifier
 .pointerInput(Unit) {
 detectTapGestures(
 onPress = {
 isSelected = true
 // 指を離したら 
 tryAwaitRelease()
 isSelected = false
 },
 )
 },
 ) {
 // ① のTextと②のCircleを重ねて配置 
 }

  20. Box(
 // 背景色などの設定は省略 
 modifier =
 Modifier
 .pointerInput(Unit) {
 detectTapGestures(


    onPress = {
 isSelected = true
 // 指を離したら 
 tryAwaitRelease()
 isSelected = false
 },
 )
 },
 ) {
 // ① のTextと②のCircleを重ねて配置 
 }
 押下時のアニメーションの実装③ - ジェスチャーの判定 - 47 detectTapGestures(onPress = {}) で 長押しのジェスチャーイベントを検知
  21. キーパッドらしくする 51 val keyPad =
 listOf(
 "7", "8", "9",
 "4",

    "5", "6",
 "1", "2", "3",
 "0", "00", "del",
 )
 
 LazyVerticalGrid(
 columns = GridCells.Fixed(3),
 ) {
 items(keyPad) { key ->
 Key(
 text = key,
 isSelected = selected
 )
 }
 }

  22. キーパッドらしくする 52 val keyPad =
 listOf(
 "7", "8", "9",
 "4",

    "5", "6",
 "1", "2", "3",
 "0", "00", "del",
 )
 
 LazyVerticalGrid(
 columns = GridCells.Fixed(3),
 ) {
 items(keyPad) { key ->
 Key(
 text = key,
 isSelected = selected
 )
 }
 }
 ドラッグ時の実装をしよう!
  23. ドラッグ時のアニメーションの実装① - ジェスチャーの検出 - 56 Modifier
 .pointerInput(Unit) {
 detectDragGestures(
 onDragStart

    = {
 // ドラッグ開始時
 },
 onDrag = {
 // ドラッグ中
 },
 onDragEnd = {
 // ドラッグ終了時
 },
 onDragCancel = {
 // 別のジェスチャが入力されたとき 
 },
 )
 },

  24. ドラッグ時のアニメーションの実装① - ジェスチャーの検出 - 57 Modifier
 .pointerInput(Unit) {
 detectDragGestures(
 onDragStart

    = {
 // ドラッグ開始時
 },
 onDrag = {
 // ドラッグ中
 },
 onDragEnd = {
 // ドラッグ終了時
 },
 onDragCancel = {
 // 別のジェスチャが入力されたとき 
 },
 )
 },
 detectDragGesturesで ドラッグのジェスチャーイベントを検知
  25. ドラッグ時のアニメーションの実装② - ポインタの位置の検出 - 59 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset): Int? =


    layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 0
 1
 2
 3
 4
 5

  26. 60 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset): Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo

    ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 0
 1
 2
 3
 4
 5
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  27. 61 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset): Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo

    ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 0
 1
 2
 3
 4
 5
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  28. 0
 1
 2
 3
 4
 5
 62 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  29. 0
 1
 2
 3
 4
 5
 63 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 120*120の正方形
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  30. 0
 1
 2
 3
 4
 5
 64 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(0,0)
 (350, 131) - (0,0) = (350,131)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  31. 0
 1
 2
 3
 4
 5
 65 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(120,0)
 (350, 131) - (120,0) = (230,131)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  32. 0
 1
 2
 3
 4
 5
 66 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(240,0)
 (350, 131) - (240,0) = (110,131)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  33. 0
 1
 2
 3
 4
 5
 67 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(0,120)
 (350, 131) - (0,120) = (350,11)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  34. 0
 1
 2
 3
 4
 5
 68 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(120,120)
 (350, 131) - (120,120) = (230,11)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  35. 0
 1
 2
 3
 4
 5
 69 fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset):

    Int? =
 layoutInfo.visibleItemsInfo
 .find { itemInfo ->
 itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)
 }?.key as? Int
 Create a photo grid with multiselect behavior using Jetpack Compose https://medium.com/androiddevelopers/create-a-photo-grid-with-multiselect-behavior-using-jetpack-compose-9a8d588a9b63 hitPoint(350.2, 130.7)
 itemInfo.offset(240,120)
 (350, 131) - (240,120) = (110,11)
 ドラッグ時のアニメーションの実装② - ポインタの位置の検出 -
  36. ドラッグ時のアニメーションの実装③ - ドラッグ中のポインタ検出 - 71 detectDragGestures(
 onDragStart = { offset

    ->
 lazyGridState.gridItemKeyAtPosition(offset)?.let { key ->
 pointer.value = key
 }
 },
 onDrag = { change, _ ->
 lazyGridState.gridItemKeyAtPosition(change.position)?.let { key ->
 pointer.value = key
 }
 },
 onDragEnd = {
 pointer.value = null
 },
 onDragCancel = {
 pointer.value = null
 },
 )

  37. detectDragGestures(
 onDragStart = { offset ->
 lazyGridState.gridItemKeyAtPosition(offset)?.let { key ->


    pointer.value = key
 }
 },
 onDrag = { change, _ ->
 lazyGridState.gridItemKeyAtPosition(change.position)?.let { key ->
 pointer.value = key
 }
 },
 onDragEnd = {
 pointer.value = null
 },
 onDragCancel = {
 pointer.value = null
 },
 )
 72 ドラッグ開始時のoffsetを受けて 現在のキーをpointerに格納 ドラッグ時のアニメーションの実装③ - ドラッグ中のポインタ検出 -
  38. detectDragGestures(
 onDragStart = { offset ->
 lazyGridState.gridItemKeyAtPosition(offset)?.let { key ->


    pointer.value = key
 }
 },
 onDrag = { change, _ ->
 lazyGridState.gridItemKeyAtPosition(change.position)?.let { key ->
 pointer.value = key
 }
 },
 onDragEnd = {
 pointer.value = null
 },
 onDragCancel = {
 pointer.value = null
 },
 )
 73 onDrag中は常に変更を受け取るので 都度最新のkeyを返す ドラッグ時のアニメーションの実装③ - ドラッグ中のポインタ検出 -
  39. detectDragGestures(
 onDragStart = { offset ->
 lazyGridState.gridItemKeyAtPosition(offset)?.let { key ->


    pointer.value = key
 }
 },
 onDrag = { change, _ ->
 lazyGridState.gridItemKeyAtPosition(change.position)?.let { key ->
 pointer.value = key
 }
 },
 onDragEnd = {
 pointer.value = null
 },
 onDragCancel = {
 pointer.value = null
 },
 )
 74 Drag終了・キャンセル時はnull ドラッグ時のアニメーションの実装③ - ドラッグ中のポインタ検出 -
  40. まとめ 77 ・完全に振る舞いから実装  → Jetpack Composeの知識だけで    既存に近い実装ができた! ・既存645行 /

    移行後234行  → コード量が63%減少! ・ジェスチャーのハンドリングが難しい  → 上下のドラッグがうまく反応しないことも    長押しとドラッグが競合している可能性 ・金額入力画面やっぱり可愛くて楽しい  → 全画面もっと楽しく可愛くしたい!