Quản lý tiêu điểm bàn phím trong Compose

1. Giới thiệu

Thông qua bàn phím cứng, người dùng có thể tương tác với ứng dụng của bạn, thường là trên các thiết bị có màn hình lớn như máy tính bảng và thiết bị ChromeOS, nhưng cũng có thể tương tác trên các thiết bị XR. Điều quan trọng là người dùng có thể di chuyển hiệu quả trong ứng dụng bằng bàn phím cứng cũng như bằng màn hình cảm ứng. Ngoài ra, khi thiết kế ứng dụng cho TV và màn hình trên ô tô (có thể không hỗ trợ phương thức nhập bằng cách chạm mà thay vào đó là sử dụng D-pad hoặc bộ mã hoá xoay), bạn cần áp dụng các nguyên tắc di chuyển bằng bàn phím tương tự.

Compose giúp bạn xử lý thông tin đầu vào từ bàn phím cứng, D-pad và bộ mã hoá xoay theo cách thống nhất. Một nguyên tắc quan trọng để mang lại trải nghiệm tốt cho những người dùng các phương thức nhập này là họ có thể di chuyển tiêu điểm bàn phím một cách trực quan và nhất quán đến thành phần mà mình muốn tương tác.

Trong lớp học lập trình này, bạn sẽ tìm hiểu những nội dung sau:

  • Cách triển khai các mô hình quản lý tiêu điểm bàn phím phổ biến để di chuyển trực quan và nhất quán
  • Cách kiểm tra xem tiêu điểm bàn phím có di chuyển như mong đợi hay không

Điều kiện tiên quyết

  • Có kinh nghiệm dùng Compose để tạo ứng dụng
  • Có kiến thức cơ bản về Kotlin, bao gồm cả lambda và coroutine

Sản phẩm bạn sẽ tạo ra

Bạn triển khai các mô hình quản lý tiêu điểm bàn phím thông thường sau đây:

  • Di chuyển tiêu điểm bàn phím — Từ đầu đến cuối, từ trên xuống dưới theo hình chữ z
  • Tiêu điểm ban đầu hợp lý – Đặt tiêu điểm vào phần tử trên giao diện người dùng mà người dùng có khả năng tương tác
  • Khôi phục tiêu điểm – Di chuyển tiêu điểm đến phần tử trên giao diện người dùng mà người dùng từng tương tác

Kiến thức bạn sẽ học được

  • Kiến thức cơ bản về việc quản lý tiêu điểm trong Compose
  • Cách đặt một phần tử trên giao diện người dùng làm mục tiêu lấy tiêu điểm
  • Cách yêu cầu tiêu điểm để di chuyển một phần tử trên giao diện người dùng
  • Cách di chuyển tiêu điểm bàn phím đến phần tử nhất định trong một nhóm phần tử trên giao diện người dùng

Bạn cần có

  • Android Studio Ladybug trở lên
  • Bất cứ thiết bị nào sau đây để chạy ứng dụng mẫu:
  • Một thiết bị có màn hình lớn cùng với bàn phím cứng
  • Một thiết bị Android ảo dành cho thiết bị có màn hình lớn, chẳng hạn như trình mô phỏng có thể đổi kích thước

2. Thiết lập

  1. Nhân bản kho lưu trữ large-screen-codelabs trên GitHub:
git clone https://github.com/android/large-screen-codelabs

Ngoài ra, bạn có thể tải xuống và giải nén tệp zip trong kho lưu trữ large-screen-codelabs. Cách làm như sau:

  1. Chuyển đến thư mục focus-management-in-compose.
  2. Trong Android Studio, hãy mở dự án. Thư mục focus-management-in-compose chứa một dự án.
  3. Nếu bạn không dùng máy tính bảng, thiết bị gập Android hoặc thiết bị ChromeOS có bàn phím cứng, hãy mở Device Manager (Trình quản lý thiết bị) trong Android Studio rồi tạo thiết bị Resizable (Có thể đổi kích thước) trong danh mục Phone (Điện thoại).

Device Manager (Trình quản lý thiết bị) của Android Studio hiển thị danh sách các thiết bị ảo có sẵn trong danh mục điện thoại. Trình mô phỏng có thể đổi kích thước nằm trong danh mục này.Hình 1. Định cấu hình trình mô phỏng có thể đổi kích thước trong Android Studio.

3. Khám phá mã khởi đầu

