Urutan traversal kontrol

Secara default, perilaku pembaca layar aksesibilitas di aplikasi Compose diimplementasikan dalam urutan pembacaan yang diharapkan, yaitu biasanya dari kiri ke kanan, lalu dari atas ke bawah. Namun, ada beberapa jenis tata letak aplikasi yang membuat algoritma tidak dapat menentukan urutan pembacaan yang sebenarnya tanpa petunjuk tambahan. Di aplikasi berbasis tampilan, Anda dapat memperbaiki masalah tersebut menggunakan properti traversalBefore dan traversalAfter. Mulai Compose 1.5, Compose menyediakan API yang sama-sama fleksibel, tetapi dengan model konseptual baru.

isTraversalGroup dan traversalIndex adalah properti semantik yang memungkinkan Anda mengontrol aksesibilitas dan urutan fokus TalkBack dalam skenario jika algoritma pengurutan default tidak sesuai. isTraversalGroup mengidentifikasi grup yang penting secara semantik, sedangkan traversalIndex menyesuaikan urutan elemen individual dalam grup tersebut. Anda dapat menggunakan isTraversalGroup saja, atau dengan traversalIndex untuk penyesuaian lebih lanjut.

Gunakan isTraversalGroup dan traversalIndex di aplikasi Anda untuk mengontrol urutan traversal pembaca layar.

Mengelompokkan elemen dengan isTraversalGroup

isTraversalGroup adalah properti boolean yang menentukan apakah node semantik adalah grup traversal atau tidak. Jenis node ini adalah node yang fungsinya berfungsi sebagai batas atau batas dalam mengatur turunan node.

Menetapkan isTraversalGroup = true pada node berarti semua turunan node tersebut dikunjungi sebelum beralih ke elemen lain. Anda dapat menetapkan isTraversalGroup pada node yang dapat difokuskan pembaca non-layar, seperti Kolom, Baris, atau Kotak.

Contoh berikut menggunakan isTraversalGroup. Jenis ini memunculkan empat elemen teks. Dua elemen kiri milik satu elemen CardBox, sedangkan dua elemen kanan menjadi milik elemen CardBox lainnya:

// 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
        )
    }
}

Kode menghasilkan output yang mirip dengan berikut ini:

Tata letak dengan dua kolom teks, dengan kolom kiri bertuliskan 'Kalimat ini
  berada di kolom kiri' dan kolom kanan bertuliskan 'Kalimat ini di sebelah kanan'.
Gambar 1. Tata letak dengan dua kalimat (satu di kolom kiri dan satu di kolom kanan).

Karena tidak ada semantik yang ditetapkan, perilaku default pembaca layar adalah melintasi elemen dari kiri ke kanan dan dari atas ke bawah. Karena default ini, TalkBack membacakan fragmen kalimat dalam urutan yang salah:

"Kalimat ini ada di" → "Kalimat ini adalah" → "kolom kiri." → "di sebelah kanan."

Untuk mengurutkan fragmen dengan benar, ubah cuplikan asli untuk menetapkan isTraversalGroup ke 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 }
        )
    }
}

Karena isTraversalGroup ditetapkan secara khusus pada setiap CardBox, batas CardBox diterapkan saat mengurutkan elemennya. Dalam hal ini, CardBox kiri akan dibaca terlebih dahulu, diikuti oleh CardBox yang tepat.

Sekarang, TalkBack akan membacakan fragmen kalimat dalam urutan yang benar:

"Kalimat ini ada di" → "kolom kiri." → "Kalimat ini adalah" → "di sebelah kanan."

Menyesuaikan urutan traversal lebih lanjut

traversalIndex adalah properti float yang memungkinkan Anda menyesuaikan urutan traversal TalkBack. Jika mengelompokkan elemen tidak cukup bagi TalkBack untuk berfungsi dengan benar, gunakan traversalIndex bersama dengan isTraversalGroup untuk menyesuaikan urutan pembaca layar lebih lanjut.

Properti traversalIndex memiliki karakteristik berikut:

  • Elemen dengan nilai traversalIndex yang lebih rendah akan diprioritaskan terlebih dahulu.
  • Bisa positif atau negatif.
  • Nilai default-nya adalah 0f.
  • Hanya memengaruhi node yang dapat difokuskan pembaca layar, seperti elemen di layar seperti teks atau tombol. Misalnya, menetapkan hanya traversalIndex pada kolom tidak akan berpengaruh, kecuali jika kolom memiliki isTraversalGroup yang ditetapkan di kolom tersebut.

