android: fix avatar focusable

-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>
pull/538/head
kari-ts 1 year ago
parent d309f31b5a
commit 65b9e846ab

@ -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.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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 = // Outer Box for the larger focusable and clickable area
modifier.clickable( 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() }, interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false), indication = rememberRipple(bounded = true), // Apply ripple effect inside circular bounds
onClick = action) 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( Icon(
imageVector = Icons.Default.Person, imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title), contentDescription = stringResource(R.string.settings_title),
modifier = modifier) modifier = Modifier
.size((size * 0.8f).dp)
profile?.UserProfile?.ProfilePicURL?.let { url -> .clip(CircleShape) // Icon size slightly smaller than the Box
AsyncImage(model = url, modifier = Modifier.size((size * 1.2f).dp), contentDescription = null) )
}
} }
} }
} }

@ -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,25 +186,11 @@ fun MainView(
} }
}, },
trailingContent = { trailingContent = {
Box( Box(modifier = Modifier.padding(8.dp), contentAlignment = Alignment.CenterEnd) {
modifier =
Modifier.weight(1f)
.focusable()
.clickable { navigation.onNavigateToSettings() }
.padding(8.dp),
contentAlignment = Alignment.CenterEnd) {
when (user) { when (user) {
null -> SettingsButton { navigation.onNavigateToSettings() } null -> SettingsButton { navigation.onNavigateToSettings() }
else -> else -> {
Box( Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() }
contentAlignment = Alignment.Center,
modifier =
Modifier.size(42.dp).clip(CircleShape).focusable().clickable {
navigation.onNavigateToSettings()
}) {
Avatar(profile = user, size = 36) {
navigation.onNavigateToSettings()
}
} }
} }
} }

Loading…
Cancel
Save