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

LookaheadLayout のプチ解釈

Avatar for すーじ すーじ
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

Avatar for すーじ

すーじ

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) } }