Android Compose Motion Sample
Fade:互いに完全に重なり合う要素の間に滑らかなシーケンスを作成すること。カードや他のコンテナの中の写真が差し替えられる際にフェードインするようなやつ。
AnimatedContent: targetState が変化した時に動的にコンテンツを transitionSpec で指定したように アニメーションさせるコンテナ。
targetStateが変更されるとContentTransformで指定されたように、変更前のtargetStateがoutする間に現在のstateがinするようなアニメーションが適用される。
SizeTransformなるものがnullで指定されない限り新しいコンテンツに対応するために必要に応じてサイズが調整される
このデモではボタンを押すごととに写真を入れ替えてフェードさせていくため、
targetStateはrememberさせたInt型の数字。
transitionSpecは差し替えで実装しており、初回は変更後の画像が不透明になるまでコンテンツを置いている
code:kotlin
private fun fade(
durationMillis: Int = 300
): AnimatedContentScope<Int>.() -> ContentTransform {
return {
ContentTransform(
// The initial content should stay until the target content is completely opaque.
initialContentExit = fadeOut(animationSpec = snap(delayMillis = durationMillis)),
// The target content fades in. This is shown on top of the initial content.
targetContentEnter = fadeIn(
animationSpec = tween(
durationMillis = durationMillis,
// LinearOutSlowInEasing is suitable for incoming elements.
easing = LinearOutSlowInEasing
)
)
)
}
}
ちなみにAnimatedContentのtraisitionSpecの初期値は
code:kotlin
fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with fadeOut(animationSpec = tween(90))
horis.icon ContentTransform , 最初のコンテンツだけExitを指定できるのか... それ移行もコンテンツのExitアニメーション指定できると挙動が統一されていて良さそうだけどどうなんだろう
horis.icon 実際にdurationMillisを5000とかにすると最初だけ違和感ある。どれくらいのアニメーション秒数かにもよりそうなのか
今は重なるように画像が置き換わっているが targetContentZIndex で重なり順序も変えることができる
horis.icon アニメーションの途中で押してるので変に見えるだけっぽい
DemoCard
AnimatedContent(targetState=, transitionSpec=)
targetStateの変更に応じて自動でアニメーションしてくれる
transitionSpecでアニメーションを選べる(デフォルトではFadeIn, Scallin)
code:kotlin
fun fadeThrough(
durationMillis: Int = 300
): AnimatedContentScope<Boolean>.() -> ContentTransform {
return {
ContentTransform(initialContentExit = ,targetContentEnter = ,sizeTransform = )
}
}
initialContentExit 状態が消える際のアニメーションを定義 (fadeOut()などが入る)
targetContentEnter 状態に入る際のアニメーションを定義 (fadeIn()などが入る)
sizeTransform 大きさの変化のアニメーションの遷移時間や遅延時間
code:kotlin
initialContentExit = fadeOut(
animationSpec = tween(
durationMillis = durationMillis * 3 / 8,
easing = FastOutLinearInEasing
)
)
fadeInなどはtweenを引数に取り
durationMillis 遷移にかかる時間
delayMillis 遷移する遅延時間
easing アニメーションの変化速度を減衰または加速するようにする
Mori Atsushi.icon デフォルトはspringなんだけどtweenとどう使い分けたらいいんだろう?
horis.icon targetContentEnterで指定しているduration + delayが引数のdurationに収束するのなるほど
共通の要素を持つ、開いたり閉じたりするレイアウト
ConstraintLayout
createRefsでID的なものを作成
code:kotlin
val (content, icon, divider,button) = createRefs()
内部はこんなコードになってるので、上記みたいに16個まで書ける
code:kotlin
inner class ConstrainedLayoutReferences internal constructor() {
operator fun component1() = createRef()
operator fun component2() = createRef()
operator fun component3() = createRef()
operator fun component4() = createRef()
operator fun component5() = createRef()
operator fun component6() = createRef()
operator fun component7() = createRef()
operator fun component8() = createRef()
operator fun component9() = createRef()
operator fun component10() = createRef()
operator fun component11() = createRef()
operator fun component12() = createRef()
operator fun component13() = createRef()
operator fun component14() = createRef()
operator fun component15() = createRef()
operator fun component16() = createRef()
}
それ以上もcreateRefを呼べば良い
Modifierで制約を書ける
code:kotlin
CardIcon(
modifier = Modifier.constrainAs(icon) {
top.linkTo(parent.top, margin = 16.dp)
end.linkTo(parent.end, margin = 16.dp)
}
)
Mori Atsushi.icon やっぱりこれくらいのUIはColumnとRowで良くない??
updateTransition
複数のアニメーションを連携して操作出来る
ここではカードの中身と区切り線の色とボタンのラベル
Mori Atsushi.icon AnimatedContentも協調できること知らなかった
AnimatedContent
アニメーションしつつ内部のコンテンツを切り替えられる
切り替わりにはfadeThroughを使っている
上記参照
animateColor
色をアニメーション出来る
labelはレイアウトプレビュー用
Animation Inspectionはexperimentalなので設定から有効にする必要がある
animateColorはrgb分かれて表示されている
https://scrapbox.io/files/61f1417cda75a7001df1647c.png
Mori Atsushi.icon こういうの見てるとPreviewやっぱり使ったほうが良いのかな
ao.icon preview便利そう
Layout > Shared axis
空間的またはナビゲーション的な関係を持つUI要素間の遷移に使用する
x,y,z軸のshared transformationを使用して、その関係性を強化する
つまり、軸を固定して縦横前後に動くようなアニメーション?
SharedYAxis
code:kotlin
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun <T : Comparable<T>> SharedYAxis(
targetState: T,
modifier: Modifier = Modifier,
content: @Composable AnimatedVisibilityScope.(T) -> Unit
) {
遷移元の要素と遷移先の要素の FiniteAnimationSpec を定義する
enterSpecの delayMillis=exitDurationMillis で遷移先のアニメーション開始を遅らせているのがポイント?
slideDistance
スライドアニメーションの幅
code:kotlin
val exitDurationMillis = 80
val enterDurationMillis = 220
// This local function creates the AnimationSpec for outgoing elements.
fun <T> exitSpec(): FiniteAnimationSpec<T> =
tween(
durationMillis = exitDurationMillis,
easing = FastOutLinearInEasing
)
// This local function creates the AnimationSpec for incoming elements.
fun <T> enterSpec(): FiniteAnimationSpec<T> =
tween(
// The enter animation runs right after the exit animation.
delayMillis = exitDurationMillis,
durationMillis = enterDurationMillis,
easing = LinearOutSlowInEasing
)
val slideDistance = with(LocalDensity.current) { 30.dp.roundToPx() }
AnimatedContent
if (initialState < targetState)
元のページと新しいページを比較して、アニメーションの方向を決める。今回はY軸のアニメーションなので上か下。
これは元のクラス(Page)で compareTo()をoverrideしてidを比較するようにしている
code:kotlin
private class Page(
val id: Int,
@DrawableRes
val image: Int,
val title: String,
val body: String
) : Comparable<Page> {
override fun compareTo(other: Page): Int {
return id.compareTo(other.id)
}
}
code:kotlin
AnimatedContent(
targetState = targetState,
transitionSpec = {
// The state type (<T>) is Comparable.
// We compare the initial state and the target state to determine whether we are moving
// down or up.
if (initialState < targetState) { // Move down
ContentTransform(
// Outgoing elements fade out and slide up to the top.
initialContentExit = fadeOut(exitSpec()) +
slideOutVertically(exitSpec()) { -slideDistance },
// Incoming elements fade in and slide up from the bottom.
targetContentEnter = fadeIn(enterSpec()) +
slideInVertically(enterSpec()) { slideDistance }
)
} else { // Move up
ContentTransform(
// Outgoing elements fade out and slide down to the bottom.
initialContentExit = fadeOut(exitSpec()) +
slideOutVertically(exitSpec()) { slideDistance },
// Outgoing elements fade in and slide down from the top.
targetContentEnter = fadeIn(enterSpec()) +
slideInVertically(enterSpec()) { -slideDistance }
)
}
},
modifier = modifier,
content = content
)
Mori Atsushi.icon Comparableの使い方面白い
chigichan24.icon +1
Mori Atsushi.icon slideDistanceとかをpxで指定しないといけないのめんどくさい
List > Loading
CheeseDataSource.kt
PagingSource を継承している
PagingSource<Int, Cheese>
普通に androidx.paging に基づいて実装しているだけ
すき
code:kotlin
// Simulate slow network.
delay(3000)
LoadingViewModel.kt
漢のflow一本勝負
code:kotlin
val cheeses = Pager(PagingConfig(pageSize = 15)) { CheeseDataSource() }.flow
Flow<PagingData<Cheese>> を渡すだけ
LoadingDemo.kt
これが確認したい本体
ロード中にパタパタとアニメーションする
Motion provides timely feedback and the status of user actions. An animated placeholder UI can indicate that content is loading.
デモのために更新ボタンを押すたびに key を更新するようにしている。キモい
code:kotlin
var refreshCount by remember { mutableStateOf(0) }
LoadingDemoContent(
cheeses = key(refreshCount) { viewModel.cheeses.collectAsLazyPagingItems() },
onRefresh = { refreshCount++ }
)
アニメーションの delay の決定方法
itemの indexによって、delayする秒数を変更する。
今回の例では index * 80 msecだけ delayさせる
アニメーションのdelay(offset)を計算して offsetに代入し、CheeseItemに引数として渡す。
ところで、SystemClock.uptimeMillis()って起動してからの経過時間返すんすね
milliseconds of non-sleep uptime since boot
code: kotlin
Scaffold(
topBar = { /* なんの変哲もないtopbarの処理 */}
) {
val startTimeMillis = remember(cheeses) { SystemClock.uptimeMillis() }
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = rememberInsetsPaddingValues(
insets = systemBars,
applyTop = false
)
) {
itemsIndexed(items = cheeses) { index, cheese ->
val offset = (SystemClock.uptimeMillis() - startTimeMillis).toInt() - index * 80
CheeseItem(
cheese = cheese,
animationOffsetMillis = offset,
modifier = Modifier.fillMaxWidth()
)
}
}
}
animation の実態
CheeseItem の placeholderModifierにその実装がある。
Image や Text の Modifier に最後に .then(placeholderModifier) みたいな形で渡すことで実現
code:kotlin
val placeholderModifier = Modifier.placeholder(
// item がないなら placeholder を表示する
visible = cheese == null,
// 適当な色を渡す
color = MaterialTheme.colorScheme.surfaceVariant,
// fade out の animation の仕方を指定する。 ここでは、 tween で 200 msec。
placeholderFadeTransitionSpec = { tween(durationMillis = 200) },
// fade in の animation の仕方を指定する。 ここでは、 tween で 200 msec。
contentFadeTransitionSpec = { tween(durationMillis = 200) },
// placeholder highlight の animation を指定する
highlight = PlaceholderHighlight.fade(
highlightColor = MaterialTheme.colorScheme.surface,
// アニメーションを作る。わざと offset をつけることで、 stagger なエフェクトを実現できる。
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 800
0f at 0
0f at 200
1f at 800 with FastOutSlowInEasing // CubicBezierEasing で作られている。どういうふうに前フレームから遷移させるかを指定できる。他には linear とかもある。
},
// こうすることで、連続性のある感じのアニメーションにしている。Restartというパラメータもある。
repeatMode = RepeatMode.Reverse,
// list での index を考慮して、delay を入れる。(引数で渡ってくるやつ。)
initialStartOffset = StartOffset(
offsetType = StartOffsetType.FastForward,
offsetMillis = animationOffsetMillis
)
)
)
)
Mori Atsushi.icon placeholderのUIは分けたほうが良いかも?
emptyPainterとかもいらなくなる
Mori Atsushi.icon Paging使う?使わない?
ao.icon xmlより簡単になっている?
気になるポイント
コメント