Kiểm soát thứ tự duyệt qua

Theo mặc định, hành vi của trình đọc màn hình hỗ trợ tiếp cận trong ứng dụng Compose được triển khai theo thứ tự đọc dự kiến, thường là từ trái sang phải, sau đó là từ trên xuống dưới. Tuy nhiên, có một số loại bố cục ứng dụng mà thuật toán không thể xác định thứ tự đọc thực tế nếu không có gợi ý bổ sung. Trong các ứng dụng dựa trên khung hiển thị, bạn có thể khắc phục các vấn đề như vậy bằng cách sử dụng thuộc tính traversalBeforetraversalAfter. Kể từ Compose 1.5, Compose cung cấp một API linh hoạt không kém, nhưng có mô hình khái niệm mới.

isTraversalGrouptraversalIndex là các thuộc tính ngữ nghĩa cho phép bạn điều khiển chức năng hỗ trợ tiếp cận và thứ tự lấy nét TalkBack trong các trường hợp mà thuật toán sắp xếp mặc định không phù hợp. isTraversalGroup xác định các nhóm quan trọng về mặt ngữ nghĩa, trong khi traversalIndex điều chỉnh thứ tự của các phần tử riêng lẻ trong các nhóm đó. Bạn có thể sử dụng riêng isTraversalGroup hoặc sử dụng traversalIndex để tuỳ chỉnh thêm.

Sử dụng isTraversalGrouptraversalIndex trong ứng dụng của bạn để kiểm soát thứ tự truyền tải trình đọc màn hình.

Nhóm các phần tử bằng isTraversalGroup

isTraversalGroup là một thuộc tính boolean xác định xem nút ngữ nghĩa có phải là một nhóm truyền tải hay không. Loại nút này là nút có chức năng đóng vai trò là ranh giới hoặc đường viền trong việc sắp xếp các phần tử con của nút.

Việc đặt isTraversalGroup = true trên một nút có nghĩa là mọi thành phần con của nút đó đều được truy cập trước khi chuyển sang các phần tử khác. Bạn có thể đặt isTraversalGroup trên các nút có thể làm tâm điểm không phải của trình đọc màn hình, chẳng hạn như Cột, Hàng hoặc Hộp.

Ví dụ sau đây sử dụng isTraversalGroup. Biểu ngữ này trao đổi 4 thành phần văn bản. Hai phần tử bên trái thuộc về một phần tử CardBox, trong khi hai phần tử bên phải thuộc về một phần tử CardBox khác:

// CardBox() function takes in top and bottom sample text.
@Composable
fun CardBox(
    topSampleText: String,
    bottomSampleText: String,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        Column {
            Text(topSampleText)
            Text(bottomSampleText)
        }
    }
}

@Composable
fun TraversalGroupDemo() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is "
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
            topSampleText1,
            bottomSampleText1
        )
        CardBox(
            topSampleText2,
            bottomSampleText2
        )
    }
}

Mã tạo ra kết quả tương tự như sau:

Bố cục có hai cột văn bản, trong đó cột bên trái có nội dung "Câu này nằm ở cột bên trái" và cột bên phải có nội dung "Câu này nằm ở bên phải".
Hình 1. Bố cục có hai câu (một câu ở cột bên trái và một câu ở cột bên phải).

Vì chưa có ngữ nghĩa nào được thiết lập nên hành vi mặc định của trình đọc màn hình là truyền tải các phần tử từ trái sang phải và từ trên xuống dưới. Do mặc định này, TalkBack sẽ đọc các đoạn câu theo thứ tự không chính xác:

"Câu này nằm ở" → "Câu này là" → "cột bên trái". → "ở bên phải".

Để sắp xếp các phân đoạn một cách chính xác, hãy sửa đổi đoạn mã ban đầu để đặt isTraversalGroup thành true:

@Composable
fun TraversalGroupDemo2() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is"
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
//      1,
            topSampleText1,
            bottomSampleText1,
            Modifier.semantics { isTraversalGroup = true }
        )
        CardBox(
//      2,
            topSampleText2,
            bottomSampleText2,
            Modifier.semantics { isTraversalGroup = true }
        )
    }
}

isTraversalGroup được đặt cụ thể trên mỗi CardBox, nên các ranh giới CardBox sẽ áp dụng khi sắp xếp các phần tử của chúng. Trong trường hợp này, CardBox bên trái sẽ được đọc trước, sau đó là CardBox bên phải.

Lúc này, TalkBack sẽ đọc các đoạn câu theo đúng thứ tự:

"Câu này nằm ở" → "cột bên trái". → "Câu này là" → "ở bên phải".

Tuỳ chỉnh thêm thứ tự duyệt qua

traversalIndex là một thuộc tính số thực cho phép bạn tuỳ chỉnh thứ tự truyền tải TalkBack. Nếu việc nhóm các thành phần lại với nhau là chưa đủ để TalkBack hoạt động chính xác, hãy sử dụng traversalIndex kết hợp với isTraversalGroup để tuỳ chỉnh thêm thứ tự của trình đọc màn hình.

Thuộc tính traversalIndex có các đặc điểm sau:

  • Các phần tử có giá trị traversalIndex thấp hơn sẽ được ưu tiên trước.
  • Có thể mang tính tích cực hoặc tiêu cực.
  • Giá trị mặc định là 0f.
  • Chỉ ảnh hưởng đến các nút có thể lấy tiêu điểm của trình đọc màn hình, chẳng hạn như các phần tử trên màn hình như văn bản hoặc nút. Ví dụ: việc chỉ đặt traversalIndex trên một cột sẽ không có hiệu lực, trừ phi cột đó cũng đặt isTraversalGroup.

