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

[shibuya.apk #46] Composableの枠を超えてアニメーションする / A...

syarihu
January 19, 2024

[shibuya.apk #46] Composableの枠を超えてアニメーションする / Animation beyond Composable

shibuya.apk #46 で発表した資料です。
動きのあるスライドが多いため、動きを見たい場合は以下のGoogle Slidesをご参照ください。
https://docs.google.com/presentation/d/1bRE9RRSn7A5BPMuflTWU5kvP8mHO03ZHRlbW6UIjlGU/edit?usp=sharing

syarihu

January 19, 2024
Tweet

More Decks by syarihu

Other Decks in Programming

Transcript

  1. Composableの枠を超えて アニメーションする shibuya.apk #46 2024/01/19 (Fri.) Taichi Sato / @syarihu

    Giftmall, Inc. / mikan Co., Ltd. Android Engineer 動きのあるスライドが多いため、動きを見たい場合は DescriptionにあるURLまた は右のQRコードからGoogle Slidesを見てください
  2. 単語を並べて、単語が右端までいったら単語単位で折り返す val sampleWords = listOf( "store", "to", "buy", "want", "food",

    "to", "cook", "dinner", "to", "go", "to", "the", "some", "to", "eat", "with", "my", "family", "I" ).mapIndexed { index, word -> WordEntity(wordId = "$index", word = word) } @Composable private fun Words( words: List<WordEntity> = sampleWords, ) { FlowRow( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 16.dp), mainAxisSpacing = 8.dp, crossAxisSpacing = 16.dp, ) { words.forEach { entity -> Word( word = entity.word, ) } } }
  3. 単語を並べて、単語が右端までいったら単語単位で折り返す val sampleWords = listOf( "store", "to", "buy", "want", "food",

    "to", "cook", "dinner", "to", "go", "to", "the", "some", "to", "eat", "with", "my", "family", "I" ).mapIndexed { index, word -> WordEntity(wordId = "$index", word = word) } @Composable private fun Words( words: List<WordEntity> = sampleWords, ) { FlowRow( modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp, vertical = 16.dp), mainAxisSpacing = 8.dp, crossAxisSpacing = 16.dp, ) { words.forEach { entity -> Word( word = entity.word, ) } } }
  4. 単語を並べて、単語が右端までいったら単語単位で折り返す @Composable private fun Word( word: String, showWord: Boolean =

    true, onGloballyPositioned: (LayoutCoordinates) -> Unit = {}, ) { Box( modifier = Modifier .drawWithContent { if (showWord) { drawContent() } } .border( BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(8.dp) ) .onGloballyPositioned { onGloballyPositioned(it) } ) {
  5. 単語を並べて、単語が右端までいったら単語単位で折り返す @Composable private fun Word( word: String, showWord: Boolean =

    true, onGloballyPositioned: (LayoutCoordinates) -> Unit = {}, ) { Box( modifier = Modifier .drawWithContent { if (showWord) { drawContent() } } .border( BorderStroke(2.dp, Color.LightGray), RoundedCornerShape(8.dp) ) .onGloballyPositioned { onGloballyPositioned(it) } ) {
  6. 選択済の単語の領域にDividerを描画する // FlowRowのサイズを保持する var flowRowSize by remember { mutableStateOf(IntSize.Zero) }

    FlowRow( modifier = Modifier // ... .onGloballyPositioned { flowRowSize = it.size }, // ... ) { // ... }
  7. 選択済の単語の領域にDividerを描画する // 各単語の位置を保持するための Map val wordPositions = remember { mutableMapOf<String,

    LayoutCoordinates>() } FlowRow( // ... ) { words.forEach { entity -> Word( word = entity.word, visible = showWord, onGloballyPositioned = { wordPositions[entity.wordId] = it } ) } }
  8. 選択済の単語の領域にDividerを描画する FlowRow( modifier = Modifier // ... .drawWithContent { //

    FlowRowのcontentを先に描画する drawContent() // 単語が非表示のとき if (showWord.not()) { // 描画されている単語の中から親の Composable上でx=0の位置に // 描画されている単語(各行の一番最初の単語)のみを取得する val firstWordInLines = wordPositions .filter { (_, layoutCoordinates) -> layoutCoordinates.positionInParent().x == 0f } if (firstWordInLines.isEmpty()) return@drawWithContent
  9. 選択済の単語の領域にDividerを描画する FlowRow( modifier = Modifier // ... .drawWithContent { //

    ... if (showWord.not()) { // ... // 最後行の下に dividerを表示しないために最後行の単語の keyを取得する val lastKey = firstWordInLines.keys.last() // 最後行の単語以外の単語の下に dividerを表示する firstWordInLines .filterNot { it.key == lastKey } .forEach { (_, layoutCoordinates) -> // ここでDividerを描画する } } }
  10. 選択済の単語の領域にDividerを描画する firstWordInLines .filterNot { it.key == lastKey } .forEach {

    (_, layoutCoordinates) -> // 単語の左下の y座標 + 単語とDividerの間のスペース val dividerOffsetY = Rect( layoutCoordinates.positionInParent(), layoutCoordinates.size.toSize() ).bottomLeft.y + dividerSpacing drawLine( Color.LightGray, start = Offset( // FlowRowの一番左から dividerを表示 x = 0f, y = dividerOffsetY ), end = Offset( // FlowRowの一番右まで dividerを表示 x = flowRowSize.width.toFloat(), y = dividerOffsetY ), strokeWidth = 1.dp.toPx() ) }
  11. 選択済の単語の領域にDividerを描画する firstWordInLines .filterNot { it.key == lastKey } .forEach {

    (_, layoutCoordinates) -> // 単語の左下の y座標 + 単語とDividerの間のスペース val dividerOffsetY = Rect( layoutCoordinates.positionInParent(), layoutCoordinates.size.toSize() ).bottomLeft.y + dividerSpacing drawLine( Color.LightGray, start = Offset( // FlowRowの一番左から dividerを表示 x = 0f, y = dividerOffsetY ), end = Offset( // FlowRowの一番右まで dividerを表示 x = flowRowSize.width.toFloat(), y = dividerOffsetY ), strokeWidth = 1.dp.toPx() ) }
  12. 選択済の単語の領域にDividerを描画する firstWordInLines .filterNot { it.key == lastKey } .forEach {

    (_, layoutCoordinates) -> // 単語の左下の y座標 + 単語とDividerの間のスペース val dividerOffsetY = Rect( layoutCoordinates.positionInParent(), layoutCoordinates.size.toSize() ).bottomLeft.y + dividerSpacing drawLine( Color.LightGray, start = Offset( // FlowRowの一番左から dividerを表示 x = 0f, y = dividerOffsetY ), end = Offset( // FlowRowの一番右まで dividerを表示 x = flowRowSize.width.toFloat(), y = dividerOffsetY ), strokeWidth = 1.dp.toPx() ) }
  13. 選択済の単語の領域にDividerを描画する @Preview @Composable private fun WordsPreview() { Column { Box

    { Words(words = sampleWords, showWord = false) Words(words = sampleWords.take(5), showWord = true) } Words(words = sampleWords, showWord = true) } }
  14. Composableの枠を超えてアニメーションする @Preview @Composable private fun WordsPreview() { Column { Box

    { Words(words = sampleWords, showWord = false) Words(words = selectedWords, showWord = true) } Words(words = sampleWords, showWord = true) } }
  15. Column { Box { Words {} Words {} } Words

    {} } Composableの枠を超えてアニメーションする
  16. Column { Box { Words {} Words { Word() }

    } Words { Word() } } Composableの枠を超えてアニメーションする
  17. Column { Box { Words {} Words { Word() }

    } Words { Word() } } 入れ子になったComposableを外のComposableに アニメーションで移動したい Composableの枠を超えてアニメーションする
  18. @Composable fun WordOrderChoice( words: List<WordEntity> = sampleWords, selectedWords: List<WordEntity> =

    emptyList() ) { Column( modifier = Modifier.drawWithContent { drawContent() // ここに単語のアニメーションを実装することで、 // Composableの入れ子を気にせずアニメーションできる } ) { Box { Words(words = words, showWord = false) Words(words = selectedWords) } Words(words = words, showWord = true) } } Composableの枠を超えてアニメーションする
  19. @Composable fun WordOrderChoice( words: List<WordEntity> = sampleWords, selectedWords: List<WordEntity> =

    emptyList() ) { Column( modifier = Modifier.drawWithContent { drawContent() // ここに単語のアニメーションを実装することで、 // Composableの入れ子を気にせずアニメーションできる } ) { Box { Words(words = words, showWord = false) Words(words = selectedWords) } Words(words = words, showWord = true) } } Composableの枠を超えてアニメーションする
  20. Composableの枠を超えてアニメーションする 1. Composableの単語をタップ 2. Composableの単語を非表示にする 3. Composableの単語があった位置にCanvas上で単語を表示する 4. 移動先の単語一覧のComposableに非表示状態で単語を追加 ◦

    移動先の単語の位置の確認のため 5. Canvas上でアニメーションで単語を移動 6. 移動が終わったらComposableの単語を表示し、Canvas上の単語を 非表示にする
  21. /** 単語の表示状態 */ enum class WordVisibility { /** 単語を表示する */

    Visible, /** 単語全体を塗りつぶす(文字は表示されない) */ InvisibleFilled, /** 単語を透明にする(表示領域のみの確保) */ InvisibleTransparent, } data class WordEntity( val wordId: String, val word: String, val visibility: WordVisibility = WordVisibility.Visible, ) 単語を選択できるようにする
  22. @Composable private fun Word( word: WordEntity, showWord: Boolean = true,

    onGloballyPositioned: (LayoutCoordinates) -> Unit = {}, onClick: (WordEntity) -> Unit = {}, ) { Box( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .drawWithContent { if (showWord) { when (word.visibility) { WordVisibility.Visible -> drawContent() WordVisibility.InvisibleFilled -> drawRect(Color.LightGray) else -> Unit } } } // ... .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = { onClick(word) }, ) 単語を選択できるようにする
  23. @Composable private fun Word( word: WordEntity, showWord: Boolean = true,

    onGloballyPositioned: (LayoutCoordinates) -> Unit = {}, onClick: (WordEntity) -> Unit = {}, ) { Box( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .drawWithContent { if (showWord) { when (word.visibility) { WordVisibility.Visible -> drawContent() WordVisibility.InvisibleFilled -> drawRect(Color.LightGray) else -> Unit } } } // ... .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = { onClick(word) }, ) 単語を選択できるようにする
  24. @Composable private fun Word( word: WordEntity, showWord: Boolean = true,

    onGloballyPositioned: (LayoutCoordinates) -> Unit = {}, onClick: (WordEntity) -> Unit = {}, ) { Box( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .drawWithContent { if (showWord) { when (word.visibility) { WordVisibility.Visible -> drawContent() WordVisibility.InvisibleFilled -> drawRect(Color.LightGray) else -> Unit } } } // ... .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = { onClick(word) }, ) 単語を選択できるようにする
  25. @Composable private fun Words( words: List<WordEntity>, showWords: Boolean = true,

    onClickWord: (WordEntity) -> Unit = {}, ) { // ... FlowRow( // ... ) { words.forEach { entity -> Word( // ... word = entity, onClick = onClickWord, ) } } 単語を選択できるようにする
  26. @Composable fun WordOrderChoice( words: List<WordEntity>, selectedWords: List<WordEntity>, onClickChoicesWord: (WordEntity) ->

    Unit = {}, ) { Column( modifier = Modifier.background(Color.White) ) { Box { Words(words = words, showWords = false) Words(words = selectedWords) } Words( words = words, showWords = true, onClickWord = onClickChoicesWord ) } } 単語を選択できるようにする
  27. @Preview @Composable private fun WordOrderChoicePreview() { var words by remember

    { mutableStateOf(sampleWords) } var selectedWords by remember { mutableStateOf(listOf<WordEntity>()) } WordOrderChoice( words = words, selectedWords = selectedWords, onClickChoicesWord = { clickWord -> words = words.map { if (it.wordId == clickWord.wordId) { it.copy(visibility = WordVisibility.InvisibleFilled) } else { it } } selectedWords = selectedWords.toMutableList().apply { add(clickWord) } } ) } 単語を選択できるようにする
  28. private fun ContentDrawScope.drawWord( textLayoutResult: TextLayoutResult, wordSize: Size, wordTopLeft: Offset, )

    { if (wordTopLeft == Offset.Unspecified || wordSize == Size.Zero) return // テキストがチップの中央に表示されるように計算して配置する val textTopLeft = Offset( wordTopLeft.x + wordSize.width / 2f - textLayoutResult.size.width / 2f, wordTopLeft.y + wordSize.height / 2f - textLayoutResult.size.height / 2f, ) // チップの背景色 drawRoundRect( color = Color.White, topLeft = wordTopLeft, size = wordSize, cornerRadius = CornerRadius(8.dp.toPx()) ) // ... Canvasで単語を描画する
  29. private fun ContentDrawScope.drawWord( textLayoutResult: TextLayoutResult, wordSize: Size, wordTopLeft: Offset, )

    { if (wordTopLeft == Offset.Unspecified || wordSize == Size.Zero) return // テキストがチップの中央に表示されるように計算して配置する val textTopLeft = Offset( wordTopLeft.x + wordSize.width / 2f - textLayoutResult.size.width / 2f, wordTopLeft.y + wordSize.height / 2f - textLayoutResult.size.height / 2f, ) // チップの背景色 drawRoundRect( color = Color.White, topLeft = wordTopLeft, size = wordSize, cornerRadius = CornerRadius(8.dp.toPx()) ) // ... Canvasで単語を描画する
  30. private fun ContentDrawScope.drawWord( textLayoutResult: TextLayoutResult, wordSize: Size, wordTopLeft: Offset, )

    { if (wordTopLeft == Offset.Unspecified || wordSize == Size.Zero) return // テキストがチップの中央に表示されるように計算して配置する val textTopLeft = Offset( wordTopLeft.x + wordSize.width / 2f - textLayoutResult.size.width / 2f, wordTopLeft.y + wordSize.height / 2f - textLayoutResult.size.height / 2f, ) // チップの背景色 drawRoundRect( color = Color.White, topLeft = wordTopLeft, size = wordSize, cornerRadius = CornerRadius(8.dp.toPx()) ) // ... Canvasで単語を描画する
  31. private fun ContentDrawScope.drawWord( textLayoutResult: TextLayoutResult, wordSize: Size, wordTopLeft: Offset, )

    { if (wordTopLeft == Offset.Unspecified || wordSize == Size.Zero) return // テキストがチップの中央に表示されるように計算して配置する val textTopLeft = Offset( wordTopLeft.x + wordSize.width / 2f - textLayoutResult.size.width / 2f, wordTopLeft.y + wordSize.height / 2f - textLayoutResult.size.height / 2f, ) // 単語の背景色 drawRoundRect( color = Color.White, topLeft = wordTopLeft, size = wordSize, cornerRadius = CornerRadius(8.dp.toPx()) ) // ... Canvasで単語を描画する
  32. private fun ContentDrawScope.drawWord( textLayoutResult: TextLayoutResult, wordSize: Size, wordTopLeft: Offset, )

    { // ... // 単語の枠線 drawRoundRect( color = Color.LightGray, topLeft = wordTopLeft, size = wordSize, cornerRadius = CornerRadius(8.dp.toPx()), style = Stroke(2.dp.toPx()) ) // 単語のテキスト drawText( textLayoutResult, topLeft = textTopLeft, ) } Canvasで単語を描画する
  33. data class DrawWord( /* 描画する単語の情報 */ val word: WordEntity, /*

    単語のテキストの測定結果 */ val textLayoutResult: TextLayoutResult? = null, /* 単語のサイズ */ val size: Size = Size.Zero, /* 単語の初期位置 */ val originalTopLeft: Offset = Offset.Zero, /* 単語の現在の位置(アニメーション中はこれを更新する) */ val topLeft: Offset = Offset.Zero, /** 単語がアニメーション中かどうか */ val isInAnimation: Boolean = false, ) Canvas上で単語をアニメーションするための準備
  34. @Composable fun WordOrderChoice( // ... ) { val textMeasurer =

    rememberTextMeasurer() val coroutineScope = rememberCoroutineScope() val drawWords = remember { mutableStateMapOf<String, DrawWord>() } var parentLayoutCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } Column( modifier = Modifier .background(Color.White) .drawWithContent { drawContent() drawWords.forEach { (_, drawWord) -> drawWord.textLayoutResult?.let { textLayoutResult -> drawWord(textLayoutResult, drawWord.size, drawWord.topLeft) } } } .onGloballyPositioned { parentLayoutCoordinates = it } ) { Canvas上で単語をアニメーションするための準備
  35. @Composable fun WordOrderChoice( // ... ) { val textMeasurer =

    rememberTextMeasurer() val coroutineScope = rememberCoroutineScope() val drawWords = remember { mutableStateMapOf<String, DrawWord>() } var parentLayoutCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } Column( modifier = Modifier .background(Color.White) .drawWithContent { drawContent() drawWords.forEach { (_, drawWord) -> drawWord.textLayoutResult?.let { textLayoutResult -> drawWord(textLayoutResult, drawWord.size, drawWord.topLeft) } } } .onGloballyPositioned { parentLayoutCoordinates = it } ) { Canvas上で単語をアニメーションするための準備
  36. @Composable fun WordOrderChoice( // ... ) { val textMeasurer =

    rememberTextMeasurer() val coroutineScope = rememberCoroutineScope() val drawWords = remember { mutableStateMapOf<String, DrawWord>() } var parentLayoutCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } Column( modifier = Modifier .background(Color.White) .drawWithContent { drawContent() drawWords.forEach { (_, drawWord) -> drawWord.textLayoutResult?.let { textLayoutResult -> drawWord(textLayoutResult, drawWord.size, drawWord.topLeft) } } } .onGloballyPositioned { parentLayoutCoordinates = it } ) { Canvas上で単語をアニメーションするための準備
  37. @Composable fun WordOrderChoice( // ... ) { val textMeasurer =

    rememberTextMeasurer() val coroutineScope = rememberCoroutineScope() val drawWords = remember { mutableStateMapOf<String, DrawWord>() } var parentLayoutCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } Column( modifier = Modifier .background(Color.White) .drawWithContent { drawContent() drawWords.forEach { (_, drawWord) -> drawWord.textLayoutResult?.let { textLayoutResult -> drawWord(textLayoutResult, drawWord.size, drawWord.topLeft) } } } .onGloballyPositioned { parentLayoutCoordinates = it } ) { Canvas上で単語をアニメーションするための準備
  38. @Composable private fun Words( // ... onClickWord: (WordEntity, LayoutCoordinates) ->

    Unit = { _, _ -> }, onWordPositionChanged: (WordEntity, LayoutCoordinates) -> Unit = { _, _ -> }, ) { val wordPositions = remember { mutableMapOf<String, LayoutCoordinates>() } FlowRow(...) { words.forEach { entity -> Word( // ... onGloballyPositioned = { wordPositions[entity.wordId] = it onWordPositionChanged(entity, it) }, onClick = { wordPositions[entity.wordId]?.let { layoutCoordinates -> onClickWord(it, layoutCoordinates) } }, ) } 単語をタップしたときに単語の位置を取得する
  39. @Composable private fun Words( // ... onClickWord: (WordEntity, LayoutCoordinates) ->

    Unit = { _, _ -> }, onWordPositionChanged: (WordEntity, LayoutCoordinates) -> Unit = { _, _ -> }, ) { val wordPositions = remember { mutableMapOf<String, LayoutCoordinates>() } FlowRow(...) { words.forEach { entity -> Word( // ... onGloballyPositioned = { wordPositions[entity.wordId] = it onWordPositionChanged(entity, it) }, onClick = { wordPositions[entity.wordId]?.let { layoutCoordinates -> onClickWord(it, layoutCoordinates) } }, ) } 単語をタップしたときに単語の位置を取得する
  40. @Composable private fun Words( // ... onClickWord: (WordEntity, LayoutCoordinates) ->

    Unit = { _, _ -> }, onWordPositionChanged: (WordEntity, LayoutCoordinates) -> Unit = { _, _ -> }, ) { val wordPositions = remember { mutableMapOf<String, LayoutCoordinates>() } FlowRow(...) { words.forEach { entity -> Word( // ... onGloballyPositioned = { wordPositions[entity.wordId] = it onWordPositionChanged(entity, it) }, onClick = { wordPositions[entity.wordId]?.let { layoutCoordinates -> onClickWord(it, layoutCoordinates) } }, ) } 単語をタップしたときに単語の位置を取得する
  41. @Composable fun WordOrderChoice( words: List<WordEntity> = sampleWords, selectedWords: List<WordEntity> =

    emptyList() ) { Column(...) { Box { Words(words = words, showWord = false) Words(words = selectedWords) } Words(words = words, showWord = true) } } 選択肢の単語一覧にタップイベントを実装する
  42. Words( // ... onClickWord = { entity, layoutCoordinates -> if

    ( drawWords.containsKey(entity.wordId) || selectedWords.any { it.wordId == entity.wordId } ) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  43. Words( // ... onClickWord = { entity, layoutCoordinates -> if

    ( drawWords.containsKey(entity.wordId) || selectedWords.any { it.wordId == entity.wordId } ) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  44. Words( // ... onClickWord = { entity, layoutCoordinates -> if

    ( drawWords.containsKey(entity.wordId) || selectedWords.any { it.wordId == entity.wordId } ) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  45. Words( // ... onClickWord = { entity, layoutCoordinates -> if

    ( drawWords.containsKey(entity.wordId) || selectedWords.any { it.wordId == entity.wordId } ) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  46. Words( words = words, showWords = true, onClickWord = {

    entity, layoutCoordinates -> // ... onClickChoicesWord( // タップされた単語 entity.copy(visibility = WordVisibility.InvisibleFilled), // 移動先の単語 entity.copy(visibility = WordVisibility.InvisibleTransparent), ) drawWords[entity.wordId] = drawWord } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  47. Words( words = words, showWords = true, onClickWord = {

    entity, layoutCoordinates -> // ... onClickChoicesWord( // タップされた単語 entity.copy(visibility = WordVisibility.InvisibleFilled), // 移動先の単語 entity.copy(visibility = WordVisibility.InvisibleTransparent), ) drawWords[entity.wordId] = drawWord } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  48. Words( words = words, showWords = true, onClickWord = {

    entity, layoutCoordinates -> // ... onClickChoicesWord( // タップされた単語 entity.copy(visibility = WordVisibility.InvisibleFilled), // 移動先の単語 entity.copy(visibility = WordVisibility.InvisibleTransparent), ) drawWords[entity.wordId] = drawWord } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  49. Words( words = words, showWords = true, onClickWord = {

    entity, layoutCoordinates -> // ... onClickChoicesWord( // タップされた単語 entity.copy(visibility = WordVisibility.InvisibleFilled), // 移動先の単語 entity.copy(visibility = WordVisibility.InvisibleTransparent), ) drawWords[entity.wordId] = drawWord } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  50. @Composable fun WordOrderChoice( words: List<WordEntity> = sampleWords, selectedWords: List<WordEntity> =

    emptyList() ) { Column(...) { Box { Words(words = words, showWord = false) Words(words = selectedWords) } Words(words = words, showWord = true) } } 選択肢の単語を選択済に移動するアニメーションを実装する
  51. Words( words = selectedWords, onWordPositionChanged = { entity, layoutCoordinates ->

    val targetWords = drawWords.filter { (_, drawWord) -> drawWord.isInAnimation.not() && drawWord.word.wordId == entity.wordId } if (targetWords.isEmpty()) return@Words // ... } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  52. Words( words = selectedWords, onWordPositionChanged = { entity, layoutCoordinates ->

    val targetWords = drawWords.filter { (_, drawWord) -> drawWord.isInAnimation.not() && drawWord.word.wordId == entity.wordId } if (targetWords.isEmpty()) return@Words // ... } ) 選択肢の単語を選択済に移動するアニメーションを実装する
  53. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  54. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  55. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  56. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  57. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  58. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  59. onWordPositionChanged = { entity, layoutCoordinates -> // ... coroutineScope.launch {

    val animationWords = targetWords.map { (_, drawWord) -> async { drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, initialValue = drawWord.originalTopLeft, targetValue = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } animationWords.awaitAll() 選択肢の単語を選択済に移動するアニメーションを実装する
  60. @Composable private fun Words( // ... onWordPositionChanged: (WordEntity, LayoutCoordinates) ->

    Unit = { _, _ -> }, onWordPositionsChanged: (Map<String, LayoutCoordinates>) -> Unit = {}, ) { val wordPositions = remember { mutableMapOf<String, LayoutCoordinates>() } FlowRow(...) { words.forEach { entity -> Word( word = entity, showWord = showWords, onGloballyPositioned = { wordPositions[entity.wordId] = it onWordPositionChanged(entity, it) onWordPositionsChanged(wordPositions) }, // ... 選択肢の単語の位置を取得できるようにする
  61. @Composable private fun Words( // ... onWordPositionChanged: (WordEntity, LayoutCoordinates) ->

    Unit = { _, _ -> }, onWordPositionsChanged: (Map<String, LayoutCoordinates>) -> Unit = {}, ) { val wordPositions = remember { mutableMapOf<String, LayoutCoordinates>() } FlowRow(...) { words.forEach { entity -> Word( word = entity, showWord = showWords, onGloballyPositioned = { wordPositions[entity.wordId] = it onWordPositionChanged(entity, it) onWordPositionsChanged(wordPositions) }, // ... 選択肢の単語の位置を取得できるようにする
  62. val choicesWordPositions = remember { mutableStateMapOf<String, LayoutCoordinates>() } Words( //

    ... onWordPositionsChanged = { choicesWordPositions.clear() choicesWordPositions.putAll(it) } ) 選択肢の単語の位置を取得できるようにする
  63. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    if (drawWords.containsKey(entity.wordId)) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) onClickSelectedWord(entity.copy(visibility = WordVisibility.InvisibleTransparent)) drawWords[entity.wordId] = drawWord // ... } ) 選択肢の単語の位置を取得できるようにする
  64. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    if (drawWords.containsKey(entity.wordId)) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) onClickSelectedWord(entity.copy(visibility = WordVisibility.InvisibleTransparent)) drawWords[entity.wordId] = drawWord // ... } ) 選択肢の単語の位置を取得できるようにする
  65. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    if (drawWords.containsKey(entity.wordId)) return@Words val drawWordOffset = parentLayoutCoordinates ?.localPositionOf(layoutCoordinates, Offset.Zero) ?: return@Words val drawWord = DrawWord( word = entity, topLeft = drawWordOffset, originalTopLeft = drawWordOffset, size = layoutCoordinates.size.toSize(), textLayoutResult = if (entity.word.isEmpty()) { null } else { // 単語が押されたとき、押された単語のテキストを測定し、その情報を保持する textMeasurer.measure(AnnotatedString(entity.word), drawWordTextStyle) } ) onClickSelectedWord(entity.copy(visibility = WordVisibility.InvisibleTransparent)) drawWords[entity.wordId] = drawWord // ... } ) 選択肢の単語の位置を取得できるようにする
  66. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    // ... coroutineScope.launch { val targetLayoutCoordinates = choicesWordPositions[entity.wordId] ?: return@launch drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, drawWord.originalTopLeft, parentLayoutCoordinates?.localPositionOf(targetLayoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedWordAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } ) 選択肢の単語の位置を取得できるようにする
  67. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    // ... coroutineScope.launch { val targetLayoutCoordinates = choicesWordPositions[entity.wordId] ?: return@launch drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, drawWord.originalTopLeft, parentLayoutCoordinates?.localPositionOf(targetLayoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedWordAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } ) 選択肢の単語の位置を取得できるようにする
  68. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    // ... coroutineScope.launch { val targetLayoutCoordinates = choicesWordPositions[entity.wordId] ?: return@launch drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, drawWord.originalTopLeft, parentLayoutCoordinates?.localPositionOf(targetLayoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedWordAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } ) 選択肢の単語の位置を取得できるようにする
  69. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    // ... coroutineScope.launch { val targetLayoutCoordinates = choicesWordPositions[entity.wordId] ?: return@launch drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, drawWord.originalTopLeft, parentLayoutCoordinates?.localPositionOf(targetLayoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedWordAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } ) 選択肢の単語の位置を取得できるようにする
  70. Words( words = selectedWords, onClickWord = { entity, layoutCoordinates ->

    // ... coroutineScope.launch { val targetLayoutCoordinates = choicesWordPositions[entity.wordId] ?: return@launch drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(isInAnimation = true) } animate( Offset.VectorConverter, drawWord.originalTopLeft, parentLayoutCoordinates?.localPositionOf(targetLayoutCoordinates, Offset.Zero) ?: Offset.Zero, ) { value, _ -> drawWords[entity.wordId]?.let { drawWords[entity.wordId] = it.copy(topLeft = value) } } onSelectedWordAnimationFinished(entity.copy(visibility = WordVisibility.Visible)) drawWords.remove(entity.wordId) } } ) 選択肢の単語の位置を取得できるようにする