💫
入門 Jetpack Compose Animation part3
はじめに
前回は、Value-based animationsを読んで学習した内容をまとめました。
今回は、Advanced animation exampleを読んで学習した内容をまとめます。
ジェスチャーとアニメーション
タッチイベントとアニメーションを組み合わせる場合は注意することが増えます。
ユーザーインタラクションはアニメーションよりも優先され、タッチイベントが開始されたら、たとえアニメーションの途中であっても中断して新しいイベントに反応する必要があります。
ジェスチャー
ジェスチャーサンプル: タップで青い円が移動する様子
@Composable
fun Gesture(modifier: Modifier = Modifier) {
// you can use Animatable to change Offset
val offset = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
val scope = rememberCoroutineScope()
Box(
modifier = modifier
.fillMaxSize()
.pointerInput(Unit) {
// using high level api
detectTapGestures {
scope.launch {
// cancel after new tap event comming
offset.animateTo(it)
}
}
// using low level api
coroutineScope {
// cancel after new tap event comming
while (true) {
awaitPointerEventScope {
val position = awaitFirstDown().position
launch {
offset.animateTo(position)
}
}
}
}
}
) {
Box(
modifier = Modifier
.offset { offset.value.toIntOffset() }
.size(100.dp)
.background(Color.Blue, shape = CircleShape)
)
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
自作SwipeToDismiss
スワイプで要素を画面外にDismissするサンプル
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
val decay = splineBasedDecay<Float>(this)
coroutineScope {
while (true) {
val velocityTracker = VelocityTracker()
// 1
offsetX.stop()
awaitPointerEventScope {
val pointerId = awaitFirstDown().id
horizontalDrag(pointerId) { change ->
launch {
// 2
offsetX.snapTo(offsetX.value + change.positionChange().x)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position,
)
}
}
// スワイプの速度を計測
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat(),
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// 3
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity,
)
} else {
// 4
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
@Composable
fun TodoList(modifier: Modifier = Modifier) {
val todos = remember {
mutableStateListOf(
"Take out the trash",
"Do the dishes",
"Write a blog post",
)
}
LazyColumn(
modifier = modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(32.dp)
) {
items(todos, { it }) { text ->
Box(
modifier = Modifier
.swipeToDismiss(
onDismissed = {
todos.remove(text)
}
)
.padding(horizontal = 16.dp)
.background(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp),
)
.padding(16.dp)
.height(100.dp)
.fillMaxWidth()
) {
Text(
text = text,
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
}
}
}
}
余談
offsetX.updateBounds
はアニメーション範囲を制限し、画面外への過度な移動を防ぎます。
animateDecay
は慣性や物理的挙動を模倣した減衰アニメーションを提供します。
参考文献
Discussion