以下のように、ページをスワイプするたびにイモムシみたいに動くインジゲータを作成します。
横幅は大きくなったり小さくなったり小さくなったりします。offsetは横幅が増加している時は動かず、小さくなっている時にだけ、小さくなっている分だけ進むことで、右端は動かずに小さくなります。
コード
import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearEasing import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times import kotlin.math.floor @Composable public fun Indicator( modifier: Modifier = Modifier, count: Int, size: Dp = 8.dp, indicatorPadding: Dp = 4.dp, currentIndex: Float, activeColor: Color, inActiveColor: Color, easing: Easing = LinearEasing, ) { val reverseEasing = remember(easing) { Easing { 1 - easing.transform(1 - it) } } Box(modifier = modifier.width(IntrinsicSize.Min)) { Row( modifier = Modifier.fillMaxWidth(), ) { repeat(count) { index -> Box( Modifier .size(size) .then( Modifier .background(inActiveColor, CircleShape), ), ) if (index != count - 1) { Spacer(modifier = Modifier.width(indicatorPadding)) } } } Canvas( modifier = Modifier.fillMaxWidth(), onDraw = { val a = 2 val offsetXFactor: Float val widthFactor: Float val indexRemainder = currentIndex % 1 if (indexRemainder < 0.5) { widthFactor = easing.transform(a * indexRemainder) offsetXFactor = floor(currentIndex) } else { widthFactor = reverseEasing.transform(1 - (a * (indexRemainder - 0.5f))) offsetXFactor = floor(currentIndex) + easing.transform((a * (indexRemainder - 0.5f))) } drawRoundRect( color = activeColor, topLeft = Offset(offsetXFactor * (size + indicatorPadding).toPx(), 0f), size = Size((widthFactor * (size + indicatorPadding) + size).toPx(), size.toPx()), cornerRadius = CornerRadius(size.toPx() / 2), ) }, ) } }
プレビュー
動作プレビューのコード
@OptIn(ExperimentalFoundationApi::class) @Composable @Preview(heightDp = 200, showBackground = true) private fun Preview() { Column(modifier = Modifier.fillMaxSize()) { val state = rememberPagerState() val pageCount = 5 HorizontalPager( modifier = Modifier .weight(1f) .fillMaxWidth(), state = state, pageCount = pageCount, ) { Box(modifier = Modifier.fillMaxSize()) { Text( modifier = Modifier.align(Alignment.Center), text = "Page $it", ) } } val currentIndex by remember { derivedStateOf { state.currentPage + state.currentPageOffsetFraction } } Column( modifier = Modifier.fillMaxWidth() .padding(4.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Indicator( count = pageCount, currentIndex = currentIndex, activeColor = Color.Red, inActiveColor = Color.Gray, ) Spacer(modifier = Modifier.height(4.dp)) Indicator( count = pageCount, currentIndex = currentIndex, activeColor = Color.Red, inActiveColor = Color.Gray, easing = FastOutSlowInEasing, ) Spacer(modifier = Modifier.height(4.dp)) Indicator( count = pageCount, currentIndex = currentIndex, activeColor = Color.Red, inActiveColor = Color.Gray, easing = CubicBezierEasing(0f, 1f, 0f, 1f), ) } } }
easing確認用のコード
@Composable @Preview(showBackground = true, widthDp = 100, heightDp = 200) private fun Preview2() { Column(modifier = Modifier.fillMaxSize()) { (0..20).onEach { Row { Indicator( modifier = Modifier .padding(2.dp), count = 2, currentIndex = it * 0.05f, activeColor = Color.Red, inActiveColor = Color.Gray, ) Spacer(modifier = Modifier.width(8.dp)) Indicator( modifier = Modifier .padding(2.dp), count = 2, currentIndex = it * 0.05f, activeColor = Color.Red, inActiveColor = Color.Gray, easing = FastOutSlowInEasing, ) Spacer(modifier = Modifier.width(8.dp)) Indicator( modifier = Modifier .padding(2.dp), count = 2, currentIndex = it * 0.05f, activeColor = Color.Red, inActiveColor = Color.Gray, easing = CubicBezierEasing(0f, 1f, 0f, 1f), ) } } } }