Mengubah perilaku fokus

Terkadang, Anda perlu mengganti perilaku fokus default elemen di layar. Misalnya, Anda mungkin ingin mengelompokkan composable, mencegah fokus pada composable tertentu, secara eksplisit meminta fokus pada composable, mengambil atau melepaskan fokus, atau mengalihkan fokus ke composable masuk atau keluar. Bagian ini menjelaskan cara mengubah perilaku fokus jika default tidak yang Anda butuhkan.

Menyediakan navigasi yang koheren dengan grup fokus

Terkadang, Jetpack Compose tidak langsung menebak item berikutnya yang benar untuk navigasi tab, terutama saat Composables induk yang kompleks seperti tab dan daftar berperan.

Meskipun penelusuran fokus biasanya mengikuti urutan deklarasi Composables, hal ini tidak mungkin dilakukan dalam beberapa kasus, seperti saat salah satu Composables dalam hierarki adalah dapat di-scroll horizontal yang tidak sepenuhnya terlihat. Hal ini ditunjukkan dalam contoh di bawah ini.

Jetpack Compose dapat memutuskan untuk memfokuskan item berikutnya yang paling dekat dengan awal layar, seperti yang ditunjukkan di bawah, alih-alih melanjutkan jalur yang Anda harapkan untuk navigasi satu arah:

Animasi aplikasi yang menampilkan navigasi horizontal atas dan daftar item di bawah.
Gambar 1. Animasi aplikasi yang menampilkan navigasi horizontal atas dan daftar item di bawah

Dalam contoh ini, jelas bahwa developer tidak bermaksud untuk melompat dari tab Cokelat ke gambar pertama di bawah, lalu kembali ke tab Pastries. Sebaliknya, mereka ingin fokus berlanjut pada tab hingga tab terakhir, lalu berfokus pada konten dalam:

Animasi aplikasi yang menampilkan navigasi horizontal atas dan daftar item di bawah.
Gambar 2. Animasi aplikasi yang menampilkan navigasi horizontal atas dan daftar item di bawah

Dalam situasi ketika sekelompok composable harus mendapatkan fokus secara berurutan, seperti di baris Tab dari contoh sebelumnya, Anda perlu menggabungkan Composable dalam induk yang memiliki pengubah focusGroup():

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

Navigasi dua arah mencari composable terdekat untuk arah yang diberikan— jika elemen dari grup lain lebih dekat daripada item yang tidak terlihat sepenuhnya dalam grup saat ini, navigasi akan memilih elemen terdekat. Untuk menghindari perilaku ini, Anda dapat menerapkan pengubah focusGroup().

FocusGroup membuat seluruh grup muncul seperti entitas tunggal dalam hal fokus, tetapi grup itu sendiri tidak akan mendapatkan fokus— sebagai gantinya, turunan terdekat akan mendapatkan fokus. Dengan cara ini, navigasi tahu cara menuju ke item yang tidak terlihat sepenuhnya sebelum keluar dari grup.

Dalam hal ini, tiga instance FilterChip akan difokuskan sebelum item SweetsCard, bahkan saat SweetsCards sepenuhnya terlihat oleh pengguna dan beberapa FilterChip mungkin tersembunyi. Hal ini terjadi karena pengubah focusGroup memberi tahu pengelola fokus untuk menyesuaikan urutan item difokus sehingga navigasi lebih mudah dan lebih koheren dengan UI.

Tanpa pengubah focusGroup, jika FilterChipC tidak terlihat, navigasi fokus akan mengambilnya terakhir kali. Namun, menambahkan pengubah semacam itu tidak hanya dapat ditemukan, tetapi juga akan memperoleh fokus tepat setelah FilterChipB, seperti yang diharapkan pengguna.

Membuat composable dapat difokuskan

Beberapa composable dapat difokuskan secara desain, seperti Tombol atau composable dengan pengubah clickable yang terpasang padanya. Jika Anda ingin secara khusus menambahkan perilaku yang dapat difokuskan ke composable, gunakan pengubah focusable:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

