Skip to content

Commit bd745b5

Browse files
authored
android: fix avatar padding (#559)
Update Avatar to take isFocusable as a parameter, allowing us to make the avatar focusable in the main view but not in the settings / user switcher view. This fixes the issue where the padding is too big in the settings / user switcher view. Fixes tailscale/corp#24370 Signed-off-by: kari-ts <[email protected]>
1 parent ba306bf commit bd745b5

File tree

2 files changed

+54
-52
lines changed

2 files changed

+54
-52
lines changed

android/src/main/java/com/tailscale/ipn/ui/view/Avatar.kt

Lines changed: 45 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,57 +34,52 @@ import com.tailscale.ipn.ui.model.IpnLocal
3434

3535
@OptIn(ExperimentalCoilApi::class)
3636
@Composable
37-
fun Avatar(profile: IpnLocal.LoginProfile?, size: Int = 50, action: (() -> Unit)? = null) {
38-
var isFocused = remember { mutableStateOf(false) }
39-
val focusManager = LocalFocusManager.current
37+
fun Avatar(
38+
profile: IpnLocal.LoginProfile?,
39+
size: Int = 50,
40+
action: (() -> Unit)? = null,
41+
isFocusable: Boolean = false
42+
) {
43+
var isFocused = remember { mutableStateOf(false) }
44+
val focusManager = LocalFocusManager.current
4045

41-
// Outer Box for the larger focusable and clickable area
42-
Box(
43-
contentAlignment = Alignment.Center,
44-
modifier = Modifier
45-
.padding(4.dp)
46-
.size((size * 1.5f).dp) // Focusable area is larger than the avatar
47-
.clip(CircleShape) // Ensure both the focus and click area are circular
48-
.background(
49-
if (isFocused.value) MaterialTheme.colorScheme.surface
50-
else Color.Transparent,
51-
)
52-
.onFocusChanged { focusState ->
53-
isFocused.value = focusState.isFocused
54-
}
55-
.focusable() // Make this outer Box focusable (after onFocusChanged)
56-
.clickable(
57-
interactionSource = remember { MutableInteractionSource() },
58-
indication = ripple(bounded = true), // Apply ripple effect inside circular bounds
59-
onClick = {
60-
action?.invoke()
61-
focusManager.clearFocus() // Clear focus after clicking the avatar
62-
}
46+
// Determine the modifier based on whether the avatar is focusable
47+
val outerModifier =
48+
Modifier.then(
49+
if (isFocusable) {
50+
Modifier.padding(4.dp)
51+
} else Modifier) // Add padding if focusable
52+
.size((size * 1.5f).dp)
53+
.clip(CircleShape)
54+
.background(if (isFocused.value) MaterialTheme.colorScheme.surface else Color.Transparent)
55+
.onFocusChanged { focusState -> isFocused.value = focusState.isFocused }
56+
.then(if (isFocusable) Modifier.focusable() else Modifier) // Conditionally add focusable
57+
.clickable(
58+
interactionSource = remember { MutableInteractionSource() },
59+
indication = ripple(bounded = true),
60+
onClick = {
61+
action?.invoke()
62+
focusManager.clearFocus() // Clear focus after clicking
63+
})
64+
65+
// Outer Box for the larger focusable and clickable area
66+
Box(contentAlignment = Alignment.Center, modifier = outerModifier) {
67+
// Inner Box to hold the avatar content (Icon or AsyncImage)
68+
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(size.dp).clip(CircleShape)) {
69+
if (profile?.UserProfile?.ProfilePicURL != null) {
70+
AsyncImage(
71+
model = profile.UserProfile.ProfilePicURL,
72+
modifier = Modifier.size(size.dp).clip(CircleShape),
73+
contentDescription = null)
74+
} else {
75+
Icon(
76+
imageVector = Icons.Default.Person,
77+
contentDescription = stringResource(R.string.settings_title),
78+
modifier =
79+
Modifier.size((size * 0.8f).dp)
80+
.clip(CircleShape) // Icon size slightly smaller than the Box
6381
)
64-
) {
65-
// Inner Box to hold the avatar content (Icon or AsyncImage)
66-
Box(
67-
contentAlignment = Alignment.Center,
68-
modifier = Modifier
69-
.size(size.dp)
70-
.clip(CircleShape)
71-
) {
72-
if (profile?.UserProfile?.ProfilePicURL != null) {
73-
AsyncImage(
74-
model = profile.UserProfile.ProfilePicURL,
75-
modifier = Modifier.size(size.dp).clip(CircleShape),
76-
contentDescription = null
77-
)
78-
} else {
79-
Icon(
80-
imageVector = Icons.Default.Person,
81-
contentDescription = stringResource(R.string.settings_title),
82-
modifier = Modifier
83-
.size((size * 0.8f).dp)
84-
.clip(CircleShape) // Icon size slightly smaller than the Box
85-
)
86-
}
87-
}
82+
}
8883
}
84+
}
8985
}
90-

android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,15 @@ import androidx.compose.material3.Scaffold
4545
import androidx.compose.material3.SearchBar
4646
import androidx.compose.material3.SearchBarDefaults
4747
import androidx.compose.material3.Text
48-
import androidx.compose.runtime.*
48+
import androidx.compose.runtime.Composable
49+
import androidx.compose.runtime.LaunchedEffect
50+
import androidx.compose.runtime.collectAsState
51+
import androidx.compose.runtime.derivedStateOf
52+
import androidx.compose.runtime.getValue
53+
import androidx.compose.runtime.mutableStateOf
54+
import androidx.compose.runtime.remember
4955
import androidx.compose.runtime.saveable.rememberSaveable
56+
import androidx.compose.runtime.setValue
5057
import androidx.compose.ui.Alignment
5158
import androidx.compose.ui.Modifier
5259
import androidx.compose.ui.draw.alpha
@@ -188,7 +195,7 @@ fun MainView(
188195
when (user) {
189196
null -> SettingsButton { navigation.onNavigateToSettings() }
190197
else -> {
191-
Avatar(profile = user, size = 36) { navigation.onNavigateToSettings() }
198+
Avatar(profile = user, size = 36, { navigation.onNavigateToSettings() }, isFocusable=true)
192199
}
193200
}
194201
}

0 commit comments

Comments
 (0)