Langkah utama untuk meningkatkan aksesibilitas Compose

Untuk membantu orang dengan kebutuhan aksesibilitas agar berhasil menggunakan aplikasi Anda, desain aplikasi Anda agar mendukung persyaratan aksesibilitas utama.

Mempertimbangkan ukuran target sentuh minimum

Elemen apa pun di layar yang dapat diklik, disentuh, atau berinteraksi dengan seseorang harus cukup besar untuk interaksi yang dapat diandalkan. Saat mengubah ukuran elemen ini, pastikan untuk menyetel ukuran minimum ke 48 dp agar dapat mengikuti panduan aksesibilitas Desain Material dengan benar.

Komponen Material—seperti Checkbox, RadioButton, Switch, Slider, dan Surface—menetapkan ukuran minimum ini secara internal, tetapi hanya saat komponen dapat menerima tindakan pengguna. Misalnya, saat Checkbox memiliki parameter onCheckedChange yang disetel ke nilai non-null, kotak centang akan menyertakan padding agar lebar dan tingginya minimal 48 dp.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

Jika parameter onCheckedChange ditetapkan ke null, padding tidak disertakan, karena komponen tidak dapat berinteraksi secara langsung.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Gambar 1. Kotak centang tanpa padding.

Saat menerapkan kontrol pemilihan seperti Switch, RadioButton, atau Checkbox, Anda biasanya memindahkan perilaku yang dapat diklik ke penampung induk, menyetel callback klik pada composable ke null, dan menambahkan pengubah toggleable atau selectable ke composable induk.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

Jika ukuran composable yang dapat diklik lebih kecil dari ukuran target sentuh minimum, Compose masih akan meningkatkan ukuran target sentuh. Hal ini dilakukan dengan memperluas ukuran target sentuh ke luar batas composable.

Contoh berikut berisi Box yang dapat diklik dengan ukuran sangat kecil. Area target sentuh otomatis diperluas di luar batas Box, sehingga mengetuk di samping Box tetap akan memicu peristiwa klik.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

Untuk mencegah kemungkinan tumpang-tindih di antara area sentuh composable yang berbeda, selalu gunakan ukuran minimum yang cukup besar untuk composable. Dalam contohnya, hal itu berarti menggunakan pengubah sizeIn untuk menyetel ukuran minimum kotak bagian dalam:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

Menambahkan label klik

Anda dapat menggunakan label klik untuk menambahkan makna semantik ke perilaku klik composable. Label klik menjelaskan apa yang terjadi saat pengguna berinteraksi dengan composable. Layanan aksesibilitas menggunakan label klik untuk membantu mendeskripsikan aplikasi kepada pengguna dengan kebutuhan tertentu.

Tetapkan label klik dengan meneruskan parameter dalam pengubah clickable:

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

Atau, jika Anda tidak memiliki akses ke pengubah yang dapat diklik, setel label klik di pengubah semantik:

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

Menjelaskan elemen visual

Saat Anda menentukan composable Image atau Icon, tidak ada cara otomatis bagi framework Android untuk memahami apa saja yang ditampilkan aplikasi. Anda harus meneruskan deskripsi tekstual dari elemen visual.

Bayangkan layar tempat pengguna dapat berbagi halaman saat ini dengan teman. Layar ini berisi ikon berbagi yang dapat diklik:

Strip ikon yang dapat diklik, dengan

Berdasarkan ikon itu saja, framework Android tidak dapat mendeskripsikannya kepada pengguna yang menderita gangguan penglihatan. Framework Android memerlukan deskripsi tekstual tambahan untuk ikon.

Parameter contentDescription menjelaskan elemen visual. Gunakan string yang dilokalkan, seperti yang terlihat oleh pengguna.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Beberapa elemen visual hanya bersifat dekoratif dan Anda mungkin tidak ingin menyampaikannya kepada pengguna. Saat menetapkan parameter contentDescription ke null, Anda menunjukkan ke framework Android bahwa elemen ini tidak memiliki tindakan atau status terkait.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

Andalah yang memutuskan apakah elemen visual tertentu memerlukan contentDescription. Tanyakan pada diri Anda apakah elemen itu menyampaikan informasi yang diperlukan pengguna untuk melakukan tugas mereka. Jika tidak, sebaiknya biarkan deskripsinya.

Menggabungkan elemen

Layanan aksesibilitas seperti Talkback dan Tombol Akses memungkinkan pengguna memindahkan fokus ke seluruh elemen di layar. Elemen-elemen harus difokuskan pada tingkat perincian yang tepat. Saat setiap composable level rendah di layar difokuskan secara independen, pengguna harus banyak berinteraksi untuk berpindah di layar. Jika elemen digabungkan terlalu agresif, pengguna mungkin tidak memahami elemen mana yang saling terkait

