Fix keyboard filter bar placement issues

This commit is contained in:
MM20 2024-04-21 19:03:35 +02:00
parent 9b29696757
commit eda57aea2b
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
5 changed files with 307 additions and 299 deletions

View File

@ -41,6 +41,7 @@ fun AssistantScaffold(
fixedSearchBar: Boolean = false, fixedSearchBar: Boolean = false,
) { ) {
val viewModel: LauncherScaffoldVM = viewModel() val viewModel: LauncherScaffoldVM = viewModel()
val searchVM: SearchVM = viewModel()
val context = LocalContext.current val context = LocalContext.current
@ -60,6 +61,10 @@ fun AssistantScaffold(
val searchState = rememberLazyListState() val searchState = rememberLazyListState()
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp
)
val isSearchAtStart by remember { val isSearchAtStart by remember {
derivedStateOf { derivedStateOf {
searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0 searchState.firstVisibleItemIndex == 0 && searchState.firstVisibleItemScrollOffset == 0
@ -94,6 +99,7 @@ fun AssistantScaffold(
} }
} }
} }
val showNavBarScrim by remember { val showNavBarScrim by remember {
derivedStateOf { derivedStateOf {
if (reverseSearchResults) { if (reverseSearchResults) {
@ -148,8 +154,6 @@ fun AssistantScaffold(
} }
} }
} }
val searchVM: SearchVM = viewModel()
val actions by searchVM.searchActionResults val actions by searchVM.searchActionResults
val webSearchPadding by animateDpAsState( val webSearchPadding by animateDpAsState(
if (actions.isEmpty()) 0.dp else 48.dp if (actions.isEmpty()) 0.dp else 48.dp
@ -164,7 +168,7 @@ fun AssistantScaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
paddingValues = PaddingValues( paddingValues = PaddingValues(
top = (if (bottomSearchBar) 0.dp else 56.dp + webSearchPadding) + 4.dp + windowInsets.calculateTopPadding(), top = (if (bottomSearchBar) 0.dp else 56.dp + webSearchPadding) + 4.dp + windowInsets.calculateTopPadding(),
bottom = (if (bottomSearchBar) 56.dp + webSearchPadding else 0.dp) + 4.dp + windowInsets.calculateBottomPadding() bottom = (if (bottomSearchBar) 56.dp + webSearchPadding else 0.dp) + 4.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
), ),
reverse = reverseSearchResults, reverse = reverseSearchResults,
state = searchState state = searchState
@ -179,18 +183,12 @@ fun AssistantScaffold(
LauncherSearchBar( LauncherSearchBar(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize(),
.wrapContentHeight() searchBarOffset = {
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter) (if (searchBarFocused || fixedSearchBar) 0
.windowInsetsPadding(WindowInsets.safeDrawing) else searchBarOffset.toInt() * if (bottomSearchBar) -1 else 1)
.padding(8.dp) - (if (bottomSearchBar) with(density) { keyboardFilterBarPadding.toPx() }.toInt() else 0)
.offset { },
if (searchBarFocused || fixedSearchBar) IntOffset.Zero
else IntOffset(
0,
searchBarOffset.toInt() * if (bottomSearchBar) -1 else 1
)
},
level = { searchBarLevel }, level = { searchBarLevel },
focused = searchBarFocused, focused = searchBarFocused,
onFocusChange = { onFocusChange = {
@ -204,7 +202,7 @@ fun AssistantScaffold(
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
style = searchBarStyle, style = searchBarStyle,
reverse = bottomSearchBar, bottomSearchBar = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) { onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) } { searchVM.launchBestMatchOrAction(context) }
} else null } else null

View File

@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
@ -131,6 +131,10 @@ fun PagerScaffold(
val pagerState = rememberPagerState { 2 } val pagerState = rememberPagerState { 2 }
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp
)
val isSearchAtBottom by remember { val isSearchAtBottom by remember {
derivedStateOf { derivedStateOf {
if (reverseSearchResults) { if (reverseSearchResults) {
@ -511,11 +515,11 @@ fun PagerScaffold(
val paddingValues = if (bottomSearchBar) { val paddingValues = if (bottomSearchBar) {
PaddingValues( PaddingValues(
top = 4.dp + windowInsets.calculateTopPadding(), top = 4.dp + windowInsets.calculateTopPadding(),
bottom = 60.dp + webSearchPadding + windowInsets.calculateBottomPadding() bottom = 60.dp + webSearchPadding + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding
) )
} else { } else {
PaddingValues( PaddingValues(
bottom = 4.dp + windowInsets.calculateBottomPadding(), bottom = 4.dp + windowInsets.calculateBottomPadding() + keyboardFilterBarPadding,
top = 60.dp + webSearchPadding + windowInsets.calculateTopPadding() top = 60.dp + webSearchPadding + windowInsets.calculateTopPadding()
) )
} }
@ -597,27 +601,17 @@ fun PagerScaffold(
LauncherSearchBar( LauncherSearchBar(
modifier = Modifier modifier = Modifier
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter) .fillMaxSize(),
.fillMaxWidth() level = { searchBarLevel },
.wrapContentHeight() searchBarOffset = {
.windowInsetsPadding(WindowInsets.safeDrawing) (if (focusSearchBar || fixedSearchBar) 0 else searchBarOffset.value.toInt() * if (bottomSearchBar) 1 else -1) +
.padding(8.dp)
.offset {
IntOffset(
0,
if (focusSearchBar || fixedSearchBar) 0 else searchBarOffset.value.toInt() * if (bottomSearchBar) 1 else -1
)
}
.offset {
IntOffset(
0,
with(density) { with(density) {
widgetEditModeOffset (widgetEditModeOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx() .toPx()
.roundToInt() .roundToInt()
}) }
}, },
level = { searchBarLevel }, bottomSearchBar = bottomSearchBar,
focused = focusSearchBar, focused = focusSearchBar,
onFocusChange = { onFocusChange = {
if (it) viewModel.openSearch() if (it) viewModel.openSearch()
@ -630,7 +624,6 @@ fun PagerScaffold(
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
style = searchBarStyle, style = searchBarStyle,
reverse = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) { onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) } { searchVM.launchBestMatchOrAction(context) }
} else null } else null
@ -702,7 +695,8 @@ fun Modifier.pagerScaffoldScrollHandler(
val available = dragAmount - preConsumed val available = dragAmount - preConsumed
val consumedY = val consumedY =
scrollableState.scrollBy(available.y * scrollMultiplier) * scrollMultiplier scrollableState.scrollBy(available.y * scrollMultiplier) * scrollMultiplier
val consumedX = pagerState.scrollBy(available.x * pagerMultiplier) * pagerMultiplier val consumedX =
pagerState.scrollBy(available.x * pagerMultiplier) * pagerMultiplier
val totalConsumed = val totalConsumed =
Offset(preConsumed.x + consumedX, preConsumed.y + consumedY) Offset(preConsumed.x + consumedX, preConsumed.y + consumedY)
nestedScrollDispatcher.dispatchPostScroll( nestedScrollDispatcher.dispatchPostScroll(

View File

@ -21,11 +21,11 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
@ -116,6 +116,10 @@ fun PullDownScaffold(
val maxOffset = with(density) { 64.dp.toPx() } val maxOffset = with(density) { 64.dp.toPx() }
val toggleSearchThreshold = with(density) { 48.dp.toPx() } val toggleSearchThreshold = with(density) { 48.dp.toPx() }
val keyboardFilterBarPadding by animateDpAsState(
if (WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0 && !searchVM.showFilters.value) 50.dp else 0.dp
)
val isSearchAtTop by remember { val isSearchAtTop by remember {
derivedStateOf { derivedStateOf {
if (reverseSearchResults) { if (reverseSearchResults) {
@ -300,9 +304,7 @@ fun PullDownScaffold(
handleBackOrHomeEvent() handleBackOrHomeEvent()
} }
val keyboardController = LocalSoftwareKeyboardController.current
val gestureManager = LocalGestureDetector.current val gestureManager = LocalGestureDetector.current
val hapticFeedback = LocalHapticFeedback.current
val view = LocalView.current val view = LocalView.current
LaunchedEffect(isOverThreshold) { LaunchedEffect(isOverThreshold) {
@ -517,7 +519,9 @@ fun PullDownScaffold(
), ),
paddingValues = PaddingValues( paddingValues = PaddingValues(
top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 60.dp + webSearchPadding else 4.dp, top = windowInsets.calculateTopPadding() + if (!bottomSearchBar) 60.dp + webSearchPadding else 4.dp,
bottom = windowInsets.calculateBottomPadding() + if (bottomSearchBar) 60.dp + webSearchPadding else 4.dp bottom = windowInsets.calculateBottomPadding() +
keyboardFilterBarPadding +
if (bottomSearchBar) 60.dp + webSearchPadding else 4.dp
), ),
state = searchState, state = searchState,
reverse = reverseSearchResults, reverse = reverseSearchResults,
@ -575,32 +579,21 @@ fun PullDownScaffold(
LauncherSearchBar( LauncherSearchBar(
modifier = Modifier modifier = Modifier
.align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter) .fillMaxSize(),
.fillMaxWidth()
.wrapContentHeight()
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(8.dp)
.offset {
IntOffset(
0,
if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.value.toInt() * (if (bottomSearchBar) 1 else -1)
)
}
.offset {
IntOffset(
0,
with(density) {
editModeSearchBarOffset
.toPx()
.roundToInt()
})
},
level = { searchBarLevel }, level = { searchBarLevel },
focused = searchBarFocused, focused = searchBarFocused,
onFocusChange = { onFocusChange = {
if (it) viewModel.openSearch() if (it) viewModel.openSearch()
viewModel.setSearchbarFocus(it) viewModel.setSearchbarFocus(it)
}, },
searchBarOffset = {
(if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.value.toInt() * (if (bottomSearchBar) 1 else -1)) +
with(density) {
(editModeSearchBarOffset - if(bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx()
.roundToInt()
}
},
actions = actions, actions = actions,
highlightedAction = searchVM.bestMatch.value as? SearchAction, highlightedAction = searchVM.bestMatch.value as? SearchAction,
isSearchOpen = isSearchOpen, isSearchOpen = isSearchOpen,
@ -608,7 +601,7 @@ fun PullDownScaffold(
onValueChange = { searchVM.search(it) }, onValueChange = { searchVM.search(it) },
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
style = searchBarStyle, style = searchBarStyle,
reverse = bottomSearchBar, bottomSearchBar = bottomSearchBar,
onKeyboardActionGo = if (launchOnEnter) { onKeyboardActionGo = if (launchOnEnter) {
{ searchVM.launchBestMatchOrAction(context) } { searchVM.launchBestMatchOrAction(context) }
} else null } else null

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.search.filters
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -38,202 +39,197 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia import de.mm20.launcher2.ui.icons.Wikipedia
import de.mm20.launcher2.ui.overlays.Overlay
@Composable @Composable
fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -> Unit) { fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -> Unit) {
Overlay { val allCategoriesEnabled = filters.allCategoriesEnabled
val allCategoriesEnabled = filters.allCategoriesEnabled Column(
Box(modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.imePadding(), contentAlignment = Alignment.BottomCenter) { .background(MaterialTheme.colorScheme.surfaceContainerLow)
Column( .imePadding()
.height(50.dp)
) {
HorizontalDivider()
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
FilterChip(
selected = filters.allowNetwork,
onClick = {
onFiltersChange(filters.copy(allowNetwork = !filters.allowNetwork))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Language,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_online)) }
)
VerticalDivider(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .height(36.dp)
.background(MaterialTheme.colorScheme.surfaceContainerLow) .padding(horizontal = 8.dp)
) { )
HorizontalDivider() FilterChip(
Row( modifier = Modifier.padding(end = 8.dp),
modifier = Modifier selected = filters.apps && !allCategoriesEnabled,
.horizontalScroll(rememberScrollState()) onClick = {
.padding(horizontal = 8.dp), onFiltersChange(filters.toggleApps())
verticalAlignment = Alignment.CenterVertically, },
) { leadingIcon = {
FilterChip( Icon(
selected = filters.allowNetwork, imageVector = Icons.Rounded.Apps,
onClick = { contentDescription = null,
onFiltersChange(filters.copy(allowNetwork = !filters.allowNetwork)) modifier = Modifier.size(FilterChipDefaults.IconSize)
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Language,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_online)) }
) )
VerticalDivider( },
modifier = Modifier label = { Text(stringResource(R.string.search_filter_apps)) }
.height(36.dp) )
.padding(horizontal = 8.dp) FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.files && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleFiles())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Description,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_files)) }
selected = filters.apps && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleApps()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.contacts && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.toggleContacts())
imageVector = Icons.Rounded.Apps, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.Person,
}, contentDescription = null,
label = { Text(stringResource(R.string.search_filter_apps)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_contacts)) }
selected = filters.files && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleFiles()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.events && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.toggleEvents())
imageVector = Icons.Rounded.Description, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.Today,
}, contentDescription = null,
label = { Text(stringResource(R.string.preference_search_files)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_calendar)) }
selected = filters.contacts && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleContacts()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.shortcuts && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.toggleShortcuts())
imageVector = Icons.Rounded.Person, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.AppShortcut,
}, contentDescription = null,
label = { Text(stringResource(R.string.preference_search_contacts)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_appshortcuts)) }
selected = filters.events && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleEvents()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.articles && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.toggleArticles())
imageVector = Icons.Rounded.Today, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.Wikipedia,
}, contentDescription = null,
label = { Text(stringResource(R.string.preference_search_calendar)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_wikipedia)) }
selected = filters.shortcuts && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleShortcuts()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.websites && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.toggleWebsites())
imageVector = Icons.Rounded.AppShortcut, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.Public,
}, contentDescription = null,
label = { Text(stringResource(R.string.preference_search_appshortcuts)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_websites)) }
selected = filters.articles && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleArticles()) modifier = Modifier.padding(end = 8.dp),
}, selected = filters.places && !allCategoriesEnabled,
leadingIcon = { onClick = {
Icon( onFiltersChange(filters.togglePlaces())
imageVector = Icons.Rounded.Wikipedia, },
contentDescription = null, leadingIcon = {
modifier = Modifier.size(FilterChipDefaults.IconSize) Icon(
) imageVector = Icons.Rounded.Place,
}, contentDescription = null,
label = { Text(stringResource(R.string.preference_search_wikipedia)) } modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.preference_search_locations)) }
selected = filters.websites && !allCategoriesEnabled, )
onClick = { FilterChip(
onFiltersChange(filters.toggleWebsites()) selected = filters.tools && !allCategoriesEnabled,
}, onClick = {
leadingIcon = { onFiltersChange(filters.toggleTools())
Icon( },
imageVector = Icons.Rounded.Public, leadingIcon = {
contentDescription = null, Icon(
modifier = Modifier.size(FilterChipDefaults.IconSize) imageVector = Icons.Rounded.Handyman,
) contentDescription = null,
}, modifier = Modifier.size(FilterChipDefaults.IconSize)
label = { Text(stringResource(R.string.preference_search_websites)) }
) )
FilterChip( },
modifier = Modifier.padding(end = 8.dp), label = { Text(stringResource(R.string.search_filter_tools)) }
selected = filters.places && !allCategoriesEnabled, )
onClick = { VerticalDivider(
onFiltersChange(filters.togglePlaces()) modifier = Modifier
}, .height(36.dp)
leadingIcon = { .padding(horizontal = 8.dp)
Icon( )
imageVector = Icons.Rounded.Place, FilterChip(
contentDescription = null, selected = filters.hiddenItems,
modifier = Modifier.size(FilterChipDefaults.IconSize) onClick = {
) onFiltersChange(filters.copy(hiddenItems = !filters.hiddenItems))
}, },
label = { Text(stringResource(R.string.preference_search_locations)) } leadingIcon = {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
) )
FilterChip( },
selected = filters.tools && !allCategoriesEnabled, label = { Text(stringResource(R.string.preference_hidden_items)) }
onClick = { )
onFiltersChange(filters.toggleTools())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Handyman,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_tools)) }
)
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(horizontal = 8.dp)
)
FilterChip(
selected = filters.hiddenItems,
onClick = {
onFiltersChange(filters.copy(hiddenItems = !filters.hiddenItems))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_hidden_items)) }
)
}
HorizontalDivider()
}
} }
HorizontalDivider()
} }
} }

