
또한 전체 페이지가 6개 이상이면 초반에도 5개 점 중 마지막 2개의 점은 작은 사이즈를 가지지만

페이지가 5개 이하일 때는 모든 점이 다 큰 사이즈를 유지한다.
우선 현재 페이지가 바뀔 때마다 변하는 것이 아니기 때문에 애니메이션이 변경되는 가운데 세 개의 점의 인덱스를 가지고 있고 그 양옆으로 작은 크기의 점까지의 인덱스도 추가로 가지고 있어서 작은 점을 보여줄 지 말지 결정할 수 있도록 하였다.
var fullDotLeft by remember {
mutableIntStateOf(0)
}
var fullDotRight by remember {
mutableIntStateOf(
if (totalPage <= 5) {
5
} else {
fullDotLeft + 2
}
)
}
var left by remember {
mutableIntStateOf(0)
}
var right by remember {
mutableIntStateOf(minOf(totalPage, 5))
}
LaunchedEffect(currentPage) {
if (currentPage > fullDotRight) {
fullDotRight++
fullDotLeft++
if (fullDotLeft - left > 2) {
left++
}
right = minOf(totalPage - 1, right + 1)
}
if (currentPage < fullDotLeft) {
fullDotRight--
fullDotLeft--
if (right - fullDotRight > 2) {
right--
}
left = maxOf(0, left - 1)
}
}
currentPage가 fullDotLeft..fullDotRight의 범위를 벗어나는 순간에 위치 정보를 갱신하여 애니메이션이 적용되도록 하였다.
val dotScales = List(totalPage) { index ->
val targetValue = when {
index in fullDotLeft..fullDotRight -> 1f
index == fullDotLeft - 1 && index >= left -> 0.7f
index == fullDotRight + 1 && index <= right -> 0.7f
index == fullDotLeft - 2 && index >= left -> 0.4f
index == fullDotRight + 2 && index <= right -> 0.4f
else -> 0f
}
animateFloatAsState(
targetValue = targetValue,
animationSpec = tween(
durationMillis = animationDuration,
easing = LinearEasing
),
label = "dotScale"
).value
}
위치 인덱스값을 기준으로 크기를 설정해주었다.
left..right 의 범위를 벗어나는 경우는 0f를 설정해주었는데 이는 항상 큰 세 개의 점이 가운데에 위치하도록 빈공간으로 자리를 차지하게 하기 위함이다.
AnimatedVisibility(
visible = index in left..right,
enter = expandHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
),
exit = shrinkHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
)
) {
Box(
modifier = Modifier
.width(dotSize)
.aspectRatio(1f)
.scale(dotScales[index])
.clip(CircleShape)
.background(
if (index == currentPage) {
Color(0xFF0096FB)
} else {
Color(0xFFDADFE3)
}
)
)
}
repeat(2) {
AnimatedVisibility(
visible = right - fullDotRight < 2,
enter = expandHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
),
exit = shrinkHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
)
) {
Row {
Box(
modifier = Modifier
.width(dotSize)
.aspectRatio(1f)
.scale(0f)
.clip(CircleShape)
)
}
}
}
다음과 같이 뷰의 크기는 존재하지만 scale은 0f로 하여서 화면에는 보이지 않도록 하였다.
@Composable
fun InstagramDotIndicator(
modifier: Modifier = Modifier,
currentPage: Int,
totalPage: Int,
spacePadding: Dp,
) {
require(totalPage > 0) {
"At least 1 page is required"
}
require(currentPage in 0..<totalPage) {
"currentPage is out of totalPage bounds"
}
var width by remember {
mutableIntStateOf(0)
}
var height by remember {
mutableIntStateOf(0)
}
val maxPageDots = 7
val density = LocalDensity.current
val dotSize by remember {
derivedStateOf {
with(density) {
minOf(
((width - spacePadding.toPx() * (maxPageDots - 1)) / maxPageDots).toDp(),
height.toDp()
)
}
}
}
var fullDotLeft by remember {
mutableIntStateOf(0)
}
var fullDotRight by remember {
mutableIntStateOf(
if (totalPage <= 5) {
5
} else {
fullDotLeft + 2
}
)
}
var left by remember {
mutableIntStateOf(0)
}
var right by remember {
mutableIntStateOf(minOf(totalPage, 5))
}
LaunchedEffect(currentPage) {
if (currentPage > fullDotRight) {
fullDotRight++
fullDotLeft++
if (fullDotLeft - left > 2) {
left++
}
right = minOf(totalPage - 1, right + 1)
}
if (currentPage < fullDotLeft) {
fullDotRight--
fullDotLeft--
if (right - fullDotRight > 2) {
right--
}
left = maxOf(0, left - 1)
}
}
val animationDuration = 100
val dotScales = List(totalPage) { index ->
val targetValue = when {
index in fullDotLeft..fullDotRight -> 1f
index == fullDotLeft - 1 && index >= left -> 0.7f
index == fullDotRight + 1 && index <= right -> 0.7f
index == fullDotLeft - 2 && index >= left -> 0.4f
index == fullDotRight + 2 && index <= right -> 0.4f
else -> 0f
}
animateFloatAsState(
targetValue = targetValue,
animationSpec = tween(durationMillis = animationDuration, easing = LinearEasing),
label = "dotScale"
).value
}
Box(
modifier = modifier.onGloballyPositioned {
width = it.size.width
height = it.size.height
},
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(spacePadding),
verticalAlignment = Alignment.CenterVertically
) {
repeat(2) {
AnimatedVisibility(
visible = fullDotLeft - left < 2,
enter = expandHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
),
exit = shrinkHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
)
) {
Row {
Box(
modifier = Modifier
.width(dotSize)
.aspectRatio(1f)
.scale(0f)
.clip(CircleShape)
)
}
}
}
repeat(totalPage) { index ->
AnimatedVisibility(
visible = index in left..right,
enter = expandHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
),
exit = shrinkHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
)
) {
Box(
modifier = Modifier
.width(dotSize)
.aspectRatio(1f)
.scale(dotScales[index])
.clip(CircleShape)
.background(
if (index == currentPage) {
Color(0xFF0096FB)
} else {
Color(0xFFDADFE3)
}
)
)
}
}
repeat(2) {
AnimatedVisibility(
visible = right - fullDotRight < 2,
enter = expandHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
),
exit = shrinkHorizontally(
animationSpec = tween(
durationMillis = animationDuration,
easing = FastOutSlowInEasing
)
)
) {
Row {
Box(
modifier = Modifier
.width(dotSize)
.aspectRatio(1f)
.scale(0f)
.clip(CircleShape)
)
}
}
}
}
}
}