Các danh sách có Compose cho Wear OS


Danh sách cho phép người dùng chọn một mục trong một tập hợp các lựa chọn trên thiết bị Wear OS.

Nhiều thiết bị Wear OS sử dụng màn hình tròn, điều này khiến bạn khó xem được các mục trong danh sách xuất hiện ở gần đầu và cuối màn hình. Vì lý do này, Compose cho Wear OS thêm một phiên bản của lớp LazyColumn có tên là TransformingLazyColumn. Phiên bản này hỗ trợ các hiệu ứng chuyển tỉ lệ và biến đổi. Khi các mục di chuyển ra rìa, chúng sẽ nhỏ hơn và mờ dần.

Cách áp dụng hiệu ứng thu phóng và cuộn được đề xuất:

  1. Sử dụng Modifier.transformedHeight để cho phép Compose tính toán sự thay đổi chiều cao khi mục cuộn qua màn hình.
  2. Sử dụng transformation = SurfaceTransformation(transformationSpec) để áp dụng các hiệu ứng hình ảnh, bao gồm cả việc thu nhỏ nội dung của mục.
  3. Sử dụng TransformationSpec tuỳ chỉnh cho các thành phần không lấy transformation làm tham số, chẳng hạn như Text.

Ảnh động sau đây cho thấy cách một phần tử trong danh sách thay đổi kích thước và hình dạng khi tiến đến đầu và cuối màn hình:

Đoạn mã sau đây cho biết cách tạo danh sách bằng bố cục TransformingLazyColumn để tạo nội dung hiển thị đẹp mắt trên nhiều kích thước màn hình Wear OS.

Đoạn mã này cũng minh hoạ việc sử dụng đối tượng sửa đổi minimumVerticalContentPadding mà bạn nên đặt trên các mục trong danh sách để áp dụng khoảng đệm chính xác ở đầu và cuối danh sách.

Để hiện chỉ báo cuộn, hãy chia sẻ columnState giữa ScreenScaffoldTransformingLazyColumn:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
    scrollState = columnState
) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding
    ) {
        item {
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text(text = "Header")
            }
        }
        // ... other items
        item {
            Button(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec),
                onClick = { /* ... */ },
                icon = {
                    Icon(
                        imageVector = Icons.Default.Build,
                        contentDescription = "build",
                    )
                },
            ) {
                Text(
                    text = "Build",
                    maxLines = 1,
                    overflow = TextOverflow.Ellipsis,
                )
            }
        }
    }
}

Thêm hiệu ứng chụp nhanh và hất

Tính năng căn chỉnh đảm bảo rằng khi người dùng hoàn tất cử chỉ cuộn hoặc hất, danh sách sẽ được đặt với một mục được định vị chính xác tại một điểm cụ thể, thường là ở giữa màn hình. Trên màn hình tròn, nơi các mục được điều chỉnh tỷ lệ và biến đổi khi di chuyển ra xa tâm, tính năng căn chỉnh đặc biệt hữu ích để đảm bảo mục phù hợp nhất vẫn hiển thị đầy đủ và dễ đọc trong vùng xem tối ưu.

Để thêm hành vi hất và trượt nhanh, hãy đặt tham số flingBehavior thành TransformingLazyColumnDefaults.snapFlingBehavior(columnState). Đặt rotaryScrollableBehavior cho phù hợp bằng cách sử dụng RotaryScrollableDefaults.snapBehavior(columnState) để có trải nghiệm nhất quán khi dùng núm vặn hoặc viền vật lý.

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        flingBehavior = TransformingLazyColumnDefaults.snapFlingBehavior(columnState),
        rotaryScrollableBehavior = RotaryScrollableDefaults.snapBehavior(columnState)
    ) {
        // ...
        // ...
    }
}

Bố cục đảo ngược

Theo mặc định, danh sách có thể di chuyển sẽ được cố định vào cạnh trên cùng. Nếu người dùng đã cuộn xuống cuối danh sách tiêu chuẩn và một mục mới được thêm vào cuối, thì danh sách sẽ duy trì chế độ xem của người dùng đối với mục hiện tại. Ví dụ: nếu người dùng đang xem mục 10 ở cuối màn hình và mục 11 được thêm vào, thì khung hiển thị vẫn tập trung vào mục 10 và mục 11 sẽ xuất hiện ngoài màn hình bên dưới khung hiển thị hiện tại.

Đối với các trường hợp sử dụng như ứng dụng nhắn tin hoặc nhật ký trực tiếp, bạn thường không muốn hành vi này. Khi có mục mới, người dùng thường muốn xem ngay nội dung mới nhất nếu họ đã ở cuối danh sách. Nếu nhiều mục đến cùng lúc, danh sách sẽ bỏ qua để hiển thị mục mới nhất ở dưới cùng (nghĩa là một số mục trung gian có thể không được hiển thị trừ phi người dùng cuộn lên).

