android: fix avatar focusable (#538)

-Apply focusable() directly to outer Box instead of nested with clickable()
-Explicitly handle focus
-Simplify focusable area

Fixes tailscale/corp/#23762

Signed-off-by: kari-ts <kari@tailscale.com>
Signed-off-by: kari-ts <135075563+kari-ts@users.noreply.github.com>
pull/544/head
kari-ts 1 year ago committed by GitHub
parent 18b8c78754
commit 6ec54234ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,20 +3,28 @@
package com.tailscale.ipn.ui.view package com.tailscale.ipn.ui.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal
@OptIn(ExperimentalCoilApi::class) @OptIn(ExperimentalCoilApi::class)
@Composable @Composable
fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) { fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) { var isFocused = remember { mutableStateOf(false) }
var modifier = Modifier.size((size * .8f).dp) val focusManager = LocalFocusManager.current
action?.let {
modifier =
modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = action)
}
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title),
modifier = modifier)
profile?.UserProfile?.ProfilePicURL?.let { url -> // Outer Box for the larger focusable and clickable area
AsyncImage(model = url, modifier = Modifier.size((size * 1.2f).dp), contentDescription = null) Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(4.dp)
.size((size * 1.5f).dp) // Focusable area is larger than the avatar
.clip(CircleShape) // Ensure both the focus and click area are circular
.background(
if (isFocused.value) MaterialTheme.colorScheme.surface
else Color.Transparent,
)
.onFocusChanged { focusState ->
isFocused.value = focusState.isFocused
}
.focusable() // Make this outer Box focusable (after onFocusChanged)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
onClick = {
action?.invoke()
focusManager.clearFocus() // Clear focus after clicking the avatar
}
)
) {
// Inner Box to hold the avatar content (Icon or AsyncImage)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(size.dp)
.clip(CircleShape)
) {
if (profile?.UserProfile?.ProfilePicURL != null) {
AsyncImage(
model = profile.UserProfile.ProfilePicURL,
modifier = Modifier.size(size.dp).clip(CircleShape),
contentDescription = null
)
} else {
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title),
modifier = Modifier
.size((size * 0.8f).dp)
.clip(CircleShape) // Icon size slightly smaller than the Box
)
}
}
} }
}
} }

@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown import androidx.compose.material.icons.outlined.ArrowDropDown
@ -187,28 +186,14 @@ fun MainView(
} }
}, },
trailingContent = { trailingContent = {
Box( Box(modifier = Modifier.padding(8.dp), contentAlignment = Alignment.CenterEnd) {
modifier = when (user) {
Modifier.weight(1f) null -> SettingsButton { navigation.onNavigateToSettings() }
.focusable() else -> {
.clickable { navigation.onNavigateToSettings() } Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() }
.padding(8.dp),
contentAlignment = Alignment.CenterEnd) {
when (user) {
null -> SettingsButton { navigation.onNavigateToSettings() }
else ->
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.size(42.dp).clip(CircleShape).focusable().clickable {
navigation.onNavigateToSettings()
}) {
Avatar(profile = user, size = 36) {
navigation.onNavigateToSettings()
}
}
}
} }
}
}
}) })
when (state) { when (state) {

Loading…
Cancel
Save