Membuat composable tidak dapat difokuskan

Mungkin ada situasi saat beberapa elemen Anda tidak boleh berpartisipasi dalam fokus. Dalam kasus yang jarang terjadi ini, Anda dapat memanfaatkan canFocus property untuk mengecualikan Composable agar tidak dapat difokuskan.

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

Minta fokus keyboard dengan FocusRequester

Dalam beberapa kasus, Anda mungkin ingin secara eksplisit meminta fokus sebagai respons terhadap interaksi pengguna. Misalnya, Anda dapat bertanya kepada pengguna apakah mereka ingin memulai ulang pengisian formulir, dan jika mereka menekan "ya", Anda ingin memfokuskan ulang kolom pertama formulir tersebut.

Hal pertama yang harus dilakukan adalah mengaitkan objek FocusRequester dengan composable yang ingin Anda pindahkan fokus keyboard. Dalam cuplikan kode berikut, objek FocusRequester dikaitkan dengan TextField dengan menyetel pengubah yang disebut Modifier.focusRequester:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Anda dapat memanggil metode requestFocus FocusRequester untuk mengirim permintaan fokus yang sebenarnya. Anda harus memanggil metode ini di luar konteks Composable (jika tidak, metode ini akan dieksekusi ulang pada setiap rekomposisi). Cuplikan berikut menunjukkan cara meminta sistem untuk memindahkan fokus keyboard saat tombol diklik:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

Mengambil dan melepaskan fokus

Anda dapat memanfaatkan fokus untuk memandu pengguna memberikan data yang tepat yang diperlukan aplikasi Anda untuk menjalankan tugasnya—misalnya, mendapatkan alamat email atau nomor telepon yang valid. Meskipun status error memberi tahu pengguna tentang apa yang terjadi, Anda mungkin memerlukan kolom dengan informasi yang salah agar tetap fokus hingga diperbaiki.

Untuk mengambil fokus, Anda dapat memanggil metode captureFocus(), dan melepasnya setelahnya dengan metode freeFocus(), seperti pada contoh berikut:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

Prioritas pengubah fokus

Modifiers dapat dilihat sebagai elemen yang hanya memiliki satu turunan, jadi saat Anda mengantrekannya, setiap Modifier di sebelah kiri (atau atas) menggabungkan Modifier yang mengikuti di sebelah kanan (atau di bawahnya). Ini berarti Modifier kedua dimuat di dalam yang pertama, sehingga saat mendeklarasikan dua focusProperties, hanya yang paling teratas yang berfungsi, karena yang berikut berada di bagian paling atas.

Untuk memperjelas konsepnya lebih lanjut, lihat kode berikut:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

Dalam hal ini, focusProperties yang menunjukkan item2 sebagai fokus yang tepat tidak akan digunakan, seperti yang terdapat dalam fokus sebelumnya; sehingga item1 akan menjadi yang digunakan.

Dengan memanfaatkan pendekatan ini, induk juga dapat mereset perilaku ke default menggunakan FocusRequester.Default:

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

Induk tidak harus menjadi bagian dari rantai pengubah yang sama. Composable induk dapat menimpa properti fokus composable turunan. Misalnya, perhatikan FancyButton ini yang membuat tombol tidak dapat difokuskan:

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

Pengguna dapat membuat tombol ini dapat difokuskan lagi dengan menyetel canFocus ke true:

FancyButton(Modifier.focusProperties { canFocus = true })

Seperti setiap Modifier, yang terkait fokus memiliki perilaku yang berbeda berdasarkan urutan Anda mendeklarasikannya. Misalnya, kode seperti berikut membuat Box dapat difokuskan, tetapi FocusRequester tidak terkait dengan dapat difokuskan karena dideklarasikan setelah focusable.

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

Penting untuk diingat bahwa focusRequester dikaitkan dengan elemen pertama yang dapat difokuskan di bawahnya dalam hierarki, sehingga focusRequester ini mengarah ke turunan pertama yang dapat difokuskan. Jika tidak ada yang tersedia, ikon tidak akan menunjuk ke apa pun. Namun, karena Box dapat difokuskan (berkat pengubah focusable()), Anda dapat menavigasi ke dalamnya menggunakan navigasi dua arah.

