From 06437df84deb428ca1ea2df26773c6ce32f27a95 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:00:51 +0100 Subject: [PATCH] Adjust search action settings screen design --- .../component/preferences/PreferenceScreen.kt | 14 -- .../mm20/launcher2/ui/ktx/CornerBasedShape.kt | 39 ++---- .../SearchActionsSettingsScreen.kt | 130 ++++++++++++++---- 3 files changed, 112 insertions(+), 71 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt index edc73d4cb..1b4ccf49e 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt @@ -88,21 +88,7 @@ fun PreferenceScreen( val colorScheme = MaterialTheme.colorScheme - val touchSlop = LocalViewConfiguration.current.touchSlop var fabVisible by remember { mutableStateOf(true) } - val nestedScrollConnection = remember { - object : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - if (consumed.y < -touchSlop) fabVisible = false - else if (consumed.y > touchSlop) fabVisible = true - return super.onPostScroll(consumed, available, source) - } - } - } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt index 57a6315d4..111d78bc2 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/CornerBasedShape.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.RectangleShape @@ -19,30 +19,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density -/** - * Animates the corners of a shape between its original value and 0. - * For each parameter set to true, the corresponding corner will animate to the original value. - * Otherwise, it will animate to 0. - */ -@Composable -fun CornerBasedShape.animateCorners( - topStart: Boolean, - topEnd: Boolean, - bottomEnd: Boolean, - bottomStart: Boolean, -): CornerBasedShape { - val value by animateShapeAsState( - copy( - topStart = if (topStart) this.topStart else CornerSize(0f), - topEnd = if (topEnd) this.topEnd else CornerSize(0f), - bottomEnd = if (bottomEnd) this.bottomEnd else CornerSize(0f), - bottomStart = if (bottomStart) this.bottomStart else CornerSize(0f) - ) - ) - - return value -} - /** * Animate between two shapes. * Limitations: @@ -52,12 +28,19 @@ fun CornerBasedShape.animateCorners( */ @Composable fun animateShapeAsState( - shape: CornerBasedShape, + shape: Shape, animationSpec: AnimationSpec = remember { spring() }, visibilityThreshold: CornerBasedShape? = null, label: String = "ValueAnimation", - finishedListener: ((CornerBasedShape) -> Unit)? = null -): State { + finishedListener: ((Shape) -> Unit)? = null +): State { + + if (shape !is CornerBasedShape) { + return remember(shape) { + mutableStateOf(shape) + } + } + val density = LocalDensity.current val converter = remember(shape.javaClass, density) { if (shape is CutCornerShape) CutCornerShapeConverter(density) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt index 7581a3ece..4ed436899 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt @@ -6,19 +6,22 @@ import androidx.appcompat.app.AppCompatActivity import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.DropdownMenuGroup import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -26,6 +29,7 @@ import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -34,8 +38,11 @@ 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.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -54,6 +61,7 @@ import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState import de.mm20.launcher2.ui.component.getSearchActionIconVector import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.SwitchPreference +import de.mm20.launcher2.ui.ktx.animateShapeAsState import de.mm20.launcher2.ui.locals.LocalBackStack import kotlinx.serialization.Serializable @@ -84,6 +92,8 @@ fun SearchActionsSettingsScreen() { val searchActions by viewModel.searchActions.collectAsState() val disabledActions by viewModel.disabledActions.collectAsState() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + Scaffold( topBar = { CenterAlignedTopAppBar( @@ -95,6 +105,10 @@ fun SearchActionsSettingsScreen() { maxLines = 1 ) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), navigationIcon = { IconButton(onClick = { if (backStack.size <= 1) { @@ -110,12 +124,6 @@ fun SearchActionsSettingsScreen() { } }, actions = { - IconButton(onClick = { viewModel.createAction() }) { - Icon( - painterResource(R.drawable.add_24px), - contentDescription = null - ) - } IconButton(onClick = { CustomTabsIntent.Builder() .setDefaultColorSchemeParams( @@ -134,9 +142,12 @@ fun SearchActionsSettingsScreen() { contentDescription = "Help" ) } - } + }, + scrollBehavior = scrollBehavior, ) - }) { + }, + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) { LazyDragAndDropColumn( state = listState, @@ -144,6 +155,8 @@ fun SearchActionsSettingsScreen() { contentPadding = it, modifier = Modifier .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { item( key = "disabled-info" @@ -168,19 +181,28 @@ fun SearchActionsSettingsScreen() { ) } } - items( + itemsIndexed( items = searchActions, - key = { it.key } - ) { item -> + key = { _, it -> it.key } + ) { index, item -> DraggableItem( state = listState, key = item.key ) { + + val shape = getShape(index, searchActions.size) + val draggedShape = MaterialTheme.shapes.medium + val elevation by animateDpAsState(if (it) 4.dp else 0.dp) Surface( shadowElevation = elevation, tonalElevation = elevation, - modifier = Modifier.zIndex(if (it) 1f else 0f) + modifier = Modifier + .padding(horizontal = 12.dp) + .zIndex(if (it) 1f else 0f), + shape = animateShapeAsState( + if (it) draggedShape else shape + ).value ) { if (item is CustomizableSearchActionBuilder) { Preference( @@ -263,24 +285,48 @@ fun SearchActionsSettingsScreen() { Box( modifier = Modifier .fillMaxWidth() - .height(0.5.dp) - .background( - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) - ) + .height(8.dp) ) } - items( + itemsIndexed( items = disabledActions, - key = { "disabled-${it.key}" } - ) { item -> - SwitchPreference( - icon = getSearchActionIconVector(item.icon), - title = item.label, - value = false, - onValueChanged = { - viewModel.addAction(item) - } - ) + key = { _, it -> "disabled-${it.key}" } + ) { index, item -> + val shape = getShape(index, disabledActions.size) + + Box( + modifier = Modifier + .padding(horizontal = 12.dp) + .clip(shape) + ) { + SwitchPreference( + icon = getSearchActionIconVector(item.icon), + title = item.label, + value = false, + onValueChanged = { + viewModel.addAction(item) + } + ) + } + } + item(key = "disabled-button") { + FilledTonalButton( + modifier = Modifier + .padding(top = 10.dp, start = 12.dp, end = 12.dp) + .navigationBarsPadding(), + contentPadding = ButtonDefaults.ButtonWithIconContentPadding, + onClick = { + viewModel.createAction() + }) { + Icon( + painterResource(R.drawable.add_20px), + null, + modifier = Modifier + .padding(end = ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text(stringResource(R.string.create_search_action_title)) + } } } } @@ -308,4 +354,30 @@ fun SearchActionsSettingsScreen() { viewModel.dismissDialogs() } ) +} + +@Composable +private fun getShape(index: Int, total: Int): Shape { + if (total == 1) { + return MaterialTheme.shapes.medium + } + + if (total > 1 && index > 0 && index < total - 1) { + return MaterialTheme.shapes.extraSmall + } + + val xs = MaterialTheme.shapes.extraSmall + val md = MaterialTheme.shapes.medium + + if (index == 0) { + return xs.copy( + topStart = md.topStart, + topEnd = md.topEnd + ) + } else { + return xs.copy( + bottomStart = md.bottomStart, + bottomEnd = md.bottomEnd + ) + } } \ No newline at end of file