Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Jetpack Compose で SharedElementTransitionっぽい挙動を...
Search
Shun Miyazaki
May 21, 2023
200
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Jetpack Compose で SharedElementTransitionっぽい挙動をやってみる
Shun Miyazaki
May 21, 2023
More Decks by Shun Miyazaki
See All by Shun Miyazaki
DroidKaigi2025アプリのスクリーンショットテストが失敗していた原因を調べてみた
shunm
0
260
DroidKaigi アプリに初コントリビュートしました
shunm
0
120
Kotlinで簡単なDSLを作ってみる
shunm
0
74
Google I/O 2024 - Gemeni API on Androidの解説
shunm
0
960
Google I/O 2023 - Debugging Jetpack Composeについて
shunm
0
1.1k
Featured
See All Featured
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
69
65k
The SEO identity crisis: Don't let AI make you average
varn
0
550
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.6k
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.6k
Balancing Empowerment & Direction
lara
6
1.3k
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
Being A Developer After 40
akosma
91
590k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
410
StorybookのUI Testing Handbookを読んだ
zakiyama
31
6.9k
Mobile First: as difficult as doing things right
swwweet
225
10k
Put a Button on it: Removing Barriers to Going Fast.
kastner
60
4.6k
The Straight Up "How To Draw Better" Workshop
denniskardys
239
140k
Transcript
Jetpack Composeで、 SharedElementTransitionっぽい挙動やってみる 宮﨑 瞬 1
目次 ・ SharedElementTransisionとは ・ Composeで実現する方法 - movableContentOfとは - LookaheadLayoutとは ・上記2つを組み合わせることで、シームレスな遷移方法を実現
2
About Me 宮﨑 瞬 ・ Androidエンジニア ・ Github shunm-999 個人開発のAndroidアプリを、Githubで公開してます
Gitの技術書をAmazon Kindleで出版してます 3
SharedElementTransitionとは? 4
SharedElementTransitionって、こんな動き Viewベースで、 共通要素がある画面間を遷移する場合に、 シームレスに遷移するための仕組み。 5
画面構成 6 MainActivity FirstFragment SecondFragment SharedElementTransition
Composeでも、こんな動きさせたいな〜 7
できますよ!!! 8
movableContentOf & LookaheadLayout 9
movableContentOfとは ・Recompositionの必要なく、contentを移動できる仕組み ・Composableを使い回して、不要なRecompositionを避けることがで きる 10
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を渡す ←複数箇所で使用できる ←複数箇所で使用できる
実際の動き 12 ←複数箇所で使用できる ←複数箇所で使用できる
movableContentOf使う場合/使わない場合の Recompositionの差 13 ←複数箇所で使用できる ←複数箇所で使用できる movableContentOf使わない場合 movableContentOf使う場合
LookaheadLayoutとは ・ レイアウトが変更される時、変更後のレイアウトを先読みして、 フレームごとのサイズと位置のアニメーションができる ・ 先読みが走るのは、Composeのツリーが変更された時か、 Stateの変更時 14
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を配置する処理
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押下で、画像の左下寄せ⇔右上寄せを切り替える ← レイアウトのサイズをアニメーション ← レイアウトの位置をアニメーション
実際の動き 17
組み合わせると…? movableContentOf ✖️ LookaheadLayout = 18
SharedElementTransitionっぽい動きに! 19
ご清聴ありがとうございました!! 20
おまけ 21
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時に呼ばれる。 事前計算されたレイアウトのサイズが取得できる。
アニメーション用の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) } } } }
アニメーション用の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) } } }
アニメーション用の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) } } } }
アニメーション用の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) } } } }