Dự án này có 2 mô-đun:

  • start – Chứa mã khởi đầu của dự án. Bạn sẽ thay đổi mã này để hoàn tất lớp học lập trình.
  • solution — Chứa mã hoàn chỉnh cho lớp học lập trình này.

Ứng dụng mẫu gồm 3 thẻ:

  • Focus target (Mục tiêu lấy tiêu điểm)
  • Focus traversal order (Thứ tự duyệt qua tiêu điểm)
  • Focus group (Nhóm tiêu điểm)

Thẻ Focus target (Mục tiêu lấy tiêu điểm) sẽ xuất hiện khi ứng dụng khởi chạy.

Khung hiển thị đầu tiên của ứng dụng mẫu. Khung hiển thị này có 3 thẻ, trong đó thẻ Focus target (Mục tiêu lấy tiêu điểm) (thẻ đầu tiên) được chọn. Thẻ này hiển thị 3 thẻ nội dung được xếp theo cột.

Hình 2. Thẻ Focus target (Mục tiêu lấy tiêu điểm) sẽ xuất hiện khi ứng dụng khởi chạy.

Gói ui chứa mã giao diện người dùng sau đây mà bạn tương tác:

4. Mục tiêu lấy tiêu điểm

Mục tiêu lấy tiêu điểm là một phần tử trên giao diện người dùng mà tiêu điểm bàn phím có thể di chuyển đến. Người dùng có thể di chuyển tiêu điểm bàn phím bằng phím Tab hoặc các phím định hướng (mũi tên):

  • Phím Tab – Tiêu điểm di chuyển đến mục tiêu lấy tiêu điểm tiếp theo/trước đó theo một chiều.
  • Phím định hướng – Tiêu điểm có thể di chuyển theo hai chiều: lên, xuống, trái và phải.

Các thẻ là mục tiêu lấy tiêu điểm. Trong ứng dụng mẫu, nền của các thẻ được cập nhật trực quan khi thẻ lấy tiêu điểm.

Tệp ảnh động GIF cho thấy cách tiêu điểm bàn phím di chuyển qua các phần tử trên giao diện người dùng. Tiêu điểm sẽ di chuyển qua 3 thẻ, sau đó thẻ nội dung đầu tiên sẽ được lấy tiêu điểm.

Hình 3. Nền của thành phần sẽ thay đổi khi tiêu điểm di chuyển đến một mục tiêu lấy tiêu điểm.

Theo mặc định, các phần thử tương tác trên giao diện người dùng là mục tiêu lấy tiêu điểm

Theo mặc định, thành phần tương tác là mục tiêu lấy tiêu điểm. Nói cách khác, phần tử trên giao diện người dùng là mục tiêu lấy tiêu điểm nếu người dùng có thể nhấn vào phần tử đó.

Ứng dụng mẫu có 3 thẻ nội dung trong thẻ Focus target (Mục tiêu lấy tiêu điểm). Thẻ nội dung 1thẻ nội dung 3 là mục tiêu lấy tiêu điểm; riêng thẻ nội dung 2 thì không. Nền của thẻ nội dung 3 được cập nhật khi người dùng di chuyển tiêu điểm từ thẻ nội dung 1 bằng phím Tab.

Ảnh động GIF cho thấy cách di chuyển ban đầu của tiêu điểm bàn phím trong thẻ Focus target (Mục tiêu lấy tiêu điểm). Khi người dùng nhấn phím Tab trên thẻ nội dung 1, tiêu điểm sẽ bỏ qua thẻ nội dung 2 và chuyển đến thẻ nội dung 3 từ thẻ nội dung 1.

Hình 4. Mục tiêu lấy tiêu điểm của ứng dụng không bao gồm thẻ nội dung 2.

Sửa đổi thẻ nội dung 2 thành mục tiêu lấy tiêu điểm

Bạn có thể đặt thẻ nội dung 2 làm mục tiêu lấy tiêu điểm bằng cách thay đổi thẻ này thành một phần tử tương tác trên giao diện người dùng. Cách dễ nhất là dùng đối tượng sửa đổi clickable như sau:

  1. Mở FocusTargetTab.kt trong gói tabs
  2. Sửa đổi thành phần kết hợp SecondCard bằng đối tượng sửa đổi clickable như sau:
@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }
}

Chạy ứng dụng

Giờ đây, người dùng có thể di chuyển tiêu điểm sang thẻ nội dung 2 ngoài thẻ nội dung 1thẻ nội dung 3. Bạn có thể thử trên thẻ Focus target (Mục tiêu lấy tiêu điểm); xác nhận rằng bạn có thể di chuyển tiêu điểm từ thẻ nội dung 1 sang thẻ nội dung 2 bằng phím Tab.

