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

LookaheadLayout のプチ解釈

すーじ
November 30, 2022

LookaheadLayout のプチ解釈

Mobile勉強会 Wantedly × チームラボ #7の発表資料
Compose 1.3.0 で追加されたLookaheadLayoutを使って Shared Element Transitionを実現できる話についてLTしました。

Gifs 見えないのでリンク用意します。

Shared Element Transition: https://giphy.com/gifs/erMZH0BMrIsnyAJ79D
共通レイアウトを両方渡す:https://giphy.com/gifs/KMsgXtxgbvKKpp8kNu
movableContentOf(): https://giphy.com/gifs/yf0rNHVo8LHdDUrh05
animatedXXAsState(): https://giphy.com/gifs/txF2T4WdvONebtLFdq

すーじ

November 30, 2022
Tweet

More Decks by すーじ

Other Decks in Education

Transcript

  1. Shared Element Transition Composeで画面遷移でよく使われる Shared Element Transition を実現しようと思ったらどう進めますか? val showDetails

    by remember { .. } if (showDetails) { DetailsScreen(..) else ListScreen(..) https://giphy.com/gifs/erMZH0BMrIsnyAJ79D
  2. Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面 両方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよ うにする。 3.

    共通のレイアウト@Composableの size とoffsetをア ニメーションさせる。 https://giphy.com/gifs/erMZH0BMrIsnyAJ79D
  3. Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 3.

    共通のレイアウト@Composableの size とoffsetをア ニメーションさせる。 val itemLayout = CommonItemLayout() if (showDetails) DetailsScreen(header = { itemLayout() }, ..) else ListScreen(itemLayout = { itemLayout() }, ..) @Composable fun CommonItemLayout() {..} https://giphy.com/gifs/KMsgXtxgbvKKpp8kNu
  4. • Compose 1.2.0 で追加されたAPI。 • ある @Composable lambda を 保特し

    (remember) ツリーの中、Recompositionせず移 動させることができます。 val itemLayout = movableContentOf(CommonItemLayout()) if (selectedItem != null) { DetailsScreen(header = { itemLayout(..) }, ..) } else { ListScreen(itemLayout = { itemLayout(..) }, ..) } cs.android.com/movabelContentOf Shared Element Transition movableContentOf() https://giphy.com/gifs/yf0rNHVo8LHdDUrh05
  5. Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 3.

    共通のレイアウト@Composableの size とoffsetをアニ メーションさせる。 次の画面まで animateXXAsState()を実行すれ ばいいんじゃない? ✅ ✅
  6. Shared Element Transition animateXXAsState() @Composable fun Sample(expand: Boolean) { val

    sideLength by animateSizeAsState( targetValue = if (expand) 200.dp else 20.dp ) Box(modifier = Modifier..size(sideLength)) } Composeで animateXXAsState() を使ってターゲットバ リューまでアニメーションを実行させる。
  7. val height by animateDpAsState( targetValue = if (showDetails) 200.dp //

    details header height else 72.dp // list item height ) val itemLayout = movableContentOf( CommonItemLayout(modifier = Modifier..height(height)) ) Shared Element Transition animateXXAsState() https://giphy.com/gifs/txF2T4WdvONebtLFdq
  8. • DetailsScreen と ListScreen で CommonItemLayout のサイ ズが分からないから、 200.dp, 72.dp

    みたいな magic number を使わ ないといけない。 val height by animateDpAsState( targetValue = if (showDetails) 200.dp // details header height else 72.dp // list item height ) val itemLayout = movableContentOf( CommonItemLayout(modifier = Modifier..height(height)) ) Shared Element Transition animateXXAsState()
  9. • DetailsScreen と ListScreen で CommonItemLayout のサイ ズが分からないから、 200.dp, 72.dp

    みたいな magic number を使わ ないといけない。 • 実現可能なんですけが、 magic number のスケーラビリティ問題はあり ますね。 val height by animateDpAsState( targetValue = if (showDetails) 200.dp // details header height else 72.dp // list item height ) val itemLayout = movableContentOf( CommonItemLayout(modifier = Modifier..height(height)) ) Shared Element Transition animateXXAsState()
  10. Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 3.

    共通のレイアウト@Composableの size とoffsetをアニ メーションさせる。 次の画面のまで animateXXAsState()を実行すれば いいんじゃない? ✅ ✅
  11. Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 3.

    共通のレイアウト@Composableの size とoffsetをアニ メーションさせる。 次の画面のまで animateXXAsState()を実行すれば いいんじゃない? 次の画面が計算される前に子供 size と offset 取ってもら えるようなAPIあれば楽だな〜 ✅ ✅
  12. LookaheadLayout • Compose 1.3.0 で追加されました。 • measure-layout pass(今の例だと、DetailsScreen が計算される前に) の前に子供のターゲット

    size と offset を 計算して取ってくれる Layout です。 @Composable fun LookaheadLayout( content: @Composable LookaheadLayoutScope.() -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) cs.android.com/LookaheadLayout
  13. LookaheadLayout 事前に size と offset 計算されるステップを lookahead(先読み)と 言われてます。 この場合、lookahead size

    = 1080 x 550 と lookahead offset = (0,0) になります。 DetailsScreen{}で size: 1080 x 550 offset: (0, 0) になりますよ〜
  14. LookaheadLayout • Compose 1.3.0 で追加されました。 • measure-layout pass の前に子供のターゲット size

    と offset を計算して取ってくれる Layout です。 • LookaheadLayoutScope 内で、新たに追加された2つの Modifier を使えます :Modifier.intermediateLayout{..} と Modifier.onPlaced {..} @Composable fun LookaheadLayout( content: @Composable LookaheadLayoutScope.() -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) cs.android.com/LookaheadLayout
  15. LookaheadLayout 意図としては、この lookahead size と offset に向けてアニメーション を実行するようなカスタム Modifier を作って変形したい子供に

    使います。 fun Modifier.sharedTransition( scope: LookaheadLayoutScope ) = composed { with(scope) { Modifier .onPlaced { _, _ -> .. // lookahead offset を取得する } .intermediateLayout { _, _, lookaheadSize -> .. // lookahead size を取得する layout {..} } } } LookaheadLayout( content = { .. ItemLayout(modifier = Modifier..sharedTransition(this)) } )
  16. Modifier.intermediateLayout {} • ここで lookaheadSize にもアクセスできて、 val sizeAnimation: Animatable<IntSize, AnimationVector2D>

    by .. var targetSize: IntSize? by remember { mutableStateOf(null) } snapshotFlow { targetSize } .collect { target -> // lookaheadSize 向けてアニメーション走らせる sizeAnimation.animateTo(target) } Modifier .intermediateLayout { measurable, constraints, lookaheadSize -> targetSize = lookaheadSize // 中間レイアウトのサイズ val intermediateSize = sizeAnimation.value val constraints = Constraints.fixed( intermediateSize.width, intermediateSize.height ) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val offset = //.. offset アニメーションから貰える placeable.place(offset.x, offset.y) } } Target lookaheadSize: 1080 x 550
  17. Modifier.intermediateLayout {} • ここでlookaheadSizeにもアクセスできて、それに向 かってアニメーションを実行させます。 val sizeAnimation: Animatable<IntSize, AnimationVector2D> by

    .. var targetSize: IntSize? by remember { mutableStateOf(null) } snapshotFlow { targetSize } .collect { target -> // lookaheadSize 向けてアニメーション走らせる sizeAnimation.animateTo(target) } Modifier .intermediateLayout { measurable, constraints, lookaheadSize -> targetSize = lookaheadSize // 中間レイアウトのサイズ val intermediateSize = sizeAnimation.value val constraints = Constraints.fixed( intermediateSize.width, intermediateSize.height ) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val offset = //.. offset アニメーションから貰える placeable.place(offset.x, offset.y) } } Initial size: 92 x 198 Target lookaheadSize: 1080 x 550
  18. Modifier.intermediateLayout {} • ここでlookaheadSizeにもアクセスできて、それに向 かってアニメーションを実行させます。 • 実行させたアニメーションの途中のバリューを使って中間レイ アウトのサイズと配置を決めます。 val sizeAnimation:

    Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by remember { mutableStateOf(null) } snapshotFlow { targetSize } .collect { target -> // lookaheadSize 向けてアニメーション走らせる sizeAnimation.animateTo(target) } Modifier .intermediateLayout { measurable, constraints, lookaheadSize -> targetSize = lookaheadSize // 中間レイアウトのサイズ val intermediateSize = sizeAnimation.value val constraints = Constraints.fixed( intermediateSize.width, intermediateSize.height ) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val offset = //.. offset アニメーションから貰える placeable.place(offset.x, offset.y) } } Initial size: 92 x 198 intermediateLayout #1 size: 1003 x 241 intermediateLayout #2 size: 1066 x 493 Target lookaheadSize: 1080 x 550
  19. Modifier.onPlaced {} val offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by .. var targetOffset:

    IntOffset? by remember { mutableStateOf(null) } var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } snapshotFlow { targetOffset } .collect { target -> // lookahead offset に向けてアニメーション走らせる offsetAnimation.animateTo(target) } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates) .round() placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero) .round() } • 子供が親 (LookaheadLayout) 内に調整されたらこのコー ルバックが呼び出されます。
  20. Modifier.onPlaced {} val offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by .. var targetOffset:

    IntOffset? by remember { mutableStateOf(null) } var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } snapshotFlow { targetOffset } .collect { target -> // lookahead offset に向けてアニメーション走らせる offsetAnimation.animateTo(target) } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates) .round() placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero) .round() } • 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 • .localLookaheadPositionOf() を使って子供の lookahead offset を取得して、 Initial offset: (44, 1188) Target offset: (0, 0)
  21. Modifier.onPlaced {} • 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 • .localLookaheadPositionOf() を使って子供の

    lookahead offset を取得して、それに向かってアニメーション を実行します。 Target offset: (0, 0) (targetOffset) Initial offset: (44, 902) val offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by .. var targetOffset: IntOffset? by remember { mutableStateOf(null) } var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } snapshotFlow { targetOffset } .collect { target -> // lookahead offset に向けてアニメーション実行する offsetAnimation.animateTo(target) } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates) .round() placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero) .round() }
  22. Modifier.onPlaced {} • 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 • .localLookaheadPositionOf() を使って子供の

    lookahead offset を取得して、それに向かってアニメーション を実行します。 • .localPositionOf() は子供の親 (LookaheadLayout) に対して現在の offset を返します。 val offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by .. var targetOffset: IntOffset? by remember { mutableStateOf(null) } var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } snapshotFlow { targetOffset } .collect { target -> // lookahead offset に向けてアニメーション走らせる offsetAnimation.animateTo(target) } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates) .round() placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero) .round() }
  23. Modifier.onPlaced {} • 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 • .localLookaheadPositionOf() を使って子供の

    lookahead offset を取得して、それに向かってアニメーション を実行します。 • .localPositionOf() は子供の親 (LookaheadLayout) に対して現在の offset を返します。 • 現在の offset と lookahead offset に向かってるアニメー ションを使って次の中間レイアウトの offset を決めま す。 val offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by .. var targetOffset: IntOffset? by remember { mutableStateOf(null) } var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } snapshotFlow { targetOffset } .collect { target -> // lookahead offset に向けてアニメーション走らせる offsetAnimation.animateTo(target) } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> targetOffset = .. placementOffset = .. } .intermediateLayout { measurable, _, lookaheadSize -> val placeable = .. // 中間レイアウトの placeable layout(..) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } }
  24. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } Let’s try visualising
  25. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } lookahead size を取得する
  26. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } lookahead offset を取得する
  27. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } sizeAnimationのバリューを使って中間レ イアウト#1のサイズ決める
  28. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } offsetAnimationと現在のoffset (placementOffset) を 使って中間レイアウト#1のoffsetを決める
  29. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } intermediateLayoutが中間レイアウト#1を決める
  30. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } 子供が調整されたので onPlaced が呼び出される
  31. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } sizeAnimationのバリューを使って中間レ イアウト#2のサイズ決める
  32. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } offsetAnimationと現在のoffset (placementOffset) を 使って中間レイアウト#2のoffsetを決める
  33. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } intermediateLayoutが中間レイアウト#2を決める
  34. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } また、子供が調整されたので onPlaced が 呼び出される
  35. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } intermediateLayout #3 sizeAnimation.value = 1080 x 550 sizeAnimationのバリューを使って中間レ イアウト#3のサイズ決める
  36. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } offsetAnimationと現在のoffset (placementOffset) を 使って中間レイアウト#3のoffsetを決める
  37. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } } intermediateLayoutが中間レイアウト#3を決める
  38. var sizeAnimation: Animatable<IntSize, AnimationVector2D> by .. var targetSize: IntSize? by

    remember { mutableStateOf(null) } var offsetAnimation: Animatable<IntOffset, AnimationVector2D> by .. var placementOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { launch { snapshotFlow { targetSize } .collect { target -> sizeAnimation.animateTo(target, ..) } } launch { snapshotFlow { targetOffset } .collect { target -> offsetAnimation.animateTo(target, ..) } } } Modifier .onPlaced { lookaheadScopeCoordinates, layoutCoordinates -> placementOffset = lookaheadScopeCoordinates .localPositionOf(layoutCoordinates, Offset.Zero).round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf(layoutCoordinates).round() } .intermediateLayout { measurable, _, lookaheadSize -> targetSize = lookaheadSize val actualSize = sizeAnimation.value val constraints = Constraints.fixed(actualSize.width, actualSize.height) val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = offsetAnimation.value - placementOffset placeable.place(x, y) } }