Công cụ phân trang trong Compose

Để lật qua nội dung theo chiều trái và phải hoặc lên và xuống, bạn có thể sử dụng các thành phần kết hợp HorizontalPagerVerticalPager tương ứng. Các thành phần kết hợp này có chức năng tương tự như ViewPager trong hệ thống thành phần hiển thị. Theo mặc định, HorizontalPager chiếm toàn bộ chiều rộng của màn hình, VerticalPager chiếm toàn bộ chiều cao và trình phân trang chỉ hất một trang tại một thời điểm. Bạn có thể định cấu hình tất cả các giá trị mặc định này.

HorizontalPager

Để tạo một trình phân trang cuộn theo chiều ngang sang trái và phải, hãy sử dụng HorizontalPager:

Hình 1. Bản minh hoạ HorizontalPager

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

VerticalPager

Để tạo một trình phân trang cuộn lên và xuống, hãy sử dụng VerticalPager:

Hình 2. Bản minh hoạ VerticalPager

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

Tạo từng phần

Các trang trong cả HorizontalPagerVerticalPager đều được thành phần hoá một cách lười biếng và bố trí khi cần. Khi người dùng cuộn qua các trang, thành phần kết hợp sẽ xoá mọi trang không còn cần thiết.

Tải thêm trang ngoài màn hình

Theo mặc định, trình phân trang chỉ tải các trang hiển thị trên màn hình. Để tải thêm trang ngoài màn hình, hãy đặt beyondBoundsPageCount thành một giá trị lớn hơn 0.

Di chuyển đến một mục trong trình phân trang

Để cuộn đến một trang nhất định trong trình phân trang, hãy tạo đối tượng PagerState bằng cách sử dụng rememberPagerState() và truyền đối tượng đó dưới dạng tham số state đến trình phân trang. Bạn có thể gọi PagerState#scrollToPage() ở trạng thái này, bên trong CoroutineScope:

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")
}

Nếu bạn muốn tạo ảnh động cho trang, hãy sử dụng hàm 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")
}

Nhận thông báo về các thay đổi về trạng thái trang

PagerState có ba thuộc tính chứa thông tin về các trang: currentPage, settledPagetargetPage.

  • currentPage: Trang gần nhất với vị trí chụp nhanh. Theo mặc định, vị trí chụp nhanh nằm ở đầu bố cục.
  • settledPage: Số trang khi không có ảnh động hoặc thao tác cuộn nào đang chạy. Điều này khác với thuộc tính currentPage ở chỗ currentPage cập nhật ngay lập tức nếu trang đủ gần với vị trí chụp nhanh, nhưng settledPage vẫn giữ nguyên cho đến khi tất cả ảnh động chạy xong.
  • targetPage: Vị trí dừng được đề xuất cho một chuyển động cuộn.

Bạn có thể sử dụng hàm snapshotFlow để quan sát các thay đổi đối với các biến này và phản hồi các thay đổi đó. Ví dụ: để gửi một sự kiện phân tích trên mỗi thay đổi trang, bạn có thể làm như sau:

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")
}

Thêm chỉ báo trang

Để thêm chỉ báo vào một trang, hãy sử dụng đối tượng PagerState để lấy thông tin về trang nào được chọn trong số các trang và vẽ chỉ báo tuỳ chỉnh.

Ví dụ: nếu muốn có một chỉ báo vòng tròn đơn giản, bạn có thể lặp lại số lượng vòng tròn và thay đổi màu vòng tròn dựa trên việc trang có được chọn hay không bằng cách sử dụng 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)
        )
    }
}

Trình chuyển trang hiển thị chỉ báo hình tròn bên dưới nội dung
Hình 3. Trình chuyển trang hiển thị chỉ báo hình tròn bên dưới nội dung

Áp dụng hiệu ứng cuộn mục cho nội dung

Một trường hợp sử dụng phổ biến là sử dụng vị trí cuộn để áp dụng hiệu ứng cho các mục của trình phân trang. Để tìm hiểu khoảng cách của một trang so với trang hiện đang được chọn, bạn có thể sử dụng PagerState.currentPageOffsetFraction. Sau đó, bạn có thể áp dụng hiệu ứng biến đổi cho nội dung dựa trên khoảng cách từ trang đã chọn.

Hình 4. Áp dụng phép biến đổi cho nội dung Pager

Ví dụ: để điều chỉnh độ mờ của các mục dựa trên khoảng cách của các mục đó với tâm, hãy thay đổi alpha bằng cách sử dụng Modifier.graphicsLayer trên một mục bên trong trình phân trang:

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
    }
}

Kích thước trang tuỳ chỉnh

Theo mặc định, HorizontalPagerVerticalPager lần lượt chiếm toàn bộ chiều rộng hoặc chiều cao. Bạn có thể đặt biến pageSize thành Fixed, Fill (mặc định) hoặc tính toán kích thước tuỳ chỉnh.

Ví dụ: để đặt trang có chiều rộng cố định là 100.dp:

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

Để định cỡ trang dựa trên kích thước khung nhìn, hãy sử dụng tính năng tính toán kích thước trang tuỳ chỉnh. Tạo một đối tượng PageSize tuỳ chỉnh và chia availableSpace cho 3, có tính đến khoảng cách giữa các mục:

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

Khoảng đệm nội dung

Cả HorizontalPagerVerticalPager đều hỗ trợ việc thay đổi khoảng đệm nội dung, cho phép bạn tác động đến kích thước tối đa và căn chỉnh các trang.

Ví dụ: việc đặt khoảng đệm start sẽ căn chỉnh các trang về phía cuối:

Trình phân trang có khoảng đệm bắt đầu hiển thị nội dung được căn chỉnh về phía cuối

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

Việc đặt cả khoảng đệm startend thành cùng một giá trị sẽ căn giữa mục theo chiều ngang:

Trình phân trang có khoảng đệm bắt đầu và kết thúc hiển thị nội dung ở giữa

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

Việc thiết lập khoảng đệm end sẽ căn chỉnh các trang về phía đầu:

Trình phân trang có khoảng đệm bắt đầu và kết thúc hiển thị nội dung được căn chỉnh với phần bắt đầu

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

Bạn có thể đặt các giá trị topbottom để đạt được các hiệu ứng tương tự cho VerticalPager. Giá trị 32.dp chỉ được dùng ở đây để minh hoạ; bạn có thể đặt từng kích thước khoảng đệm thành bất kỳ giá trị nào.

Tuỳ chỉnh hành vi cuộn

Thành phần kết hợp HorizontalPagerVerticalPager mặc định chỉ định cách thao tác cuộn hoạt động với trình chuyển trang. Tuy nhiên, bạn có thể tuỳ chỉnh và thay đổi các giá trị mặc định như pagerSnapDistance hoặc flingBehavior.

Khoảng cách chụp nhanh

Theo mặc định, HorizontalPagerVerticalPager đặt số lượng trang tối đa mà cử chỉ hất có thể cuộn qua một trang tại một thời điểm. Để thay đổi điều này, hãy đặt pagerSnapDistance trên flingBehavior:

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),
        beyondViewportPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}