Ảnh động GIF cho thấy cách tiêu điểm bàn phím di chuyển sau khi sửa đổi. Khi người dùng nhấn phím Tab trên thẻ nội dung 1, tiêu điểm sẽ di chuyển từ thẻ nội dung 1.

Hình 5. Di chuyển tiêu điểm từ thẻ nội dung 1 sang thẻ nội dung 2 bằng phím Tab.

5. Duyệt qua tiêu điểm theo hình chữ z

Người dùng mong muốn tiêu điểm bàn phím di chuyển từ trái sang phải và từ trên xuống dưới trong phần cài đặt ngôn ngữ viết từ trái sang phải. Thứ tự duyệt qua tiêu điểm này được gọi là hình chữ z.

Tuy nhiên, Compose không xác định mục tiêu lấy tiêu điểm tiếp theo của phím Tab dựa vào bố cục, mà thay vào đó sử dụng cách duyệt qua tiêu điểm theo một chiều dựa trên thứ tự của các lệnh gọi hàm có khả năng kết hợp.

Duyệt qua tiêu điểm theo một chiều

Thứ tự duyệt qua tiêu điểm theo một chiều được xác định dựa trên thứ tự của các lệnh gọi hàm có khả năng kết hợp, chứ không phải bố cục ứng dụng.

Trong ứng dụng mẫu, tiêu điểm di chuyển trên thẻ Focus traversal order (Thứ tự duyệt qua tiêu điểm) theo thứ tự sau:

  1. Thẻ nội dung 1
  2. Thẻ nội dung 4
  3. Thẻ nội dung 3
  4. Thẻ nội dung 2

Ảnh động GIF cho thấy tiêu điểm bàn phím di chuyển theo cách khác với mong đợi của người dùng.  Tiêu điểm sẽ di chuyển từ thẻ nội dung 1 sang thẻ nội dung 3, sau đó đến thẻ nội dung 4 và thẻ nội dung 2. Điều này có thể không như người dùng mong đợi.

Hình 6. Quá trình duyệt qua tiêu điểm tuân theo thứ tự của các hàm có khả năng kết hợp.

Hàm FocusTraversalOrderTab triển khai thẻ Focus traversal order (Thứ tự duyệt qua tiêu điểm) của ứng dụng mẫu. Hàm này gọi các hàm có khả năng kết hợp cho thẻ nội dung: FirstCard, FourthCard, ThirdCardSecondCard theo thứ tự đó.

@Composable
fun FocusTraversalOrderTab(
    modifier: Modifier = Modifier
) {
    Row(
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(x = 256.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier
                    .width(240.dp)
                    .offset(y = (-151).dp)
            )
        }
        SecondCard(
            modifier = Modifier.width(240.dp)
        )
    }
}

Di chuyển tiêu điểm theo hình chữ z

Bạn có thể tích hợp cách di chuyển tiêu điểm theo hình chữ z trong thẻ Focus traversal order (Thứ tự duyệt qua tiêu điểm) của ứng dụng mẫu theo các bước sau:

  1. Mở tabs.FocusTraversalOrderTab.kt
  2. Xoá đối tượng sửa đổi bù trừ khỏi các thành phần kết hợp ThirdCardFourthCard.
  3. Thay đổi bố cục của thẻ từ 1 hàng 2 cột (hiện tại) thành 1 cột 2 hàng.
  4. Di chuyển các thành phần kết hợp FirstCardSecondCard sang hàng đầu tiên.
  5. Di chuyển các thành phần kết hợp ThirdCardFourthCard sang hàng thứ hai.

Mã đã sửa đổi sẽ có dạng như sau:

@Composable
fun FocusTraversalOrderTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            FirstCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp),
            )
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(240.dp)
            )
        }
    }
}

Chạy ứng dụng

Giờ đây, người dùng có thể di chuyển tiêu điểm từ phải sang trái, từ trên xuống dưới theo hình chữ z. Bạn có thể dùng phím Tab để xác nhận rằng tiêu điểm di chuyển theo thứ tự sau trong thẻ Focus traversal order (Thứ tự duyệt qua tiêu điểm):

  1. Thẻ nội dung 1
  2. Thẻ nội dung 2
  3. Thẻ nội dung 3
  4. Thẻ nội dung 4

