여러 App을 사용하다보면, 광고 같은 경우 특정 시간이 지나면 다음 광고가 보이는 화면을 볼 수 있습니다.
이러한 경우에 Compose에서는 HorizontalPager를 사용하여 구현할 수 있습니다.
@OptIn(ExperimentalPagerApi::class)
@Composable
fun MainScreen() {
val pagerState = rememberPagerState(initialPage = 0)
val imageSlider = listOf(
painterResource(id = R.drawable.ic_launcher_background),
painterResource(id = R.drawable.ic_launcher_background),
painterResource(id = R.drawable.ic_launcher_background)
)
LaunchedEffect(Unit) {
while (true) {
yield()
delay(2600)
pagerState.animateScrollToPage(
page = (pagerState.currentPage + 1) % (pagerState.pageCount)
)
}
}
Column {
HorizontalPager(
count = imageSlider.size,
state = pagerState,
contentPadding = PaddingValues(horizontal = DIMENS_16dp),
modifier = Modifier
.height(DIMENS_114dp)
.fillMaxWidth()
) { page ->
Card(
shape = RoundedCornerShape(DIMENS_12dp),
modifier = Modifier
.graphicsLayer {
val pageOffset = calculateCurrentOffsetForPage(page).absoluteValue
lerp(
start = 0.85f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
).also { scale ->
scaleX = scale
scaleY = scale
}
alpha = lerp(
start = 0.5f,
stop = 1f,
fraction = 1f - pageOffset.coerceIn(0f, 1f)
)
}
) {
Image(
painter = imageSlider[page],
contentDescription = "image slider",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(DIMENS_16dp)
)
}
}
LauchedEffect(Unit)를 사용함으로써, LaunchedEffect 하단의 블록은 해당 Composable이 처음 시작할 때, 동작합니다.
이때 while에서 무한 루프를 돌면서 계속해서 내부 동작을 계속 진행하게 됩니다.
`yield()`는 현재 코루틴을 일시 중단하고, 다른 코루틴이 실행될 수 있도록 제어권을 양보합니다. 이는 코루틴이 너무 빨리 다음 페이지로 이동하는 것을 방지해줍니다.
좀 더 상세히 설명하면, 만약 yield()가 없는 경우에는 while(ture) 로 인하여 반복적으로 동작을 수행합니다.
그렇지만 yield()는 현재 코루틴의 실행을 일시 중단하고 스케줄러에게 현재 스레드의 제어권을 양보합니다. 제어권을 넘겨받은 스케줄러는 다른 실행 가능한 코루틴을 찾거나, 현재 스레드에 할당된 다른 작업을 수행합니다. 그러다 스케줄러가 다시 이 코루틴을 실행할 차례가 되면, yield()를 호출한 바로 다음 줄부터 실행을 재개합니다.
그렇기 때문에 반복적인 동작 수행만 하는 것이 아니라, 사이에 약간의 시간을 주어서 다른 코루틴 적업 혹은 UI 렌더링 작업을 수행할 수 있는 기회를 줍니다. 그 결과 CPU 자원을 많이 소모하는 작업이 있거나, 복잡한 UI를 그려야 하는 경우에 좀 더 쾌적한 성능을 제공할 수 있습니다.
page = (pagerState.currentPage + 1) % (pagerState.pageCount)
상단의 코드를 통해 다음 페이지로 이동할 수 있습니다. 추가적으로 pagerState.pageCount으로 나누어줌으로써, 마지막 페이지에 도달했을 때, 첫 번째 페이지로 다시 돌아올 수 있게 됩니다.
`Modifier.graphicsLayer` 를 통해 페이지를 넘길 때, 발생하는 시각효과를 설정할 수 있습니다.
HorizontalPagerIndicator은 현재 이미지가 몇 번째 이미지인지 알려주는 역할을 합니다.
@ExperimentalPagerApi
@Composable
fun HorizontalPagerIndicator(
pagerState: PagerState,
modifier: Modifier = Modifier,
pageCount: Int = pagerState.pageCount,
pageIndexMapping: (Int) -> Int = { it },
activeColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
inactiveColor: Color = activeColor.copy(ContentAlpha.disabled),
indicatorWidth: Dp = 8.dp,
indicatorHeight: Dp = indicatorWidth,
spacing: Dp = indicatorWidth,
indicatorShape: Shape = CircleShape,
) {
val indicatorWidthPx = LocalDensity.current.run { indicatorWidth.roundToPx() }
val spacingPx = LocalDensity.current.run { spacing.roundToPx() }
Box(
modifier = modifier,
contentAlignment = Alignment.CenterStart
) {
Row(
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically,
) {
val indicatorModifier = Modifier
.size(width = indicatorWidth, height = indicatorHeight)
.background(color = inactiveColor, shape = indicatorShape)
repeat(pageCount) {
Box(indicatorModifier)
}
}
Box(
Modifier
.offset {
val position = pageIndexMapping(pagerState.currentPage)
val offset = pagerState.currentPageOffset
val next = pageIndexMapping(pagerState.currentPage + offset.sign.toInt())
val scrollPosition = ((next - position) * offset.absoluteValue + position)
.coerceIn(0f, (pageCount - 1).coerceAtLeast(0).toFloat())
IntOffset(
x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(),
y = 0
)
}
.size(width = indicatorWidth, height = indicatorHeight)
.then(
if (pageCount > 0) Modifier.background(
color = activeColor,
shape = indicatorShape,
)
else Modifier
)
)
}
}
HorizontalPagerIndicator의 내부 구조입니다.
pagerState를 인자로 받아서 전체 indicator 및 현재 indicator에 대해 알려줍니다.
repeat을 통해 pageCount만큼 반복하여 화면에 그려주기도 합니다.
scrollPosition은 현재 페이지에서 다음 페이지까지 얼마나 스크롤 되었는지를 알려주는 실수값입니다. 이를 활용하여 페이지가 이동한 만큼 indicator도 동일하게 이동하는 것이 가능합니다.
완성 화면입니다.
추가적으로 DIMENS_4dp 코드의 재사용성을 위해 생성한 상수입니다. 실제로 구현하는 경우에 DIMENS_4dp는 4dp로 변경해주시면 구현이 가능합니다.
'Android Studio > compose' 카테고리의 다른 글
| Android에 새롭게 등장한 Navigation3 알아보자 (0) | 2025.07.06 |
|---|---|
| Compose with navigation (2) : arguments (0) | 2022.12.16 |
| Compose with navigation (1) : 화면 이동 (0) | 2022.12.16 |
| [Compose] java.lang.RuntimeException: Cannot create an instance of class ViewModel 에러 해결 (0) | 2022.12.16 |