Contoh berikut menunjukkan cara menggunakan traversalIndex dan isTraversalGroup secara bersamaan.

Contoh: Jelajahi tampilan jam

Tampilan jam adalah skenario umum saat pengurutan traversal standar tidak berfungsi. Contoh di bagian ini adalah pemilih waktu, yang memungkinkan pengguna menjelajahi angka-angka pada tampilan jam dan memilih digit untuk slot jam dan menit.

Tampilan jam dengan pemilih waktu di atasnya.
Gambar 2. Gambar tampilan jam.

Dalam cuplikan yang disederhanakan berikut, ada CircularLayout yang menampilkan 12 angka, dimulai dengan 12 dan bergerak searah jarum jam di sekeliling lingkaran:

@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())
    }
}

Karena tampilan jam tidak dibaca secara logis dengan urutan default kiri ke kanan dan atas ke bawah, TalkBack membaca angka secara tidak berurutan. Untuk memperbaikinya, gunakan nilai penghitung yang bertambah, seperti yang ditunjukkan dalam cuplikan berikut:

@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())
    }
}

Untuk menetapkan pengurutan traversal dengan benar, pertama-tama buat CircularLayout sebagai grup traversal dan tetapkan isTraversalGroup = true. Kemudian, saat setiap teks jam digambar ke tata letak, setel traversalIndex yang sesuai ke nilai penghitung.

Karena nilai penghitung terus meningkat, setiap traversalIndex nilai jam menjadi lebih besar saat angka ditambahkan ke layar—nilai jam 0 memiliki traversalIndex 0, dan nilai jam 1 memiliki traversalIndex 1. Dengan cara ini, urutan yang dibaca TalkBack akan ditetapkan. Sekarang, angka dalam CircularLayout dibaca dalam urutan yang diharapkan.

Karena traversalIndexes yang telah ditetapkan hanya bersifat relatif terhadap indeks lain dalam pengelompokan yang sama, urutan layar lainnya telah dipertahankan. Dengan kata lain, perubahan semantik yang ditampilkan dalam cuplikan kode sebelumnya hanya mengubah pengurutan dalam tampilan jam yang telah menetapkan isTraversalGroup = true.

Perhatikan bahwa, tanpa menetapkan semantik CircularLayout's ke isTraversalGroup = true, perubahan traversalIndex tetap berlaku. Namun, tanpa CircularLayout untuk mengikatnya, dua belas digit tampilan jam akan dibaca terakhir, setelah semua elemen lain di layar telah dikunjungi. Hal ini terjadi karena semua elemen lain memiliki traversalIndex default 0f, dan elemen teks jam dibaca setelah semua elemen 0f lainnya.

Contoh: Menyesuaikan urutan traversal untuk tombol tindakan mengambang

Dalam contoh ini, traversalIndex dan isTraversalGroup mengontrol urutan traversal tombol tindakan mengambang (FAB) Desain Material. Dasar dari contoh ini adalah tata letak berikut:

Tata letak dengan panel aplikasi atas, teks contoh, tombol tindakan mengambang, dan
  panel aplikasi bawah.
Gambar 3. Tata letak dengan panel aplikasi atas, teks contoh, tombol tindakan mengambang, dan panel aplikasi bawah.

Secara default, tata letak dalam contoh ini memiliki urutan TalkBack berikut:

Panel Aplikasi Atas → Contoh teks 0 sampai 6 → tombol tindakan mengambang (FAB) → Panel Aplikasi Bawah

Anda mungkin ingin pembaca layar berfokus terlebih dahulu pada FAB. Untuk menetapkan traversalIndex pada elemen Material seperti FAB, lakukan hal berikut:

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

Dalam cuplikan ini, membuat kotak dengan isTraversalGroup yang ditetapkan ke true dan menetapkan traversalIndex di kotak yang sama (-1f lebih rendah dari nilai default 0f) berarti kotak mengambang akan muncul sebelum semua elemen lainnya di layar.

Selanjutnya, Anda dapat menempatkan kotak mengambang dan elemen lainnya ke dalam scaffold yang mengimplementasikan tata letak Desain Material:

@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 berinteraksi dengan elemen dalam urutan berikut:

FAB → Panel Aplikasi Atas → Contoh teks 0 sampai 6 → Panel Aplikasi Bawah

Referensi tambahan