android/ui: restyle the search bar (#272)

Updates tailscale/corp#18202

restyles the search bar to look a little more iOS like.  switch to a normal text field which gives us much more flexibility over the look and all of the flexbiility of SearchBar.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
pull/274/head
Jonathan Nobels 2 months ago committed by GitHub
parent 6e503f29a9
commit e9465988dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -11,6 +11,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.Typography
@ -205,3 +207,21 @@ val ColorScheme.secondaryButton: ButtonColors
val ColorScheme.disabled: Color
get() = Color(0xFFAFACAB) // gray-400
@OptIn(ExperimentalMaterial3Api::class)
val ColorScheme.searchBarColors: TextFieldColors
@Composable
get() {
val defaults = OutlinedTextFieldDefaults.colors()
return OutlinedTextFieldDefaults.colors(
focusedLeadingIconColor = MaterialTheme.colorScheme.onSurface,
unfocusedLeadingIconColor = MaterialTheme.colorScheme.onSurface,
focusedTextColor = MaterialTheme.colorScheme.onSurface,
unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.background,
unfocusedContainerColor = MaterialTheme.colorScheme.background,
disabledContainerColor = MaterialTheme.colorScheme.background,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent)
}

@ -1,16 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.util
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun settingsRowModifier(): Modifier {
return Modifier.clip(shape = RoundedCornerShape(8.dp)).fillMaxWidth()
}

@ -24,6 +24,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowDropDown
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
@ -33,16 +35,20 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
@ -66,6 +72,7 @@ import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.theme.disabled
import com.tailscale.ipn.ui.theme.listItem
import com.tailscale.ipn.ui.theme.primaryListItem
import com.tailscale.ipn.ui.theme.searchBarColors
import com.tailscale.ipn.ui.theme.secondaryButton
import com.tailscale.ipn.ui.theme.surfaceContainerListItem
import com.tailscale.ipn.ui.util.Lists
@ -318,75 +325,94 @@ fun PeerList(
val searchTermStr by viewModel.searchTerm.collectAsState(initial = "")
val netmap = viewModel.netmap.collectAsState()
SearchBar(
query = searchTermStr,
onQueryChange = onSearch,
onSearch = onSearch,
active = true,
onActiveChange = {},
shape = RoundedCornerShape(10.dp),
leadingIcon = { Icon(Icons.Outlined.Search, null) },
trailingIcon = {
if (searchTermStr.isNotEmpty()) ClearButton({ onSearch("") }) else CloseButton()
},
tonalElevation = 0.dp,
shadowElevation = 0.dp,
colors =
SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface,
dividerColor = MaterialTheme.colorScheme.outline),
modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
var first = true
peerList.value.forEach { peerSet ->
if (!first) {
item(key = "spacer_${peerSet.user?.DisplayName}") {
Lists.ItemDivider()
Spacer(Modifier.height(24.dp))
}
}
first = false
stickyHeader {
ListItem(
modifier = Modifier.heightIn(max = 48.dp),
headlineContent = {
Text(
text =
peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold)
})
}
val focusManager = LocalFocusManager.current
var isFocussed by remember { mutableStateOf(false) }
itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer ->
ListItem(
modifier = Modifier.clickable { onNavigateToPeerDetails(peer) },
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier =
Modifier.padding(top = 2.dp)
.size(10.dp)
.background(
color = peer.connectedColor(netmap.value),
shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(8.dp))
Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium)
}
},
supportingContent = {
Text(
text = peer.Addresses?.first()?.split("/")?.first() ?: "",
style = MaterialTheme.typography.bodyMedium)
})
}
Box(modifier = Modifier.fillMaxWidth().background(color = MaterialTheme.colorScheme.surface)) {
OutlinedTextField(
modifier =
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).onFocusChanged {
isFocussed = it.isFocused
},
singleLine = true,
shape = MaterialTheme.shapes.large,
colors = MaterialTheme.colorScheme.searchBarColors,
leadingIcon = { Icon(imageVector = Icons.Outlined.Search, contentDescription = "search") },
trailingIcon = {
if (isFocussed) {
IconButton(
onClick = {
focusManager.clearFocus()
onSearch("")
}) {
Icon(
imageVector =
if (searchTermStr.isEmpty()) Icons.Outlined.Close
else Icons.Outlined.Clear,
contentDescription = "clear search",
tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
},
placeholder = {
Text(
text = stringResource(id = R.string.search),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1)
},
value = searchTermStr,
onValueChange = { onSearch(it) })
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
var first = true
peerList.value.forEach { peerSet ->
if (!first) {
item(key = "spacer_${peerSet.user?.DisplayName}") {
Lists.ItemDivider()
Spacer(Modifier.height(24.dp))
}
}
first = false
stickyHeader {
ListItem(
modifier = Modifier.heightIn(max = 48.dp),
headlineContent = {
Text(
text = peerSet.user?.DisplayName ?: stringResource(id = R.string.unknown_user),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold)
})
}
itemsWithDividers(peerSet.peers, key = { it.StableID }) { peer ->
ListItem(
modifier = Modifier.clickable { onNavigateToPeerDetails(peer) },
colors = MaterialTheme.colorScheme.listItem,
headlineContent = {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier =
Modifier.padding(top = 2.dp)
.size(10.dp)
.background(
color = peer.connectedColor(netmap.value),
shape = RoundedCornerShape(percent = 50))) {}
Spacer(modifier = Modifier.size(8.dp))
Text(text = peer.ComputedName, style = MaterialTheme.typography.titleMedium)
}
},
supportingContent = {
Text(
text = peer.Addresses?.first()?.split("/")?.first() ?: "",
style = MaterialTheme.typography.bodyMedium)
})
}
}
}
}
@OptIn(ExperimentalPermissionsApi::class)

@ -16,6 +16,7 @@
<string name="offline">Offline</string>
<string name="ok">OK</string>
<string name="warning">Warning</string>
<string name="search">Search\n</string>
<!-- Strings for the about screen -->
<string name="app_name">Tailscale</string>

Loading…
Cancel
Save