Để hỗ trợ các trường hợp sử dụng này, TransformingLazyColumn cho phép bạn đảo ngược bố cục bằng cách đặt reverseLayout = true. Thao tác này sẽ thay đổi điểm neo của danh sách từ cạnh trên thành cạnh dưới.

Để thuận tiện, việc đặt reverseLayout = true cũng đảo ngược thứ tự hiển thị của các mục và hướng của cử chỉ cuộn:

  • Các mục được tạo từ dưới lên, tức là chỉ mục 0 xuất hiện ở cuối màn hình.
  • Khi bạn di chuyển lên, các mục có chỉ mục cao hơn sẽ xuất hiện.

Để thêm hành vi hất và trượt cùng với bố cục đảo ngược, bạn có thể kết hợp flingBehaviorrotaryScrollableBehavior như minh hoạ trong đoạn mã sau:

val columnState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(scrollState = columnState) { contentPadding ->
    TransformingLazyColumn(
        state = columnState,
        contentPadding = contentPadding,
        reverseLayout = true,
        modifier = Modifier.fillMaxWidth()
    ) {
        items(10) { index ->
            Button(
                label = {
                    Text(
                        text = "Item ${index + 1}"
                    )
                },
                onClick = {},
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            )
        }
        item {
            // With reverseLayout = true, the last item declared appears at the top.
            ListHeader(
                modifier = Modifier
                    .fillMaxWidth()
                    .transformedHeight(this, transformationSpec)
                    .minimumVerticalContentPadding(ListHeaderDefaults.minimumTopListContentPadding),
                transformation = SurfaceTransformation(transformationSpec)
            ) {
                Text("Header")
            }
        }
    }
}

Các hình ảnh sau đây cho thấy sự khác biệt giữa danh sách thông thường và danh sách đảo ngược:

Một TransformingLazyColumn có bố cục bình thường, hiển thị Mục 1 ở trên cùng và các mục theo thứ tự tăng dần.
Hình 1. Một bố cục danh sách tiêu chuẩn, trong đó nội dung sẽ lấp đầy từ trên xuống dưới.
Một TransformingLazyColumn có bố cục đảo ngược, cho thấy Mục 1 ở dưới cùng và các mục theo thứ tự giảm dần về phía trên cùng.
Hình 2. Bố cục danh sách đảo ngược, trong đó nội dung lấp đầy từ dưới lên trên.

Các nút ở cạnh trên danh sách

Đối với Material 3, bạn có thể thêm một EdgeButton (nút ôm viền) ở cuối danh sách. Tuy nhiên, hãy cẩn thận để không thêm phần này dưới dạng một mục trong TransformingLazyColumn mà thay vào đó, hãy dùng khe edgeButton trong ScreenScaffold.

Việc sử dụng vùng edgeButton đảm bảo rằng nút được đặt đúng vị trí ở cuối màn hình và hoạt động phù hợp khi danh sách được cuộn.

Đoạn mã sau đây cho biết cách triển khai EdgeButton:

val columnState = rememberTransformingLazyColumnState()
ScreenScaffold(
    scrollState = columnState,
    edgeButton = {
        EdgeButton(
            onClick = { /* TODO */ },
            modifier = Modifier.scrollable(
                columnState,
                orientation = Orientation.Vertical,
                reverseDirection = true,
                // Apply overscroll to the EdgeButton for proper scrolling behavior.
                overscrollEffect = rememberOverscrollEffect(),
            )
        ) {
            Text("More")
        }
    }
) { contentPadding ->
    TransformingLazyColumn(
        contentPadding = contentPadding,
        state = columnState,
    ) {
        // ...
        // ...
    }
}

Thao tác vuốt để hiển thị trong danh sách

Thành phần SwipeToReveal cho phép bạn truy cập vào các thao tác cho một mục trong danh sách, chẳng hạn như Card hoặc Chip, bằng cách vuốt. Thao tác vuốt thường sẽ hiển thị một hoặc hai nút thao tác (chẳng hạn như "Xoá" hoặc "Tuỳ chọn khác") ở bên cạnh.

Khi sử dụng SwipeToReveal trong TransformingLazyColumn, hãy làm theo các nguyên tắc sau:

  • Đặt lại khi cuộn: Khi người dùng cuộn danh sách, hãy đặt lại mọi mục đã mở bằng thao tác vuốt về trạng thái được che.
  • Chiều cao nhất quán: Đặt chiều cao của nút hành động sao cho khớp với mục được vuốt bên trong (cho dù đó là Button hay Card) để đảm bảo giao diện nhất quán.
  • Biến đổi vùng chứa: Áp dụng đối tượng sửa đổi transformedHeighttransformationSpec cho chính thành phần SwipeToReveal.
  • Không biến đổi gấp đôi: Đừng áp dụng các đối tượng sửa đổi transformedHeight hoặc transformation cho mục được vuốt bên trong (thẻ hoặc nút bên trong vùng chứa SwipeToReveal).