Ảnh động GIF cho thấy cách tiêu điểm bàn phím di chuyển sau khi sửa đổi. Tiêu điểm sẽ di chuyển từ trái sang phải, từ trên xuống dưới, theo thứ tự hình chữ z.

Hình 7. Duyệt qua tiêu điểm theo hình chữ z.

6. focusGroup

Tiêu điểm di chuyển từ thẻ nội dung 1 sang thẻ nội dung 3 trên thẻ Focus group (Nhóm tiêu điểm) bằng phím định hướng right. Người dùng có thể hơi bối rối khi thấy cách di chuyển này vì 2 thẻ nội dung không nằm cạnh nhau.

Ảnh động GIF cho thấy tiêu điểm bàn phím di chuyển từ thẻ nội dung 1 sang thẻ nội dung 3 bằng phím định hướng phải. 2 thẻ nội dung này nằm trong các hàng khác nhau.

Hình 8. Di chuyển tiêu điểm ngoài mong muốn từ thẻ nội dung 1 sang thẻ nội dung 3.

Duyệt qua tiêu điểm theo hai chiều dựa trên thông tin bố cục

Thao tác nhấn phím định hướng sẽ kích hoạt quá trình duyệt qua tiêu điểm theo hai chiều. Đây là cách di chuyển tiêu điểm phổ biến trên TV khi người dùng tương tác với ứng dụng của bạn bằng D-pad. Thao tác nhấn phím định hướng trên bàn phím cũng kích hoạt quá trình duyệt qua tiêu điểm theo hai chiều vì các phím này mô phỏng thao tác di chuyển bằng D-pad.

Trong quá trình duyệt qua tiêu điểm theo hai chiều, hệ thống tham chiếu đến thông tin hình học của các phần tử trên giao diện người dùng và xác định mục tiêu lấy tiêu điểm để di chuyển tiêu điểm. Ví dụ: tiêu điểm sẽ di chuyển đến thẻ nội dung 1 từ thẻ Focus target (Mục tiêu lấy tiêu điểm) bằng phím định hướng down, và thao tác nhấn phím định hướng lên sẽ di chuyển tiêu điểm đến thẻ Focus target (Mục tiêu lấy tiêu điểm).

Ảnh GIF cho thấy tiêu điểm di chuyển đến thẻ nội dung 1 từ thẻ Focus target (Mục tiêu lấy tiêu điểm) bằng phím định hướng xuống, sau đó quay lại thẻ bằng phím định hướng lên. 2 mục tiêu lấy tiêu điểm này là những mục tiêu gần nhất theo chiều dọc.

Hình 9. Duyệt qua tiêu điểm bằng phím định hướng xuống và lên.

Quá trình duyệt qua tiêu điểm theo hai chiều không quay vòng, trái ngược với quá trình duyệt qua tiêu điểm theo một chiều bằng phím Tab. Ví dụ: người dùng không thể di chuyển tiêu điểm bằng phím định hướng xuống khi thẻ nội dung 2 được lấy tiêu điểm.

Ảnh GIF cho thấy tiêu điểm vẫn là thẻ nội dung 2 ngay cả khi người dùng nhấn phím định hướng xuống vì không có mục tiêu lấy tiêu điểm nào được đặt bên dưới thẻ nội dung này.

Hình 10. Phím định hướng xuống không thể di chuyển tiêu điểm khi thẻ nội dung 2 được lấy tiêu điểm.

Mục tiêu lấy mục tiêu ở cùng cấp

Mã sau đây triển khai màn hình được đề cập ở trên. Có 4 mục tiêu lấy tiêu điểm: FirstCard, SecondCard, ThirdCardFourthCard. 4 mục tiêu lấy tiêu điểm này ở cùng cấp và ThirdCard là mục đầu tiên ở bên phải FirstCard trong bố cục. Đó là lý do tiêu điểm di chuyển từ thẻ nội dung 1 sang thẻ nội dung 3 bằng phím định hướng right.

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Nhóm mục tiêu lấy tiêu điểm bằng đối tượng sửa đổi focusGroup

Bạn có thể thay đổi cách di chuyển tiêu điểm gây nhầm lẫn theo các bước sau:

  1. Mở tabs.FocusGroup.kt
  2. Sửa đổi hàm có khả năng kết hợp Column trong hàm có khả năng kết hợp FocusGroupTab bằng đối tượng sửa đổi focusGroup.

