Compose의 페이저

콘텐츠를 왼쪽과 오른쪽 또는 위아래로 넘기려면 HorizontalPagerVerticalPager 컴포저블을 각각 사용하면 됩니다. 이러한 컴포저블에는 뷰 시스템의 ViewPager와 유사한 함수가 있습니다. 기본적으로 HorizontalPager는 화면의 전체 너비를 차지하고 VerticalPager는 전체 높이를 차지하며 페이저는 한 번에 한 페이지만 플링합니다. 이러한 기본값은 모두 구성할 수 있습니다.

HorizontalPager

가로 왼쪽과 오른쪽으로 스크롤되는 페이저를 만들려면 HorizontalPager를 사용합니다.

그림 1. HorizontalPager 데모

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

VerticalPager

위아래로 스크롤되는 페이저를 만들려면 VerticalPager를 사용합니다.

그림 2. VerticalPager 데모

// Display 10 items
val pagerState = rememberPagerState(pageCount = {
    10
})
VerticalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier.fillMaxWidth()
    )
}

지연 생성

HorizontalPagerVerticalPager의 페이지는 모두 지연 구성되고 필요한 경우 배치됩니다. 사용자가 페이지를 스크롤하면 컴포저블은 더 이상 필요하지 않은 페이지를 삭제합니다.

화면 밖으로 더 많은 페이지 로드하기

기본적으로 페이저는 화면에 표시되는 페이지만 로드합니다. 화면 밖에서 더 많은 페이지를 로드하려면 beyondBoundsPageCount을 0보다 큰 값으로 설정하세요.

페이저의 항목으로 스크롤

페이저에서 특정 페이지로 스크롤하려면 rememberPagerState()를 사용하여 PagerState 객체를 만들고 이를 페이저에 state 매개변수로 전달합니다. 이 상태에서 CoroutineScope 내에서 PagerState#scrollToPage()를 호출할 수 있습니다.

val pagerState = rememberPagerState(pageCount = {
    10
})
HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.scrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

페이지에 애니메이션을 적용하려면 PagerState#animateScrollToPage() 함수를 사용합니다.

val pagerState = rememberPagerState(pageCount = {
    10
})

HorizontalPager(state = pagerState) { page ->
    // Our page content
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )
}

// scroll to page
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
    coroutineScope.launch {
        // Call scroll to on pagerState
        pagerState.animateScrollToPage(5)
    }
}, modifier = Modifier.align(Alignment.BottomCenter)) {
    Text("Jump to Page 5")
}

페이지 상태 변경 알림 받기

PagerState에는 페이지에 관한 정보가 포함된 세 가지 속성, 즉 currentPage, settledPage, targetPage가 있습니다.

  • currentPage: 맞추기 위치에 가장 가까운 페이지입니다. 기본적으로 맞추기 위치는 레이아웃의 시작 부분에 있습니다.
  • settledPage: 애니메이션 또는 스크롤이 실행되고 있지 않을 때의 페이지 번호입니다. 페이지가 맞추기 위치에 충분히 가까우면 currentPage가 즉시 업데이트된다는 점에서 currentPage 속성과 다릅니다. 하지만 settledPage는 모든 애니메이션 실행이 완료될 때까지 동일하게 유지됩니다.
  • targetPage: 스크롤 이동을 위해 제안된 정지 위치입니다.

snapshotFlow 함수를 사용하여 이러한 변수의 변경사항을 관찰하고 이에 반응할 수 있습니다. 예를 들어 각 페이지 변경 시 애널리틱스 이벤트를 전송하려면 다음을 수행합니다.

val pagerState = rememberPagerState(pageCount = {
    10
})

LaunchedEffect(pagerState) {
    // Collect from the a snapshotFlow reading the currentPage
    snapshotFlow { pagerState.currentPage }.collect { page ->
        // Do something with each page change, for example:
        // viewModel.sendPageSelectedEvent(page)
        Log.d("Page change", "Page changed to $page")
    }
}

VerticalPager(
    state = pagerState,
) { page ->
    Text(text = "Page: $page")
}

페이지 표시기 추가

페이지에 표시기를 추가하려면 PagerState 객체를 사용하여 페이지 수 중에서 선택된 페이지에 관한 정보를 가져오고 맞춤 표시기를 그립니다.

예를 들어 간단한 원 표시기를 원하는 경우 pagerState.currentPage를 사용하여 원 수를 반복하고 페이지 선택 여부에 따라 원 색상을 변경할 수 있습니다.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    modifier = Modifier.fillMaxSize()
) { page ->
    // Our page content
    Text(
        text = "Page: $page",
    )
}
Row(
    Modifier
        .wrapContentHeight()
        .fillMaxWidth()
        .align(Alignment.BottomCenter)
        .padding(bottom = 8.dp),
    horizontalArrangement = Arrangement.Center
) {
    repeat(pagerState.pageCount) { iteration ->
        val color = if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
        Box(
            modifier = Modifier
                .padding(2.dp)
                .clip(CircleShape)
                .background(color)
                .size(16.dp)
        )
    }
}

