Slide 1

Slide 1 text

LookaheadLayoutのプチ解釈 Mobile勉強会 Wantedly × チームラボ #7 Suraj Sau(すーじ)

Slide 2

Slide 2 text

自己紹介 ● インド人・2022年7月やっと日本来れた ● Android @ 株式会社グッドパッチ ● 日々デザイナー仲間たちと一緒に仕事できるのが好き! 仲間募集してます!😇 ● Compose 大好き・KMM興味津々 * 日本語間違ったらお許しください 🙇 @_SUR4J_

Slide 3

Slide 3 text

Shared Element Transition Composeで画面遷移でよく使われる Shared Element Transition を実現しようと思ったらどう進めますか? val showDetails by remember { .. } if (showDetails) { DetailsScreen(..) else ListScreen(..) https://giphy.com/gifs/erMZH0BMrIsnyAJ79D

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 Recompositionで途切れるので、それをさせないため に movableContentOf() を使えばOK! 👍 3. 共通のレイアウト@Composableの size とoffsetをアニ メーションさせる。 ✅

Slide 8

Slide 8 text

● 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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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() を使ってターゲットバ リューまでアニメーションを実行させる。

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

● 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()

Slide 14

Slide 14 text

● 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()

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Shared Element Transition 1. 共通のレイアウト@Composableを遷移前後の画面両 方に渡す。 2. 遷移中レイアウト内のアニメーションを途切れないよう にする。 3. 共通のレイアウト@Composableの size とoffsetをアニ メーションさせる。 次の画面のまで animateXXAsState()を実行すれば いいんじゃない? 次の画面が計算される前に子供 size と offset 取ってもら えるようなAPIあれば楽だな〜 ✅ ✅

Slide 17

Slide 17 text

Doris Liuさんからこのツイート https://twitter.com/doris4lt/status/1531364543305175041 💡 😲 おお...

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

LookaheadLayout 事前に size と offset 計算されるステップを lookahead(先読み)と言 われてます。 size: 1080 x 550 offset: (0, 0)

Slide 20

Slide 20 text

LookaheadLayout 事前に size と offset 計算されるステップを lookahead(先読み)と 言われてます。 この場合、lookahead size = 1080 x 550 と lookahead offset = (0,0) になります。 DetailsScreen{}で size: 1080 x 550 offset: (0, 0) になりますよ〜

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Modifier.intermediateLayout {} ● ここで lookaheadSize にもアクセスできて、 val sizeAnimation: Animatable 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

Slide 24

Slide 24 text

Modifier.intermediateLayout {} ● ここでlookaheadSizeにもアクセスできて、それに向 かってアニメーションを実行させます。 val sizeAnimation: Animatable 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

Slide 25

Slide 25 text

Modifier.intermediateLayout {} ● ここでlookaheadSizeにもアクセスできて、それに向 かってアニメーションを実行させます。 ● 実行させたアニメーションの途中のバリューを使って中間レイ アウトのサイズと配置を決めます。 val sizeAnimation: Animatable 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

Slide 26

Slide 26 text

Modifier.onPlaced {} val offsetAnimation: Animatable? 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) 内に調整されたらこのコー ルバックが呼び出されます。

Slide 27

Slide 27 text

Modifier.onPlaced {} val offsetAnimation: Animatable? 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)

Slide 28

Slide 28 text

Modifier.onPlaced {} ● 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 ● .localLookaheadPositionOf() を使って子供の lookahead offset を取得して、それに向かってアニメーション を実行します。 Target offset: (0, 0) (targetOffset) Initial offset: (44, 902) val offsetAnimation: Animatable? 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() }

Slide 29

Slide 29 text

Modifier.onPlaced {} ● 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 ● .localLookaheadPositionOf() を使って子供の lookahead offset を取得して、それに向かってアニメーション を実行します。 ● .localPositionOf() は子供の親 (LookaheadLayout) に対して現在の offset を返します。 val offsetAnimation: Animatable? 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() }

Slide 30

Slide 30 text

Modifier.onPlaced {} ● 子供が親 (LookaheadLayout) 内に調整されたらこのコール バックが呼び出されます。 ● .localLookaheadPositionOf() を使って子供の lookahead offset を取得して、それに向かってアニメーション を実行します。 ● .localPositionOf() は子供の親 (LookaheadLayout) に対して現在の offset を返します。 ● 現在の offset と lookahead offset に向かってるアニメー ションを使って次の中間レイアウトの offset を決めま す。 val offsetAnimation: Animatable? 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) } }

Slide 31

Slide 31 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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

Slide 32

Slide 32 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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 を取得する

Slide 33

Slide 33 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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 を取得する

Slide 34

Slide 34 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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のサイズ決める

Slide 35

Slide 35 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 36

Slide 36 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 37

Slide 37 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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 が呼び出される

Slide 38

Slide 38 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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のサイズ決める

Slide 39

Slide 39 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 40

Slide 40 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 41

Slide 41 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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 が 呼び出される

Slide 42

Slide 42 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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のサイズ決める

Slide 43

Slide 43 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 44

Slide 44 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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を決める

Slide 45

Slide 45 text

var sizeAnimation: Animatable by .. var targetSize: IntSize? by remember { mutableStateOf(null) } var offsetAnimation: Animatable 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) } }

Slide 46

Slide 46 text

github/Orbital skydoves が今までの話をまとめてライブラリー化してくれました。ぜひ使ってみてください。 Orbital

Slide 47

Slide 47 text

参考リンク ● cs.android.com/LookaheadLayoutSample ● github/Orbital ● Introducing LookaheadLayout ● AOSP/Introduce LookaheadLayout - 1

Slide 48

Slide 48 text

ありがとうございました!