Supporting keyboard navigation with Compose
DroidKaigi 2024
Tiphaine
Handling navigation focus
in 2024
Slide 2
Slide 2 text
2
Tiphaine
● Android engineer
@ DeNA (Pococha)
● DroidKaigi volunteer staff
● Mobile Dev Japan co-organizer
Slide 3
Slide 3 text
3
Slides & Video: It's 2021, let's support accessibility (Japanese only)
Better late than never
✌🤠✌
Today is a sequel to this talk
Slide 4
Slide 4 text
4
01 Introduction to Focus
Common Problems
02
Focus in Compose
03
Slide 5
Slide 5 text
01
5
Introduction to Focus
Slide 6
Slide 6 text
6
Slide 7
Slide 7 text
7
Slide 8
Slide 8 text
8
Focus Navigation
Slide 9
Slide 9 text
9
● Alternative way to interact
with a smartphone without
using the touch screen
● Move through the UI one
element at a time
Focus Navigation
Focus
Elements
Ref: Google Play Store
Slide 10
Slide 10 text
Focus
Focus
10
● Current position in the UI
● Can only interact with this
element
● Element will have some visual
changes when focused
Ref: Google Play Store
Slide 11
Slide 11 text
Focus
11
● Accessibility Focus ≠ Navigation Focus
● Navigation Focus is sometimes
called “Keyboard Navigation”
● Both focuses are Focus Navigation variants
😵💫
Focus Types
Slide 12
Slide 12 text
12
Differences
between focuses
Slide 13
Slide 13 text
Don’t touch the screen
👋 👀
Don’t see the screen
13
Navigation Focus Accessibility Focus
Slide 14
Slide 14 text
Only actionable elements All elements
Navigation Focus Accessibility Focus
14
Slide 15
Slide 15 text
focusable attribute
importantForAccessibility
and focusable attributes
15
Only actionable elements All elements
Navigation Focus Accessibility Focus
Slide 16
Slide 16 text
Keyboard, Switch Access,
D-pad (arrow keys)…
Screen readers like TalkBack
16
focusable attribute
importantForAccessibility
and focusable attributes
Only actionable elements All elements
Navigation Focus Accessibility Focus
Slide 17
Slide 17 text
17
● Hardware keyboard
(tablets, Chromebooks)
● On-screen keyboard
(soft input keyboard)
Image: Sergi Kabrera
Devices for Nav Focus
Slide 18
Slide 18 text
18
● TV remote controller
● Smartwatches rotating bezel
● Game controller
● Switch Access
Image: Erik Mclean
Devices for Nav Focus
Slide 19
Slide 19 text
19
Changeable indicator Only one default indicator
Keyboard, Switch Access,
D-pad (arrow keys)…
Screen readers like TalkBack
focusable attribute
importantForAccessibility
and focusable attributes
Only actionable elements All elements
Navigation Focus Accessibility Focus
21
Due to the time restriction,
today’s focus will be Nav Focus
(🥶)
Slide 22
Slide 22 text
22
For more info on Accessibility Focus,
check out my 2021 talk
💁
Slides & Video: It's 2021, let's support accessibility (Japanese only)
Slide 23
Slide 23 text
23
Types of navigation
(relying on Nav Focus)
Slide 24
Slide 24 text
24
● Using a D-pad (directional pad)
or arrow keys
● Two-dimensional navigation
● Focus goes up, down, left, or right
Directional Navigation
Slide 25
Slide 25 text
25
● Using “tab” key or Switch Access
● One-dimensional navigation
(forward or backward)
● Focus follows the order in which
elements appear in the layout
Tab Navigation
1
2
3
01
27
Introduction to Focus
Common Problems
Focus in Compose
02
03
Introduction to Focus
Slide 28
Slide 28 text
02
28
Common Problems
Slide 29
Slide 29 text
1/3
29
Can all actions be done?
Slide 30
Slide 30 text
30
● Make actionable elements focusable
(if you don’t use OnClickListener)
// Also focusable with
// Accessibility Focus
android:focusable="true"
Handle click actions
Slide 31
Slide 31 text
myView.setOnLongClickListener {
// Do something
true
}
31
Can’t reach the view 🥲
Slide 32
Slide 32 text
myView.setOnLongClickListener {
// Do something
true
}
// Set this for any listener
// except setOnClickListener⚠
myView.isFocusable = true
32
Slide 33
Slide 33 text
33
How about
gesture-based actions?
🤔
Slide 34
Slide 34 text
34
Focusable, but nothing happens 😢
true
}
myView.isFocusable = true
view.performClick()
myView.setOnTouchListener { view, event ->
// Do something with the event
Slide 35
Slide 35 text
35
● Nav Focus can’t trigger OnTouchListener 😣
● Need a different way to detect input events
👉 Listen to KeyEvents instead
Handle gesture-based actions
Slide 36
Slide 36 text
myView.setOnKeyListener { view, keyCode, event ->
36
}
// Choose which one(s) you want to support
KeyEvent.KEYCODE_ENTER, ... -> {
// Do something
true
}
else -> false
when (keyCode) {
Ref: Android Developers
}
Slide 37
Slide 37 text
myView.setOnKeyListener { view, keyCode, event ->
}
when (keyCode) {
}
37
Don’t forget to parse duplicate events ⚠
when (keyCode) {
...
}
// The same event will come several times
if (event.action != KeyEvent.ACTION_UP) {
return@setOnKeyListener false
}
Slide 38
Slide 38 text
override fun onKeyUp(keyCode: Int, event: KeyEvent?)
: Boolean {
}
return when (keyCode) {
KeyEvent.KEYCODE_ENTER -> performClick()
else -> ...
}
38
Use this instead for custom Views 🚀
Slide 39
Slide 39 text
39
Is it easy to use
and navigate the app?
2/3
Slide 40
Slide 40 text
40
● Skip non-interactive elements
● Wait for an action to initiate changes
// Skip this View
android:focusable=“false”
Meet the user’s expectations
Slide 41
Slide 41 text
41
● Make sure the focus can always be removed
● Verify the Esc key allows to move away
● Double check your WebViews, scrolls
and drop-down lists
Prevent keyboard traps
Slide 42
Slide 42 text
42
Try to test on
different manufacturers
😇
Slide 43
Slide 43 text
43
● Keep the flow logical and consistent
// Tab navigation
android:nextFocusForward=“@id/...”
// Directional navigation
// Can also set up, down and left
android:nextFocusRight=“@id/...”
Maintain the expected flow
Slide 44
Slide 44 text
44
Facilitate navigation
Ref: Android Developers
// Available from API 26+
android:keyboardNavigationCluster
● Group elements with clusters
1 2 3 4
10 11 12
Main
content
Bottom nav bar
Top tabs
Slide 45
Slide 45 text
45
● Request the focus when the start is obvious
// Available from API 26+
// Won’t show on-screen keyboard
Facilitate navigation
Slide 46
Slide 46 text
46
Ref: Alex Zlatkus
Start here
instead
Slide 47
Slide 47 text
47
Is the focus always visible?
3/3
Slide 48
Slide 48 text
48
● Ensure the focus is visible
● Customize the indicator color
and/or shape when necessary
Don’t let the user get lost
Ref: Google Play Store app in 2021
Slide 49
Slide 49 text
Customize the indicator: whole app
49
...
<item name="colorControlHighlight">...</item>
Customize the indicator
Slide 50
Slide 50 text
50
...
Customize the indicator: single View ᶃ
Customize the indicator: single View
Customize the indicator
Slide 51
Slide 51 text
51
Customize the indicator: single View ᶄ
Customize the indicator: single View
Customize the indicator
Slide 52
Slide 52 text
// Set click effect as state_pressed in selector
52
Customize the indicator: single View ᶅ
Customize the indicator: single View
Customize the indicator
Slide 53
Slide 53 text
53
I will talk about on-screen keyboard
another time…
🥲
Slide 54
Slide 54 text
01
54
Introduction to Focus
Common Problems
Focus in Compose
02
03
Introduction to Focus
Common Problems
Slide 55
Slide 55 text
03
55
Focus in Compose
Slide 56
Slide 56 text
56
Compose specificities
Slide 57
Slide 57 text
57
● Focus follows the declaration
order of Composables
● Priorities goes to elements
inside the same level
Tab Navigation flow
// Level ᶃ
// Level ᶄ
// Level ᶄ
Column {
Row {
Row {
}
}
}
...
...
Modifier.focusable()
62
● Need to provide a focus indicator and
handle the displaying logic by yourself 😭
Make the element focusable
Slide 63
Slide 63 text
63
Modifier
var color by remember {
.focusable()
mutableStateOf(transparent)
}
// Modifier of your Composable
Must set focusable as the last Modifier ⚠
Slide 64
Slide 64 text
64
Modifier
.border(5.dp, indicatorColor)
.onFocusChanged { focusState ->
}
color = if (focusState.isFocused) {
black
} else {
transparent
}
var color by remember { ...}
.focusable()
🎉
Slide 65
Slide 65 text
65
Modifier.clickable {
// Do something
}
● With this Modifier, the Composable will
automatically become focusable
Handle click actions
Slide 66
Slide 66 text
66
👀
How about other actionsʁ
Slide 67
Slide 67 text
67
Handle other actions
👉 Listen to KeyEvents instead
(Again)
● Can’t trigger anything (including long click) 😢
Slide 68
Slide 68 text
68
Listen to KeyEvents
Modifier.onPreviewKeyEvent {}
Modifier.onKeyEvent {}
// Parent callback is invoked first
// Start from children callback
70
// Parse duplicates
if (keyEvent.type != KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
when (keyEvent.key) {
}
when (keyEvent.key) {
...
}
Modifier.onKeyEvent { keyEvent ->
}
Modifier.onKeyEvent
Slide 71
Slide 71 text
71
Controls & Navigation
(in Compose)
Slide 72
Slide 72 text
● Can still be clicked in TouchMode 👋
72
Modifier.focusProperties {
Skip an element
canFocus = false
}
Slide 73
Slide 73 text
73
Change the flow - Tab Navigation
Modifier.focusProperties {
}
next =
}
previous = ...
...
● Use FocusProperties with FocusRequester
Change the flow
Slide 74
Slide 74 text
74
val (first, second) = remember {
FocusRequester.createRefs()
}
Slide 75
Slide 75 text
Modifier
75
val (first, second) = remember {
FocusRequester.createRefs()
}
first
// First Composable
// Second Composable (Destination)
Modifier.focusRequester(
)
)
second
.focusRequester(
Slide 76
Slide 76 text
76
val (first, second) = remember {
FocusRequester.createRefs()
}
Modifier
.focusProperties {
}
second
next =
first)
.focusRequester(
Slide 77
Slide 77 text
77
// Won’t be used
The top most Modifier wins ⚠
Modifier
.focusProperties { }
.focusProperties { previous = second }
next = second
Slide 78
Slide 78 text
78
Modifier.focusProperties {
left = ...
right = ...
up = ...
down = ...
}
Change the flow - Directional Nav
Change the flow
Slide 79
Slide 79 text
79
val focusManager =
LocalFocusManager.current
Move the focus
focusManager.moveFocus(
)
FocusDirection.Down
// Go to the nearest focusable Composable
// Direction you want the focus to go
Slide 80
Slide 80 text
80
● Go wherever you want (as long as it’s focusable)
val requester = remember {
FocusRequester()
}
requester.requestFocus()
Request the focus
Slide 81
Slide 81 text
Button(
onClick = {
) { ... }
TextField(...
// If clicked, move to the TextField
81
)
}
Slide 82
Slide 82 text
82
Button(
onClick = {
) { ... }
TextField(...
}
)
val requester = remember { FocusRequester() }
Modifier.focusRequester(requester)
// If clicked, move to the TextField
Slide 83
Slide 83 text
83
Button(
onClick = {
) { ... }
TextField(...
}
)
val requester = remember { FocusRequester() }
Modifier.focusRequester(requester)
requester.requestFocus()
// If clicked, move to the TextField
Slide 84
Slide 84 text
84
Try it out!
Slide 85
Slide 85 text
85
ᶃ Plug it to your phone
ᶄ Tap any key
ᶅ The focus indicator will appear
(Same steps with other external tools 🎮)
With a hardware keyboard
Slide 86
Slide 86 text
86
1⃣ Use an emulator
OR
2⃣ Use device mirroring
With your computer keyboard
Slide 87
Slide 87 text
87
● D-pad navigation is enabled by default with
pre-set hardware profiles ✅
● For custom profiles, select the option when
creating the device
1⃣ Use an emulator
Slide 88
Slide 88 text
88
Slide 89
Slide 89 text
89
Slide 90
Slide 90 text
90
Slide 91
Slide 91 text
91
Control your phone
with your computer keyboard 🥳
Slide 92
Slide 92 text
92
● Use a real device through AndroidStudio
● Preferences → Tools → Device mirroring
2⃣ Use device mirroring