Mã đã cập nhật sẽ có dạng như sau:

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier.focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Đối tượng sửa đổi focusGroup tạo một nhóm tiêu điểm gồm các mục tiêu lấy tiêu điểm bên trong thành phần đã sửa đổi. Các mục tiêu lấy tiêu điểm trong cũng như ngoài nhóm tiêu điểm này ở các cấp khác nhau và không có mục tiêu lấy tiêu điểm nào được đặt ở bên phải của thành phần kết hợp FirstCard. Do đó, tiêu điểm không di chuyển đến bất kỳ thẻ nội dung nào từ thẻ nội dung 1 bằng phím định hướng right.

Chạy ứng dụng

Giờ đây, tiêu điểm không di chuyển từ thẻ nội dung 1 sang thẻ nội dung 3 bằng phím định hướng right trong thẻ Focus group (Nhóm tiêu điểm) của ứng dụng mẫu.

7. Yêu cầu lấy tiêu điểm

Người dùng không thể sử dụng bàn phím hoặc D-pad để chọn các phần tử tuỳ ý trên giao diện người dùng để tương tác. Người dùng cần di chuyển tiêu điểm bàn phím đến một thành phần tương tác thì mới tương tác được với phần tử đó.

Ví dụ: người dùng cần di chuyển tiêu điểm từ thẻ Focus target (Mục tiêu lấy tiêu điểm) sang thẻ nội dung 1 thì mới tương tác được với thẻ nội dung này. Bạn có thể giảm số lượng thao tác để bắt đầu tác vụ chính của người dùng bằng cách đặt tiêu điểm ban đầu một cách hợp lý.

Ảnh động GIF cho thấy người dùng nên nhấn phím Tab 3 lần sau khi chọn thẻ để di chuyển tiêu điểm bàn phím đến thẻ nội dung 1 trong thẻ đó.

Hình 11. 3 lần nhấn phím Tab sẽ di chuyển tiêu điểm đến thẻ nội dung 1.

Yêu cầu lấy tiêu điểm bằng FocusRequester

Bạn có thể sử dụng FocusRequester để yêu cầu lấy tiêu điểm để di chuyển một phần tử trên giao diện người dùng. Bạn phải liên kết đối tượng FocusRequester với một phần tử trên giao diện người dùng trước khi gọi phương thức requestFocus().

Đặt tiêu điểm ban đầu thành thẻ nội dung 1

Bạn có thể đặt tiêu điểm ban đầu thành thẻ nội dung 1 theo các bước sau:

  1. Mở tabs.FocusTarget.kt
  2. Khai báo giá trị firstCard trong hàm có khả năng kết hợp FocusTargetTab và khởi chạy giá trị này bằng đối tượng FocusRequester được trả về từ hàm remember.
  3. Dùng đối tượng sửa đổi focusRequester để sửa đổi hàm có khả năng kết hợp FirstCard.
  4. Chỉ định giá trị firstCard làm đối số của đối tượng sửa đổi focusRequester.
  5. Gọi hàm có khả năng kết hợp LaunchedEffect bằng giá trị Unit và gọi phương thức requestFocus() qua giá trị firstCard trong biểu thức lambda được truyền vào hàm có khả năng kết hợp LaunchedEffect.

Đối tượng FocusRequester được tạo và liên kết với một phần tử trên giao diện người dùng ở bước thứ hai và thứ ba. Ở bước thứ năm, tiêu điểm được yêu cầu di chuyển đến phần tử được liên kết trên giao diện người dùng khi thành phần kết hợp FocusdTargetTab được soạn lần đầu.

Mã đã cập nhật sẽ có dạng như sau:

@Composable
fun FocusTargetTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    val firstCard = remember { FocusRequester() }

    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier
                .width(240.dp)
                .focusRequester(focusRequester = firstCard)
        )
        SecondCard(
            modifier = Modifier
                .width(240.dp)
                .clickable(onClick = onClick)
        )
        ThirdCard(
            onClick = onClick,
            modifier = Modifier.width(240.dp)
        )
    }

    LaunchedEffect(Unit) {
        firstCard.requestFocus()
    }
}

Chạy ứng dụng

Giờ đây, tiêu điểm bàn phím sẽ di chuyển đến thẻ nội dung 1 trong thẻ Focus target (Mục tiêu lấy tiêu điểm) khi thẻ này được chọn. Bạn có thể thử bằng cách chuyển đổi thẻ. Ngoài ra, thẻ nội dung 1 sẽ được chọn khi ứng dụng khởi chạy.

Ảnh động GIF cho thấy tiêu điểm bàn phím tự động di chuyển đến thẻ nội dung 1 khi người dùng chọn thẻ Focus target (Mục tiêu lấy tiêu điểm).