콘텐츠 아래에 원 표시기를 보여주는 페이저
그림 3. 콘텐츠 아래에 원 표시기를 보여주는 페이저

콘텐츠에 항목 스크롤 효과 적용

일반적인 사용 사례는 스크롤 위치를 사용하여 페이저 항목에 효과를 적용하는 것입니다. 현재 선택된 페이지에서 페이지까지의 거리를 확인하려면 PagerState.currentPageOffsetFraction를 사용하면 됩니다. 그런 다음 선택한 페이지로부터의 거리를 기준으로 콘텐츠에 변환 효과를 적용할 수 있습니다.

그림 4. 페이저 콘텐츠에 변환 적용

예를 들어 중앙으로부터의 거리에 따라 항목의 불투명도를 조정하려면 페이저 내부의 항목에서 Modifier.graphicsLayer를 사용하여 alpha를 변경합니다.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(state = pagerState) { page ->
    Card(
        Modifier
            .size(200.dp)
            .graphicsLayer {
                // Calculate the absolute offset for the current page from the
                // scroll position. We use the absolute value which allows us to mirror
                // any effects for both directions
                val pageOffset = (
                    (pagerState.currentPage - page) + pagerState
                        .currentPageOffsetFraction
                    ).absoluteValue

                // We animate the alpha, between 50% and 100%
                alpha = lerp(
                    start = 0.5f,
                    stop = 1f,
                    fraction = 1f - pageOffset.coerceIn(0f, 1f)
                )
            }
    ) {
        // Card content
    }
}

맞춤 페이지 크기

기본적으로 HorizontalPagerVerticalPager는 각각 전체 너비 또는 전체 높이를 사용합니다. Fixed, Fill(기본값) 또는 맞춤 크기 계산을 사용하도록 pageSize 변수를 설정할 수 있습니다.

예를 들어 100.dp의 고정 너비 페이지를 설정하는 방법은 다음과 같습니다.

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    pageSize = PageSize.Fixed(100.dp)
) { page ->
    // page content
}

표시 영역 크기에 따라 페이지 크기를 조정하려면 맞춤 페이지 크기 계산을 사용하세요. 맞춤 PageSize 객체를 만들고 항목 간 간격을 고려하여 availableSpace를 3으로 나눕니다.

private val threePagesPerViewport = object : PageSize {
    override fun Density.calculateMainAxisPageSize(
        availableSpace: Int,
        pageSpacing: Int
    ): Int {
        return (availableSpace - 2 * pageSpacing) / 3
    }
}

콘텐츠 패딩

HorizontalPagerVerticalPager는 모두 콘텐츠 패딩 변경을 지원하므로 페이지의 최대 크기와 정렬에 영향을 미칠 수 있습니다.

예를 들어 start 패딩을 설정하면 페이지가 끝을 향해 정렬됩니다.

끝을 향해 정렬된 콘텐츠를 보여주는 시작 패딩이 있는 페이저

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(start = 64.dp),
) { page ->
    // page content
}

startend 패딩을 동일한 값으로 설정하면 항목이 가로로 가운데에 배치됩니다.

콘텐츠를 중앙에 표시하는 시작 및 끝 패딩이 있는 페이저

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(horizontal = 32.dp),
) { page ->
    // page content
}

end 패딩을 설정하면 페이지가 시작 방향으로 정렬됩니다.

시작 부분에 정렬된 콘텐츠를 보여주는 시작 및 끝 패딩이 있는 페이저

val pagerState = rememberPagerState(pageCount = {
    4
})
HorizontalPager(
    state = pagerState,
    contentPadding = PaddingValues(end = 64.dp),
) { page ->
    // page content
}

topbottom 값을 설정하여 VerticalPager에 비슷한 효과를 얻을 수 있습니다. 32.dp 값은 여기서 예시로만 사용됩니다. 각 패딩 크기를 원하는 값으로 설정할 수 있습니다.

스크롤 동작 맞춤설정

기본 HorizontalPagerVerticalPager 컴포저블은 스크롤 동작이 페이저에서 작동하는 방식을 지정합니다. 그러나 pagerSnapDistance 또는 flingBehaviour와 같은 기본값을 맞춤설정하고 변경할 수 있습니다.

스냅 거리

기본적으로 HorizontalPagerVerticalPager는 살짝 튕기기 동작이 한 번에 한 페이지까지 스크롤할 수 있는 최대 페이지 수를 설정합니다. 이를 변경하려면 flingBehavior에서 pagerSnapDistance를 설정하세요.

val pagerState = rememberPagerState(pageCount = { 10 })

val fling = PagerDefaults.flingBehavior(
    state = pagerState,
    pagerSnapDistance = PagerSnapDistance.atMost(10)
)

Column(modifier = Modifier.fillMaxSize()) {
    HorizontalPager(
        state = pagerState,
        pageSize = PageSize.Fixed(200.dp),
        beyondBoundsPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}