Sebagai contoh lainnya, salah satu dari hal berikut akan berfungsi, karena pengubah onFocusChanged() merujuk pada elemen pertama yang dapat difokuskan yang muncul setelah pengubah focusable() atau focusTarget().

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

Alihkan fokus saat masuk atau keluar

Terkadang, Anda perlu menyediakan jenis navigasi yang sangat spesifik, seperti yang ditunjukkan pada animasi di bawah ini:

Animasi layar yang menampilkan dua kolom tombol yang ditempatkan berdampingan dan menganimasikan fokus dari satu kolom ke kolom lainnya.
Gambar 3. Animasi layar yang menampilkan dua kolom tombol yang ditempatkan berdampingan dan menganimasikan fokus dari satu kolom ke kolom lainnya

Sebelum mempelajari cara membuatnya, penting untuk memahami perilaku default penelusuran fokus. Tanpa modifikasi apa pun, setelah penelusuran fokus mencapai item Clickable 3, menekan DOWN pada D-Pad (atau tombol panah yang setara) akan memindahkan fokus ke apa pun yang ditampilkan di bawah Column, keluar dari grup dan mengabaikan yang di sebelah kanan. Jika tidak ada item yang dapat difokuskan, fokus tidak akan berpindah ke mana pun, tetapi tetap pada Clickable 3.

Untuk mengubah perilaku ini dan menyediakan navigasi yang diinginkan, Anda dapat memanfaatkan pengubah focusProperties, yang membantu Anda mengelola hal yang terjadi saat penelusuran fokus memasuki atau keluar dari Composable:

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

Anda dapat mengarahkan fokus ke Composable tertentu setiap kali memasuki atau keluar dari bagian hierarki tertentu—misalnya, saat UI memiliki dua kolom dan Anda ingin memastikan bahwa setiap kali kolom pertama diproses, fokus beralih ke kolom kedua:

Animasi layar yang menampilkan dua kolom tombol yang ditempatkan berdampingan dan menganimasikan fokus dari satu kolom ke kolom lainnya.
Gambar 4. Animasi layar yang menampilkan dua kolom tombol yang ditempatkan berdampingan dan menganimasikan fokus dari satu kolom ke kolom lainnya

Dalam gif ini, setelah fokus mencapai Clickable 3 Composable di Column 1, item berikutnya yang difokuskan adalah Clickable 4 di Column lainnya. Perilaku ini dapat dicapai dengan menggabungkan focusDirection dengan nilai enter dan exit di dalam pengubah focusProperties. Keduanya memerlukan lambda yang menggunakan parameter, arah asal fokus, dan menampilkan FocusRequester. Lambda ini dapat berperilaku dengan tiga cara berbeda: menampilkan FocusRequester.Cancel akan menghentikan fokus agar tidak dilanjutkan, sedangkan FocusRequester.Default tidak mengubah perilakunya. Sebagai gantinya, memberikan FocusRequester yang dilampirkan ke Composable lain akan membuat fokus melompat ke Composable khusus tersebut.

Mengubah arah memajukan fokus

Untuk memajukan fokus ke item berikutnya atau ke arah yang tepat, Anda dapat memanfaatkan pengubah onPreviewKey dan menyiratkan LocalFocusManager untuk memajukan fokus dengan Pengubah moveFocus.

Contoh berikut menunjukkan perilaku default mekanisme fokus: saat penekanan tombol tab terdeteksi, fokus akan maju ke elemen berikutnya dalam daftar fokus. Meskipun ini bukan sesuatu yang biasanya perlu Anda konfigurasi, penting untuk mengetahui cara kerja bagian dalam sistem agar dapat mengubah perilaku default.

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

Dalam contoh ini, fungsi focusManager.moveFocus() memajukan fokus ke item yang ditentukan, atau ke arah yang tersirat dalam parameter fungsi.