Ví dụ sau cho thấy cách bạn có thể sử dụng kết hợp traversalIndexisTraversalGroup.

Ví dụ: Mặt đồng hồ đi ngang

Mặt đồng hồ là một trường hợp phổ biến khi thứ tự truyền tải tiêu chuẩn không hoạt động. Ví dụ trong phần này là một bộ chọn giờ, trong đó người dùng có thể chuyển qua các số trên mặt đồng hồ và chọn các chữ số cho khung giờ và phút.

Mặt đồng hồ có bộ chọn giờ ở phía trên.
Hình 2. Hình ảnh mặt đồng hồ.

Trong đoạn mã được đơn giản hoá sau đây, có một CircularLayout, trong đó có 12 số được vẽ, bắt đầu bằng số 12 và di chuyển theo chiều kim đồng hồ xung quanh vòng tròn:

@Composable
fun ClockFaceDemo() {
    CircularLayout {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Vì mặt đồng hồ không được đọc một cách hợp lý với thứ tự mặc định từ trái sang phải và từ trên xuống dưới, nên TalkBack sẽ đọc các số theo thứ tự. Để khắc phục vấn đề này, hãy sử dụng giá trị bộ đếm tăng dần như trong đoạn mã sau:

@Composable
fun ClockFaceDemo() {
    CircularLayout(Modifier.semantics { isTraversalGroup = true }) {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Để thiết lập đúng thứ tự truyền tải, trước tiên, hãy đặt CircularLayout làm một nhóm truyền tải và đặt isTraversalGroup = true. Sau đó, khi mỗi văn bản đồng hồ được vẽ vào bố cục, hãy đặt traversalIndex tương ứng của văn bản đó thành giá trị bộ đếm.

Vì giá trị bộ đếm liên tục tăng nên traversalIndex của mỗi giá trị đồng hồ sẽ lớn hơn khi các số được thêm vào màn hình – giá trị đồng hồ 0 có traversalIndex là 0 và giá trị đồng hồ 1 có traversalIndex là 1. Bằng cách này, thứ tự mà TalkBack đọc các tin nhắn đó sẽ được đặt. Giờ đây, các số bên trong CircularLayout được đọc theo thứ tự dự kiến.

traversalIndexes đã được đặt chỉ tương ứng với các chỉ mục khác trong cùng một nhóm, nên phần còn lại của thứ tự màn hình đã được giữ nguyên. Nói cách khác, thay đổi về ngữ nghĩa hiển thị trong đoạn mã trước đó chỉ sửa đổi thứ tự trong mặt đồng hồ đã đặt isTraversalGroup = true.

Xin lưu ý rằng nếu bạn không đặt ngữ nghĩa CircularLayout's thành isTraversalGroup = true, những thay đổi đối với traversalIndex vẫn áp dụng. Tuy nhiên, nếu không có CircularLayout để liên kết, thì 12 chữ số của mặt đồng hồ sẽ được đọc lần cuối, sau khi mọi phần tử khác trên màn hình đã được truy cập. Điều này xảy ra vì tất cả các phần tử khác đều có traversalIndex mặc định là 0f và các phần tử văn bản đồng hồ được đọc sau tất cả các phần tử 0f khác.

Ví dụ: Tuỳ chỉnh thứ tự duyệt cho nút hành động nổi

Trong ví dụ này, traversalIndexisTraversalGroup kiểm soát thứ tự truyền tải của nút hành động nổi (FAB) trong Material Design. Cơ sở của ví dụ này là bố cục sau:

Bố cục có thanh ứng dụng trên cùng, văn bản mẫu, nút hành động nổi và thanh ứng dụng ở dưới cùng.
Hình 3. Bố cục có thanh ứng dụng trên cùng, văn bản mẫu, nút hành động nổi và thanh ứng dụng ở dưới cùng.

Theo mặc định, bố cục trong ví dụ này có thứ tự TalkBack như sau:

Thanh ứng dụng trên cùng → Văn bản mẫu từ 0 đến 6 → nút hành động nổi (FAB) → Thanh ứng dụng ở dưới cùng

Trước tiên, bạn nên cho trình đọc màn hình tập trung vào nút hành động nổi. Để đặt traversalIndex trên một phần tử Material như FAB, hãy làm như sau:

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

Trong đoạn mã này, việc tạo một hộp có isTraversalGroup được đặt thành true và đặt traversalIndex trên chính hộp đó (-1f thấp hơn giá trị mặc định là 0f) có nghĩa là hộp nổi sẽ xuất hiện trước mọi thành phần khác trên màn hình.

Tiếp theo, bạn có thể đặt hộp nổi và các thành phần khác vào một scaffold (giàn giáo), giúp triển khai bố cục Material Design:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnWithFABFirstDemo() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Top App Bar") }) },
        floatingActionButtonPosition = FabPosition.End,
        floatingActionButton = { FloatingBox() },
        content = { padding -> ContentColumn(padding = padding) },
        bottomBar = { BottomAppBar { Text("Bottom App Bar") } }
    )
}

TalkBack tương tác với các phần tử theo thứ tự sau:

FAB → Thanh ứng dụng trên cùng → Văn bản mẫu từ 0 đến 6 → Thanh ứng dụng dưới cùng

Tài nguyên khác