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

Jetpack Compose で SharedElementTransitionっぽい挙動をやってみる

Shun Miyazaki
May 21, 2023
90

Jetpack Compose で SharedElementTransitionっぽい挙動をやってみる

Shun Miyazaki

May 21, 2023
Tweet

Transcript

  1. movableContentOfの使い方(一例) 11 Composable fun MovableContentOfSample() { AndroidAnimationCookbookTheme { Surface {

    var isColumn by remember { mutableStateOf(true) } val texts = remember { movableContentOf { Text(text = "Title") Text(text = "Message") } } Column { Button(onClick = { isColumn = !isColumn }) { Text(text = if (isColumn) "to Row" else "to Column") } if (isColumn) { Column { texts() } } else { Row { texts() } } } } } } ←movableContentOfに、使用したいComposableを渡す ←複数箇所で使用できる ←複数箇所で使用できる
  2. LookaheadLayoutについて 15 @OptIn(ExperimentalComposeUiApi::class) @Composable fun LookaheadLayoutSample() { LookaheadLayout ( content

    = { // TODO }, measurePolicy = { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { placeables.forEach { placeable -> placeable.place(0, 0) } } } ) } ←ここに使用したいComposableを配置する ←子のComposableを配置する処理
  3. LookahaedLayoutの使い方(一例) 16 OptIn(ExperimentalComposeUiApi::class) @Composable fun LookaheadLayoutSample() { LookaheadLayout( content =

    { var isLeft: Boolean by remember { mutableStateOf(true) } Column( horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { isLeft = !isLeft }) { Text(text = "Switch Layout") } Row( modifier = Modifier .fillMaxWidth() .height(200.dp), horizontalArrangement = if (isLeft) { Arrangement.Start } else { Arrangement.End }, verticalAlignment = if (isLeft) { Alignment.Bottom } else { Alignment.Top } ) { BirdImage( modifier = Modifier .height( if (isLeft) { 100.dp } else { 200.dp } ) .animateLayoutSize( lookaheadLayoutScope = this@LookaheadLayout ) .animateLayoutPlacement( lookaheadLayoutScope = this@LookaheadLayout ) ) } } }, measurePolicy = { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { placeables.forEach { placeable -> placeable.place(0, 0) } } } ) } ← Button押下で、画像の左下寄せ⇔右上寄せを切り替える ← レイアウトのサイズをアニメーション ← レイアウトの位置をアニメーション
  4. LookaheadLayoutScopeの使い方 22 Suppress("ComposableLambdaParameterPosition") @ExperimentalComposeUiApi @UiComposable @Composable fun LookaheadLayout( content: @Composable

    @UiComposable LookaheadLayoutScope.() -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) { ExperimentalComposeUiApi interface LookaheadLayoutScope { fun Modifier.onPlaced( onPlaced: ( lookaheadScopeCoordinates: LookaheadLayoutCoordinates, layoutCoordinates: LookaheadLayoutCoordinates ) -> Unit ): Modifier fun Modifier.intermediateLayout( measure: MeasureScope.( measurable: Measurable, constraints: Constraints, lookaheadSize: IntSize ) -> MeasureResult, ): Modifier } onPlaced : 再Layout時に呼ばれる。 位置の調整ができる。 現在位置と、先読み位置の取得が可能。 ←contentで、 LookaheadLayoutScopeを使える intermediateLayout : 再measure時に呼ばれる。 事前計算されたレイアウトのサイズが取得できる。
  5. アニメーション用のModifierの作成(サイズ変更) 23 @OptIn(ExperimentalComposeUiApi::class) fun Modifier.animateLayoutSize(lookaheadLayoutScope: LookaheadLayoutScope) = composed { var

    sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember { mutableStateOf(null) } var targetSize: IntSize? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { snapshotFlow { targetSize }.collect { target -> if (target == null || target == sizeAnimation?.targetValue) { return@collect } sizeAnimation?.animateTo(target) ?: run { sizeAnimation = Animatable(target, IntSize.VectorConverter) } } } with(lookaheadLayoutScope) { [email protected] { measurable, constraints, lookaheadSize -> targetSize = lookaheadSize val (width, height) = sizeAnimation?.value ?: lookaheadSize val animateConstraints = Constraints.fixed(width, height) val placeable = measurable.measure(animateConstraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } } } }
  6. アニメーション用のModifierの作成(位置変更) 24 OptIn(ExperimentalComposeUiApi::class) fun Modifier.animateLayoutPlacement(lookaheadLayoutScope: LookaheadLayoutScope) = composed { var

    offsetAnimation: Animatable<IntOffset, AnimationVector2D>? by remember { mutableStateOf(null) } var currentOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } var targetOffset: IntOffset by remember { mutableStateOf(IntOffset.Zero) } LaunchedEffect(Unit) { snapshotFlow { targetOffset }.collect { target -> if (target == offsetAnimation?.targetValue) { return@collect } offsetAnimation?.animateTo(target) ?: run { offsetAnimation = Animatable(target, IntOffset.VectorConverter) } } }
  7. アニメーション用のModifierの作成(位置変更) 25 with(lookaheadLayoutScope) { this@composed .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->

    Timber.tag("LookaheadLayout").d("onPlaced") currentOffset = lookaheadScopeCoordinates .localPositionOf( sourceCoordinates = layoutCoordinates, relativeToSource = Offset.Zero ) .round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf( sourceCoordinates = layoutCoordinates ) .round() } .intermediateLayout { measurable, constraints, lookaheadSize -> Timber.tag("LookaheadLayout").d("intermediateLayout $targetOffset $currentOffset") val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = if (offsetAnimation == null) { targetOffset - currentOffset } else { (offsetAnimation?.value ?: IntOffset.Zero) - currentOffset } placeable.place(x, y) } } } }
  8. アニメーション用のModifierの作成(位置変更) 26 with(lookaheadLayoutScope) { this@composed .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->

    Timber.tag("LookaheadLayout").d("onPlaced") currentOffset = lookaheadScopeCoordinates .localPositionOf( sourceCoordinates = layoutCoordinates, relativeToSource = Offset.Zero ) .round() targetOffset = lookaheadScopeCoordinates .localLookaheadPositionOf( sourceCoordinates = layoutCoordinates ) .round() } .intermediateLayout { measurable, constraints, lookaheadSize -> Timber.tag("LookaheadLayout").d("intermediateLayout $targetOffset $currentOffset") val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { val (x, y) = if (offsetAnimation == null) { targetOffset - currentOffset } else { (offsetAnimation?.value ?: IntOffset.Zero) - currentOffset } placeable.place(x, y) } } } }