So I'm trying to improve the keyboard focus indication for Jetpack Compose components.
I have created a custom class to draw border:
private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) :
Modifier.Node(), DrawModifierNode {
private var isFocused = false
override fun onAttach() {
coroutineScope.launch {
var focusCount = 0
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> focusCount++
is FocusInteraction.Unfocus -> focusCount--
}
val focused = focusCount > 0
if (isFocused != focused) {
isFocused = focused
invalidateDraw()
}
}
}
}
override fun ContentDrawScope.draw() {
drawContent()
if (isFocused) {
drawRoundRect(
color = themedIndicationColor,
size = size,
cornerRadius = CornerRadius(12.dp.toPx(), 12.dp.toPx()),
style = Stroke(width = 2.dp.toPx()),
alpha = 1.0f
)
}
}
}
object MyHighlightIndication : IndicationNodeFactory {
override fun create(interactionSource: InteractionSource): DelegatableNode {
return MyHighlightIndicationNode(interactionSource)
}
override fun hashCode(): Int = -1
override fun equals(other: Any?) = other === this
}
It's from this link
Now, if a Modifier has a toggleable/clickable/selectable interaction, I saw that the border gets removed after the action.
Here is client code:
@Composable
private fun GoodExample7(
snackbarLauncher: SnackbarLauncher?
) {
val cardClickMessage = stringResource(id = R.string.custom_focus_indicators_example_7_message)
val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
var isCheck by remember { mutableStateOf(false) }
OutlinedCard(
onClick = {
snackbarLauncher?.show(cardClickMessage)
},
modifier = Modifier
.padding(top = 8.dp)
.indication(
interactionSource = interactionSource,
indication = VisibleFocusIndication(
themedIndicationColor = MaterialTheme.colorScheme.primary
)
).toggleable(
value = isCheck,
onValueChange = { isCheck = it },
role = Role.Checkbox,
interactionSource = interactionSource,
indication = ripple()
)
,
// Key technique: Provide a common interactionSource to both clickable Card and indication.
interactionSource = interactionSource
) {
Row (
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
BodyText(
textId = R.string.custom_focus_indicators_example_7_card_description,
modifier = Modifier
)
Checkbox(checked = isCheck, onCheckedChange = null)
}
}
}
Before Tap:
After Click enter:
Expected: After the click event, it should keep showing the Border indication. An additional issue is if there is some other update, say, a progress bar update, in those cases, it also loses the focus from the element.
I've used some boilerplate code. Any idea how this can be solved?
So I'm trying to improve the keyboard focus indication for Jetpack Compose components.
I have created a custom class to draw border:
private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) :
Modifier.Node(), DrawModifierNode {
private var isFocused = false
override fun onAttach() {
coroutineScope.launch {
var focusCount = 0
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> focusCount++
is FocusInteraction.Unfocus -> focusCount--
}
val focused = focusCount > 0
if (isFocused != focused) {
isFocused = focused
invalidateDraw()
}
}
}
}
override fun ContentDrawScope.draw() {
drawContent()
if (isFocused) {
drawRoundRect(
color = themedIndicationColor,
size = size,
cornerRadius = CornerRadius(12.dp.toPx(), 12.dp.toPx()),
style = Stroke(width = 2.dp.toPx()),
alpha = 1.0f
)
}
}
}
object MyHighlightIndication : IndicationNodeFactory {
override fun create(interactionSource: InteractionSource): DelegatableNode {
return MyHighlightIndicationNode(interactionSource)
}
override fun hashCode(): Int = -1
override fun equals(other: Any?) = other === this
}
It's from this link
Now, if a Modifier has a toggleable/clickable/selectable interaction, I saw that the border gets removed after the action.
Here is client code:
@Composable
private fun GoodExample7(
snackbarLauncher: SnackbarLauncher?
) {
val cardClickMessage = stringResource(id = R.string.custom_focus_indicators_example_7_message)
val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
var isCheck by remember { mutableStateOf(false) }
OutlinedCard(
onClick = {
snackbarLauncher?.show(cardClickMessage)
},
modifier = Modifier
.padding(top = 8.dp)
.indication(
interactionSource = interactionSource,
indication = VisibleFocusIndication(
themedIndicationColor = MaterialTheme.colorScheme.primary
)
).toggleable(
value = isCheck,
onValueChange = { isCheck = it },
role = Role.Checkbox,
interactionSource = interactionSource,
indication = ripple()
)
,
// Key technique: Provide a common interactionSource to both clickable Card and indication.
interactionSource = interactionSource
) {
Row (
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
BodyText(
textId = R.string.custom_focus_indicators_example_7_card_description,
modifier = Modifier
)
Checkbox(checked = isCheck, onCheckedChange = null)
}
}
}
Before Tap:
After Click enter:
Expected: After the click event, it should keep showing the Border indication. An additional issue is if there is some other update, say, a progress bar update, in those cases, it also loses the focus from the element.
I've used some boilerplate code. Any idea how this can be solved?
Share Improve this question edited Feb 6 at 12:12 Amit Kundu asked Feb 6 at 11:56 Amit KunduAmit Kundu 1196 bronze badges1 Answer
Reset to default 0I have found the issue. The issue was I was creating an instance of the Focusable class. Without remember
scope. So the class was rerendering resulting is loosing focus.