Công cụ phân trang trong Compose

Để lật nội dung theo chiều ngang hoặc chiều dọc, bạn có thể sử dụng các thành phần kết hợp HorizontalPager và VerticalPager. Các thành phần này có chức năng tương tự như ViewPager trong hệ thống khung hiển thị. Theo mặc định, HorizontalPager chiếm toàn bộ chiều rộng màn hình và VerticalPager chiếm toàn bộ chiều cao. Trình chuyển trang cũng chỉ chuyển một trang mỗi lần. 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 chuyể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ạ của 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 chuyển trang cuộn lên và xuống, hãy sử dụng VerticalPager:

Hình 2. Bản minh hoạ của 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ả HorizontalPager và VerticalPager đều được tạo từng phần và bố trí khi cần. Khi người dùng di chuyể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 chuyể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 chuyển trang

Để di chuyển đến một trang nhất định trong trình chuyển trang, hãy tạo một đố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 chuyển trang. Bạn có thể gọi PagerState#scrollToPage() trên trạng thái này, bên trong một 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 đối với trạng thái trang

PagerState có 3 thuộc tính chứa thông tin về các trang: currentPage, settledPage và targetPage.

  • currentPage: Trang gần nhất với vị trí chụp nhanh. Theo mặc định, vị trí đính 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. Thuộc tính này khác với thuộc tính currentPage ở chỗ currentPage sẽ cập nhật ngay lập tức nếu trang đủ gần với vị trí đính, 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 thao tác cuộn.

Bạn có thể dùng hàm snapshotFlow để quan sát các thay đổi đối với những biến này và phản ứng với chúng. Ví dụ: để gửi một sự kiện phân tích trên mỗi thay đổi về 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 được chọn trong số các trang và vẽ chỉ báo tuỳ chỉnh.

Ví dụ: để tạo chỉ báo hình tròn, bạn có thể lặp lại số lượng hình tròn và thay đổi màu hình 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 cho thấy chỉ báo vòng tròn bên dưới nội dung
Hình 3. Trình chuyển trang cho thấy chỉ báo vòng tròn bên dưới nội dung

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

Trường hợp sử dụng phổ biến là dùng vị trí cuộn để áp dụng hiệu ứng cho các mục trong trình chuyển trang. Để tìm hiểu khoảng cách giữa một trang và trang đã 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 của Trình chuyển trang

Ví dụ: để điều chỉnh độ mờ của các mục dựa trên khoảng cách từ mục đó đến 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 chuyể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, HorizontalPager và VerticalPager lần lượt chiếm toàn bộ chiều rộng hoặc toàn bộ chiều cao. Bạn có thể đặt biến pageSize thành Fixed, Fill (mặc định) hoặc cách tính kích thước tuỳ chỉnh.

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

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

Để định kích thước trang dựa trên kích thước khung nhìn, hãy sử dụng phép tính 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ả HorizontalPager và VerticalPager đều hỗ trợ 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ách căn chỉnh 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 chuyển trang có khoảng đệm bắt đầu cho thấy nội dung được căn chỉnh về phía cuối
Hình 5. Trình chuyển trang có khoảng đệm bắt đầu.

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

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

Trình chuyển trang có khoảng đệm bắt đầu và kết thúc cho thấy nội dung ở giữa
Hình 6. Trình chuyển trang có khoảng đệm ngang.

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

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

Trình chuyển trang có khoảng đệm bắt đầu và kết thúc cho thấy nội dung được căn chỉnh theo điểm bắt đầu
Hình 7. Trình chuyển trang có khoảng đệm cuối.

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

Bạn có thể đặt giá trị top và bottom để đạt được hiệu ứng tương tự cho VerticalPager. Giá trị 32.dp chỉ được dùng làm ví dụ; bạn có thể đặt mỗi kích thước khoảng đệm thành bất kỳ giá trị nào.

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

Các thành phần kết hợp HorizontalPager và VerticalPager mặc định chỉ định cách cử chỉ 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, HorizontalPager và VerticalPager đặt số trang tối đa mà cử chỉ hất có thể cuộn qua thành 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)
    }
}

Tạo trình chuyển trang tự động chuyển