Hình 12. Tiêu điểm sẽ di chuyển đến thẻ nội dung 1 khi bạn chọn thẻ Focus target (Mục tiêu lấy tiêu điểm).

8. Di chuyển tiêu điểm đến thẻ đã chọn

Bạn có thể chỉ định mục tiêu lấy tiêu điểm khi tiêu điểm bàn phím đang chuyển vào một nhóm tiêu điểm. Ví dụ: bạn có thể di chuyển tiêu điểm đến thẻ đã chọn khi người dùng di chuyển tiêu điểm đến hàng trong thẻ.

Bạn có thể triển khai hành vi này theo các bước sau:

  1. Mở App.kt.
  2. Khai báo giá trị focusRequesters trong hàm có khả năng kết hợp App.
  3. Khởi chạy giá trị focusRequesters bằng giá trị trả về của hàm remember trả về danh sách các đối tượng FocusRequester. Độ dài của danh sách được trả về phải bằng độ dài của Screens.entries.
  4. Liên kết từng đối tượng FocusRequester của giá trị focusRequester với thành phần kết hợp Tab bằng cách sửa đổi thành phần kết hợp Tab bằng đối tượng sửa đổi focusRequester.
  5. Sửa đổi thành phần kết hợp PrimaryTabRow bằng đối tượng sửa đổi focusPropertiesfocusGroup.
  6. Truyền một biểu thức lambda vào đối tượng sửa đổi focusProperties và liên kết thuộc tính enter với một biểu thức lambda khác.
  7. Trả về FocusRequester (được lập chỉ mục bằng giá trị selectedTabIndex trong giá trị focusRequesters) từ biểu thức lambda liên kết với thuộc tính enter.

Mã đã sửa đổi sẽ có dạng như sau:

@Composable
fun App(
    modifier: Modifier = Modifier,
) {
    val context = LocalContext.current

    var selectedScreen by rememberSaveable { mutableStateOf(Screen.FocusTarget) }
    val selectedTabIndex = Screen.entries.indexOf(selectedScreen)
    val focusRequesters = remember {
        List(Screen.entries.size) { FocusRequester() }
    }

    Column(modifier = modifier) {
        PrimaryTabRow(
            selectedTabIndex = selectedTabIndex,
            modifier = Modifier
                .focusProperties {
                    enter = {
                        focusRequesters[selectedTabIndex]
                    }
                }
                .focusGroup()
        ) {
            Screen.entries.forEachIndexed { index, screen ->
                Tab(
                    selected = screen == selectedScreen,
                    onClick = { selectedScreen = screen },
                    text = { Text(stringResource(screen.title)) },
                    modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
                )
            }
        }
        when (selectedScreen) {
            Screen.FocusTarget -> {
                FocusTargetTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp),
                )
            }

            Screen.FocusTraversalOrder -> {
                FocusTraversalOrderTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp)
                )
            }

            Screen.FocusRestoration -> {
                FocusGroupTab(
                    onClick = context::onCardClicked,
                    modifier = Modifier.padding(32.dp)
                )
            }
        }
    }
}

Bạn có thể kiểm soát cách di chuyển tiêu điểm bằng đối tượng sửa đổi focusProperties. Trong biểu thức lambda được truyền vào đối tượng sửa đổi, hãy sửa đổi FocusProperties. Đối tượng này được hệ thống tham chiếu trong lúc chọn mục tiêu lấy tiêu điểm khi người dùng nhấn phím Tab hoặc phím định hướng, trong trường hợp phần tử đã sửa đổi trên giao diện người dùng được lấy tiêu điểm.

Khi bạn đặt thuộc tính enter, hệ thống sẽ đánh giá biểu thức lambda được đặt thành thuộc tính này và chuyển đến phần tử được liên kết với đối tượng FocusRequester (do biểu thức lambda được đánh giá trả về) trên giao diện người dùng.

Chạy ứng dụng

Giờ đây, tiêu điểm bàn phím sẽ di chuyển đến thẻ đã chọn khi người dùng di chuyển tiêu điểm đến hàng trong thẻ. Bạn có thể thử theo các bước sau:

  1. Chạy ứng dụng
  2. Chọn thẻ Focus group (Nhóm tiêu điểm)
  3. Di chuyển tiêu điểm đến thẻ nội dung 1 bằng phím định hướng down.
  4. Di chuyển tiêu điểm bằng phím định hướng up.