Saat Anda menerapkan pengubah clickable ke composable, Compose akan otomatis menggabungkan semua elemen yang dimuat composable. Hal ini juga berlaku untuk ListItem; elemen dalam item daftar digabungkan, dan layanan aksesibilitas melihatnya sebagai satu elemen.

Anda dapat memiliki sekumpulan composable yang membentuk grup logis, tetapi grup tersebut tidak dapat diklik atau merupakan bagian dari item daftar. Anda tetap ingin layanan aksesibilitas melihatnya sebagai satu elemen. Misalnya, bayangkan composable yang menampilkan avatar pengguna, nama mereka, dan beberapa informasi tambahan:

Sekumpulan elemen UI termasuk nama pengguna. Nama dipilih.

Anda dapat mengaktifkan Compose untuk menggabungkan elemen ini menggunakan parameter mergeDescendants dalam pengubah semantics. Dengan cara ini, layanan aksesibilitas hanya memilih elemen gabungan, dan semua properti semantik turunan akan digabungkan.

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date} • ${metadata.readTimeMinutes} min read")
        }
    }
}

Layanan aksesibilitas kini berfokus pada seluruh container sekaligus, dengan menggabungkan kontennya:

Sekumpulan elemen UI termasuk nama pengguna. Semua elemen dipilih bersama.

Menambahkan tindakan kustom

Lihat item daftar berikut:

Item daftar umum, yang berisi judul artikel, penulis, dan ikon bookmark.

Saat Anda menggunakan pembaca layar seperti Talkback untuk mendengar konten yang ditampilkan di layar, pembaca layar akan memilih seluruh item terlebih dahulu, lalu ikon bookmark.

Item daftar, dengan semua elemen dipilih bersama-sama.

Item daftar, hanya dengan ikon bookmark yang dipilih

Dalam daftar panjang, hal ini dapat menjadi sangat berulang. Pendekatan yang lebih baik adalah menentukan tindakan kustom yang memungkinkan pengguna mem-bookmark item. Perlu diingat bahwa Anda juga harus secara eksplisit menghapus perilaku ikon bookmark untuk memastikan ikon tidak dipilih oleh layanan aksesibilitas. Hal ini dilakukan dengan pengubah clearAndSetSemantics:

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

Menjelaskan status elemen

Composable dapat menentukan stateDescription untuk semantik yang digunakan framework Android untuk membacakan status tempat composable berada. Misalnya, composable yang dapat diganti statusnya dapat berupa "dicentang" atau "tidak dicentang". Dalam beberapa kasus, Anda mungkin ingin mengganti label deskripsi status default yang digunakan Compose. Anda dapat melakukannya dengan menentukan label deskripsi status secara eksplisit sebelum menentukan composable sebagai dapat diganti statusnya:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Menentukan judul

Aplikasi terkadang menampilkan banyak konten pada satu layar dalam penampung yang dapat di-scroll. Misalnya, layar dapat menampilkan konten lengkap artikel yang sedang dibaca pengguna:

Screenshot postingan blog, dengan teks artikel dalam container yang dapat di-scroll.

Pengguna dengan kebutuhan aksesibilitas mengalami kesulitan untuk menavigasi layar tersebut. Untuk membantu navigasi, tunjukkan elemen mana yang merupakan judul. Pada contoh sebelumnya, setiap judul subbagian dapat didefinisikan sebagai judul untuk aksesibilitas. Beberapa layanan aksesibilitas, seperti Talkback, memungkinkan pengguna menavigasi langsung dari judul ke judul.

Di Compose, Anda menunjukkan bahwa composable adalah judul dengan menentukan properti semantics-nya:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Menangani composable kustom

Setiap kali mengganti komponen Material tertentu di aplikasi dengan versi kustom, Anda harus mempertimbangkan aksesibilitas.

Misalnya Anda mengganti Material Checkbox dengan penerapan Anda sendiri. Anda mungkin lupa untuk menambahkan pengubah triStateToggleable, yang menangani properti aksesibilitas untuk komponen ini.

Prinsipnya adalah melihat implementasi komponen dalam library Material dan meniru perilaku aksesibilitas yang dapat Anda temukan. Selain itu, gunakan banyak pengubah Foundation, berbeda dengan pengubah tingkat UI, karena hal ini mencakup pertimbangan aksesibilitas.

Uji implementasi komponen kustom Anda dengan beberapa layanan aksesibilitas untuk memverifikasi perilakunya.

Referensi tambahan