Phần này mô tả cách tạo trình chuyển trang tự động có chỉ báo trang trong Compose. Tập hợp các mục sẽ tự động cuộn theo chiều ngang, nhưng người dùng cũng có thể vuốt giữa các mục theo cách thủ công. Nếu người dùng tương tác với trình chuyển trang, thì quá trình chuyển trang tự động sẽ dừng lại.

Ví dụ cơ bản

Cùng nhau, các đoạn mã sau đây tạo ra một cách triển khai trình chuyển trang tự động chuyển cơ bản có chỉ báo hình ảnh, trong đó mỗi trang hiển thị dưới dạng một màu khác nhau:

@Composable
fun AutoAdvancePager(pageItems: List<Color>, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        val pagerState = rememberPagerState(pageCount = { pageItems.size })
        val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState()

        val pageInteractionSource = remember { MutableInteractionSource() }
        val pageIsPressed by pageInteractionSource.collectIsPressedAsState()

        // Stop auto-advancing when pager is dragged or one of the pages is pressed
        val autoAdvance = !pagerIsDragged && !pageIsPressed

        if (autoAdvance) {
            LaunchedEffect(pagerState, pageInteractionSource) {
                while (true) {
                    delay(2000)
                    val nextPage = (pagerState.currentPage + 1) % pageItems.size
                    pagerState.animateScrollToPage(nextPage)
                }
            }
        }

        HorizontalPager(
            state = pagerState
        ) { page ->
            Text(
                text = "Page: $page",
                textAlign = TextAlign.Center,
                modifier = modifier
                    .fillMaxSize()
                    .background(pageItems[page])
                    .clickable(
                        interactionSource = pageInteractionSource,
                        indication = LocalIndication.current
                    ) {
                        // Handle page click
                    }
                    .wrapContentSize(align = Alignment.Center)
            )
        }

        PagerIndicator(pageItems.size, pagerState.currentPage)
    }
}

Các điểm chính về mã

  • Hàm AutoAdvancePager tạo một chế độ xem phân trang theo chiều ngang với tính năng tự động chuyển trang. Hàm này lấy danh sách các đối tượng Color làm dữ liệu đầu vào, được dùng làm màu nền cho mỗi trang.
  • pagerState được tạo bằng rememberPagerState, giữ trạng thái của trình chuyển trang.
  • pagerIsDragged và pageIsPressed theo dõi hoạt động tương tác của người dùng.
  • LaunchedEffect tự động chuyển trang sau mỗi hai giây, trừ phi người dùng kéo trình chuyển trang hoặc nhấn vào một trong các trang.
  • HorizontalPager hiển thị danh sách các trang, mỗi trang có một composable Text hiển thị số trang. Đối tượng sửa đổi lấp đầy trang, đặt màu nền từ pageItems và cho phép nhấp vào trang.

@Composable
fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) {
    Box(modifier = Modifier.fillMaxSize()) {
        Row(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .align(Alignment.BottomCenter)
                .padding(bottom = 8.dp),
            horizontalArrangement = Arrangement.Center
        ) {
            repeat(pageCount) { iteration ->
                val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray
                Box(
                    modifier = modifier
                        .padding(2.dp)
                        .clip(CircleShape)
                        .background(color)
                        .size(16.dp)
                )
            }
        }
    }
}

Các điểm chính về mã

  • Thành phần kết hợp Box đóng vai trò là phần tử gốc và chứa Row để sắp xếp các chỉ báo trang theo chiều ngang.
  • Chỉ báo trang tuỳ chỉnh được hiển thị dưới dạng một hàng hình tròn, trong đó mỗi Box được cắt thành CircleShape đại diện cho một trang.
  • Vòng tròn của trang hiện tại có màu DarkGray, trong khi các vòng tròn khác có màu LightGray. Tham số currentPageIndex xác định vòng tròn nào sẽ hiển thị màu xám đậm.

Kết quả

Video này cho thấy trình chuyển trang tự động chuyển cơ bản từ các đoạn mã trước:

Hình 8. Trình chuyển trang tự động chuyển với độ trễ 2 giây giữa mỗi lần chuyển trang.

Tài nguyên khác