Hình 13. Tiêu điểm sẽ chuyển đến thẻ đã chọn.

9. Khôi phục tiêu điểm

Người dùng mong muốn có thể dễ dàng tiếp tục một tác vụ khi tác vụ đó bị gián đoạn. Tính năng khôi phục tiêu điểm hỗ trợ khôi phục sau khi bị gián đoạn. Tính năng khôi phục tiêu điểm sẽ di chuyển tiêu điểm bàn phím đến phần tử đã được chọn trước đó trên giao diện người dùng.

Một trường hợp sử dụng điển hình của tính năng khôi phục tiêu điểm là màn hình chính của các ứng dụng phát video trực tuyến. Màn hình này có nhiều danh sách nội dung video, chẳng hạn như phim trong một danh mục hoặc các tập của một chương trình truyền hình. Người dùng xem qua các danh sách và tìm thấy nội dung thú vị. Đôi khi, người dùng quay lại danh sách đã kiểm tra trước đó và tiếp tục duyệt xem danh sách nêu trên. Với tính năng khôi phục tiêu điểm, người dùng có thể tiếp tục duyệt web mà không cần di chuyển tiêu điểm bàn phím đến mục cuối cùng mà họ đã xem trong danh sách.

Đối tượng sửa đổi focusRestorer khôi phục tiêu điểm về một nhóm tiêu điểm

Dùng đối tượng sửa đổi focusRestorer để lưu và khôi phục tiêu điểm về một nhóm tiêu điểm. Khi tiêu điểm rời khỏi nhóm tiêu điểm, tiêu điểm sẽ lưu trữ một tệp tham chiếu đến mục đã được lấy tiêu điểm trước đó. Sau đó, khi tiêu điểm quay lại nhóm tiêu điểm, tiêu điểm sẽ được khôi phục về mục đã được lấy làm tiêu điểm trước đó.

Tích hợp tính năng khôi phục tiêu điểm bằng thẻ Focus group (Nhóm tiêu điểm)

Thẻ Focus group (Nhóm tiêu điểm) của ứng dụng mẫu có một hàng chứa thẻ nội dung 2, thẻ nội dung 3thẻ nội dung 4.

Ảnh GIF động cho thấy tiêu điểm bàn phím di chuyển từ thẻ nội dung 1 sang thẻ nội dung 2, ngay cả khi thẻ nội dung 3 được lấy tiêu điểm trước đó.

Hình 14. Nhóm tiêu điểm chứa thẻ nội dung 2, thẻ nội dung 3thẻ nội dung 4.

Bạn có thể tích hợp tính năng khôi phục tiêu điểm trong hàng theo các bước sau:

  1. Mở tab.FocusGroupTab.kt
  2. Dùng đối tượng sửa đổi focusRestorer để sửa đổi thành phần kết hợp Row trong thành phần kết hợp FocusGroupTab. Bạn nên gọi đối tượng sửa đổi này trước đối tượng sửa đổi focusGroup.

Mã đã sửa đổi sẽ có dạng như sau:

@Composable
fun FocusGroupTab(
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = modifier,
    ) {
        FirstCard(
            onClick = onClick,
            modifier = Modifier.width(208.dp)
        )
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier
                .focusRestorer()
                .focusGroup(),
        ) {
            SecondCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            ThirdCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
            FourthCard(
                onClick = onClick,
                modifier = Modifier.width(208.dp)
            )
        }
    }
}

Chạy ứng dụng

Giờ đây, hàng trong thẻ Focus group (Nhóm tiêu điểm) sẽ khôi phục tiêu điểm và bạn có thể thử theo các bước sau:

  1. Chọn thẻ Focus group (Nhóm tiêu điểm)
  2. Di chuyển tiêu điểm đến thẻ nội dung 1
  3. Di chuyển tiêu điểm đến thẻ nội dung 4 bằng phím Tab
  4. Di chuyển tiêu điểm đến thẻ nội dung 1 bằng phím định hướng up
  5. Nhấn phím Tab

Tiêu điểm bàn phím di chuyển đến thẻ nội dung 4 khi đối tượng sửa đổi focusRestorer lưu tệp tham chiếu của thẻ nội dung đó và khôi phục tiêu điểm khi tiêu điểm bàn phím chuyển vào nhóm tiêu điểm được đặt thành hàng.

Ảnh động GIF cho thấy tiêu điểm bàn phím di chuyển đến thẻ nội dung đã chọn trước đó trong một hàng khi tiêu điểm bàn phím quay lại thẻ nội dung đó.

