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>
(cherry picked from commit 6ec54234ef)

# Conflicts:
#	android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt
pull/554/head
kari-ts 1 year ago committed by Andrea Gottardo
parent e393dad29e
commit a4ba359ea9

@ -3,20 +3,28 @@
package com.tailscale.ipn.ui.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.unit.dp
import coil.annotation.ExperimentalCoilApi
@ -27,22 +35,56 @@ import com.tailscale.ipn.ui.model.IpnLocal
@OptIn(ExperimentalCoilApi::class)
@Composable
fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) {
var modifier = Modifier.size((size * .8f).dp)
action?.let {
modifier =
modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onClick = action)
}
Icon(
imageVector = Icons.Default.Person,
contentDescription = stringResource(R.string.settings_title),
modifier = modifier)
var isFocused = remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
profile?.UserProfile?.ProfilePicURL?.let { url ->
AsyncImage(model = url, modifier = Modifier.size((size * 1.2f).dp), contentDescription = null)
// Outer Box for the larger focusable and clickable area
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 = rememberRipple(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.statusBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
@ -187,28 +186,14 @@ fun MainView(
}
},
trailingContent = {
Box(
modifier =
Modifier.weight(1f)
.focusable()
.clickable { 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()
}
}
}
Box(modifier = Modifier.padding(8.dp), contentAlignment = Alignment.CenterEnd) {
when (user) {
null -> SettingsButton { navigation.onNavigateToSettings() }
else -> {
Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() }
}
}
}
})
when (state) {

Loading…
Cancel
Save