Công cụ phân trang trong Compose

Để lật nội dung theo cách trái và phải hoặc lên và xuống, bạn có thể sử dụng thời gian HorizontalPagerVerticalPager thành phần kết hợp 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 chế độ xem hệ thống. 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à các trình phân trang chỉ hất một trang lên bất cứ lúc nào. Tất cả các chế độ mặc định này đều có thể định cấu hình.

HorizontalPager

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

Hình 1. Bản minh hoạ về 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 chế độ phân trang cuộn lên và xuống, hãy sử dụng VerticalPager:

Hình 2. Bản minh hoạ về 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 lười biếng được soạn thảo và bố cục rõ ràng khi cần. Với tư cách 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 là bắt buộc.

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 chế độ phân trang

Để cuộn đến một trang nhất định trong trình phân trang, hãy tạo một PagerState đối tượng đang sử dụng rememberPagerState() và truyền dưới dạng tham số state tới trình phân trang. Bạn có thể gọi PagerState#scrollToPage() trên 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 PagerState#animateScrollToPage() hàm:

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 kèm theo thông tin về trang: currentPage, settledPage, và targetPage.

  • currentPage: Trang gần vị trí chụp nhanh nhất. Theo mặc định, ảnh chụp nhanh vị trí là ở đầu bố cục.
  • settledPage: Số trang khi không có ảnh động hoặc tính năng cuộn nào đang chạy. Chiến dịch 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, nhưng settledPage vẫn giữ nguyên cho đến khi tất cả ảnh động chạy xong.
  • targetPage: Vị trí dừng đề xuất cho chuyển động cuộn.

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

Ví dụ: nếu muốn có chỉ báo vòng tròn đơn giản, bạn có thể lặp lại số lần 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, 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)
        )
    }
}

Số máy nhắn tin hiển thị chỉ báo vòng tròn bên dưới nội dung
Hình 3. Số máy nhắn tin hiển thị 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

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 trình phân trang mục. Để tìm hiểu khoảng cách từ một trang đến trang hiện được chọn, bạn có thể sử dụng PagerState.currentPageOffsetFraction. Sau đó, bạn có thể áp dụng các hiệu ứng biến đổi cho nội dung dựa trên khoảng cách khỏi trang đã chọn.

Hình 4. Áp dụng phép biến đổi cho nội dung 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 của các mục đó căn giữa, 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 chiếm toàn bộ chiều rộng hoặc chiều cao đầy đủ tương ứng. Bạn có thể đặt biến pageSize để có Fixed, Fill (mặc định) hoặc phép tính kích thước tuỳ chỉnh.

Ví dụ: để đặt mộ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 kích thước trang dựa trên kích thước khung nhìn, hãy sử dụng kích thước trang tuỳ chỉnh tính toán. Tạo một hình thức tuỳ chỉnh PageSize rồi 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

HorizontalPagerVerticalPager đề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ăn chỉnh trang.

Ví dụ: việc đặt khoảng đệm start sẽ căn chỉnh các trang theo hướng cuối:

Máy nhắn tin có khoảng đệm bắt đầu cho thấy nội dung được căn chỉnh ở 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:

Số máy nhắn tin có khoảng đệm đầu và cuối cho thấy nội dung ở giữa

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 theo hướng bắt đầu:

Số máy nhắn tin có khoảng đệm đầu và cuối hiển thị nội dung được căn chỉnh ở vị trí bắt đầu

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

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

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

Các thành phần kết hợp HorizontalPagerVerticalPager mặc định sẽ chỉ định cách thức cử chỉ cuộn hoạt động với trình phâ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

Theo mặc định, HorizontalPagerVerticalPager đặt số lượng tối đa các trang mà cử chỉ hất có thể cuộn qua một trang mỗi lần. Để thay đổi cái này, đặt pagerSnapDistance vào 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),
        beyondBoundsPageCount = 10,
        flingBehavior = fling
    ) {
        PagerSampleItem(page = it)
    }
}