View File

@ -2,12 +2,23 @@ package de.mm20.launcher2.ui.launcher.searchbar
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.imeAnimationTarget
import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.text.KeyboardActionScope import androidx.compose.foundation.text.KeyboardActionScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.FilterAlt import androidx.compose.material.icons.rounded.FilterAlt
@ -24,7 +35,9 @@ 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.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.SearchBarStyle import de.mm20.launcher2.preferences.SearchBarStyle
@ -47,8 +60,9 @@ fun LauncherSearchBar(
actions: List<SearchAction>, actions: List<SearchAction>,
highlightedAction: SearchAction?, highlightedAction: SearchAction?,
isSearchOpen: Boolean = false, isSearchOpen: Boolean = false,
reverse: Boolean = false,
darkColors: Boolean = false, darkColors: Boolean = false,
bottomSearchBar: Boolean = false,
searchBarOffset: () -> Int = { 0 },
onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null onKeyboardActionGo: (KeyboardActionScope.() -> Unit)? = null
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@ -61,64 +75,77 @@ fun LauncherSearchBar(
else focusManager.clearFocus() else focusManager.clearFocus()
} }
if (isSearchOpen && !searchVM.showFilters.value && WindowInsets.isImeVisible) {
KeyboardFilterBar(
filters = searchVM.filters.value,
onFiltersChange = {
searchVM.setFilters(it)
}
)
}
val _value = value() val _value = value()
SearchBar( Box(modifier = modifier) {
modifier = modifier, SearchBar(
style = style, level = level(), value = _value, onValueChange = onValueChange, modifier = Modifier
reverse = reverse, .align(if (bottomSearchBar) Alignment.BottomCenter else Alignment.TopCenter)
darkColors = darkColors, .windowInsetsPadding(WindowInsets.safeDrawing)
menu = { .padding(8.dp)
AnimatedVisibility( .offset { IntOffset(0, searchBarOffset()) },
isSearchOpen, style = style, level = level(), value = _value, onValueChange = onValueChange,
enter = scaleIn(tween(100)), reverse = bottomSearchBar,
exit = scaleOut(tween(100)) darkColors = darkColors,
) { menu = {
FilledIconButton( AnimatedVisibility(
onClick = { isSearchOpen,
searchVM.showFilters.value = !searchVM.showFilters.value enter = scaleIn(tween(100)),
}, exit = scaleOut(tween(100))
colors = if (searchVM.showFilters.value) IconButtonDefaults.filledTonalIconButtonColors()
else IconButtonDefaults.iconButtonColors()
) { ) {
Box { FilledIconButton(
Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null) onClick = {
androidx.compose.animation.AnimatedVisibility( searchVM.showFilters.value = !searchVM.showFilters.value
!searchVM.filters.value.allCategoriesEnabled, },
enter = scaleIn(tween(100)), colors = if (searchVM.showFilters.value) IconButtonDefaults.filledTonalIconButtonColors()
exit = scaleOut(tween(100)), else IconButtonDefaults.iconButtonColors()
modifier = Modifier ) {
.align(Alignment.BottomEnd) Box {
.offset(-3.dp, -3.dp) Icon(imageVector = Icons.Rounded.FilterAlt, contentDescription = null)
) { androidx.compose.animation.AnimatedVisibility(
Badge( !searchVM.filters.value.allCategoriesEnabled,
containerColor = MaterialTheme.colorScheme.tertiary, enter = scaleIn(tween(100)),
) exit = scaleOut(tween(100)),
modifier = Modifier
.align(Alignment.BottomEnd)
.offset(-3.dp, -3.dp)
) {
Badge(
containerColor = MaterialTheme.colorScheme.tertiary,
)
}
} }
} }
} }
} SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange)
SearchBarMenu(searchBarValue = _value, onSearchBarValueChange = onValueChange) },
}, actions = {
actions = { SearchBarActions(
SearchBarActions( actions = actions,
actions = actions, reverse = bottomSearchBar,
reverse = reverse, highlightedAction = highlightedAction
highlightedAction = highlightedAction )
},
focusRequester = focusRequester,
onFocus = { onFocusChange(true) },
onUnfocus = { onFocusChange(false) },
onKeyboardActionGo = onKeyboardActionGo
)
AnimatedVisibility (isSearchOpen && !searchVM.showFilters.value
// Use imeAnimationTarget instead of isImeVisible to animate the filter bar at the same time as the keyboard
&& WindowInsets.imeAnimationTarget.getBottom(LocalDensity.current) > 0,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter)
) {
KeyboardFilterBar(
filters = searchVM.filters.value,
onFiltersChange = {
searchVM.setFilters(it)
}
) )
}, }
focusRequester = focusRequester, }
onFocus = { onFocusChange(true) },
onUnfocus = { onFocusChange(false) },
onKeyboardActionGo = onKeyboardActionGo
)
} }