Hình 15. Tiêu điểm quay lại thẻ nội dung 4 sau khi nhấn phím định hướng lên, theo sau là nhấn phím Tab.

10. Viết chương trình kiểm thử

Bạn có thể kiểm thử tính năng quản lý tiêu điểm bàn phím đã triển khai bằng các chương trình kiểm thử. Compose cung cấp một API để kiểm tra xem một phần tử trên giao diện người dùng có được lấy tiêu điểm hay không và thực hiện thao tác nhấn phím đối với các thành phần trên giao diện người dùng. Hãy tham khảo lớp học lập trình Kiểm thử trong Jetpack Compose để biết thêm thông tin.

Kiểm thử thẻ Focus target (Mục tiêu lấy tiêu điểm)

Bạn đã sửa đổi hàm có khả năng kết hợp FocusTargetTab để đặt thẻ nội dung 2 làm mục tiêu lấy tiêu điểm trong phần trước. Hãy viết một chương trình kiểm thử cho quá trình triển khai mà bạn đã thực hiện theo cách thủ công trong phần trước. Bạn có thể viết chương trình kiểm thử theo các bước sau:

  1. Mở FocusTargetTabTest.kt. Bạn sẽ sửa đổi hàm testSecondCardIsFocusTarget trong các bước sau.
  2. Yêu cầu tiêu điểm di chuyển đến thẻ nội dung 1 bằng cách gọi phương thức requestFocus trên đối tượng SemanticsNodeInteraction cho thẻ nội dung 1.
  3. Đảm bảo rằng thẻ nội dung 1 được lấy tiêu điểm bằng phương thức assertIsFocused().
  4. Thực hiện thao tác nhấn phím Tab bằng cách gọi phương thức pressKey với giá trị Key.Tab bên trong biểu thức lambda được truyền vào phương thức performKeyInput.
  5. Kiểm thử xem tiêu điểm bàn phím có di chuyển đến thẻ nội dung 2 hay không bằng cách gọi phương thức assertIsFocused() trên đối tượng SemanticsNodeInteraction cho thẻ nội dung 2.

Mã đã cập nhật sẽ có dạng như sau:

@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
    composeTestRule.setContent {
        LocalInputModeManager
            .current
            .requestInputMode(InputMode.Keyboard)
        FocusTargetTab(onClick = {})
    }
    val context = InstrumentationRegistry.getInstrumentation().targetContext

    // Ensure the 1st card is focused
    composeTestRule
        .onNodeWithText(context.getString(R.string.first_card))
        .requestFocus()
        .performKeyInput { pressKey(Key.Tab) }

    // Test if focus moves to the 2nd card from the 1st card with Tab key
    composeTestRule
        .onNodeWithText(context.getString(R.string.second_card))
        .assertIsFocused()
}

Chạy ứng dụng

Bạn có thể chạy chương trình kiểm thử bằng cách nhấp vào biểu tượng tam giác hiển thị ở bên trái phần khai báo lớp FocusTargetTest. Hãy tham khảo phần Chạy kiểm thử trong bài viết Kiểm thử trong Android Studio để biết thêm thông tin.

Android Studio sẽ hiển thị một trình đơn theo bối cảnh để chạy "FocusTargetTabTest".

11. Xin chúc mừng

Rất tốt! Bạn đã tìm hiểu các thành phần để quản lý tiêu điểm bàn phím:

  • Mục tiêu lấy tiêu điểm
  • Duyệt qua tiêu điểm

Bạn có thể kiểm soát thứ tự duyệt qua tiêu điểm bằng các đối tượng sửa đổi sau đây trong Compose:

  • Đối tượng sửa đổi focusGroup
  • Đối tượng sửa đổi focusProperties

Bạn đã triển khai mô hình điển hình cho trải nghiệm người dùng bằng bàn phím cứng, tiêu điểm ban đầu và khôi phục tiêu điểm. Các mô hình này được triển khai bằng cách kết hợp các API sau:

  • Lớp FocusRequester
  • Đối tượng sửa đổi focusRequester
  • Đối tượng sửa đổi focusRestorer
  • Hàm có khả năng kết hợp LaunchedEffect

Bạn có thể kiểm thử trải nghiệm người dùng đã triển khai bằng các tệp kiểm thử đo lường. Compose cung cấp các phương pháp để thực hiện thao tác nhấn phím và kiểm tra xem SemanticsNode có tiêu điểm bàn phím hay không.

Tìm hiểu thêm