Skip to content

Commit

Permalink
feat: move search to the bottom
Browse files Browse the repository at this point in the history
  • Loading branch information
tiago-araujo-edo committed Jun 11, 2024
1 parent 0430db7 commit 6242870
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 25 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
android:clearTaskOnLaunch="true"
android:excludeFromRecents="true"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:stateNotNeeded="true"
android:theme="@style/Theme.GridLauncher">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class GetAppListUseCase @Inject constructor(
operator fun invoke() = appsManager.installedAppsFlow
.map { appList ->
appList
.distinctBy { it.packageName }
.filterNot { it.packageName == context.packageName } // Hide this app from the launcher list
.sortedBy { app -> app.name }
}
Expand Down
233 changes: 233 additions & 0 deletions app/src/main/java/tgo1014/gridlauncher/ui/composables/SearchFab.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package tgo1014.gridlauncher.ui.composables

import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.DecorationBox
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import tgo1014.gridlauncher.R
import tgo1014.gridlauncher.ui.theme.GridLauncherTheme

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchFab(
modifier: Modifier = Modifier,
searchText: String,
buttonState: SearchFabState,
onCloseClicked: () -> Unit = {},
onSearchTextChanged: (String) -> Unit = {},
onButtonClicked: () -> Unit = {},
) = BoxWithConstraints(modifier) {
val keyboardController = LocalSoftwareKeyboardController.current
val fabSize = 56.dp
val itemsSize = 32.dp
val shape = RoundedCornerShape(25)
val interactionSource = remember { MutableInteractionSource() }
val transition = updateTransition(buttonState, label = "Width")
val height by transition.animateDp(
targetValueByState = {
when (it) {
SearchFabState.FAB -> fabSize
SearchFabState.SEARCH -> 65.dp
}
}, label = "Height"
)
val width by transition.animateDp(
targetValueByState = {
if (it == SearchFabState.FAB) fabSize else maxWidth
},
transitionSpec = { tween() },
label = "width"
)
Surface(
shape = shape,
color = MaterialTheme.colorScheme.primary,
//elevation = FloatingActionButtonDefaults.elevation().elevation(interactionSource).value,
modifier = Modifier
.width(width)
.height(height)
.align(Alignment.BottomEnd)
.clickable { onButtonClicked() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(12.dp)
) {
val paddingEnd by animateDpAsState(targetValue = if (buttonState == SearchFabState.FAB) 24.dp else 12.dp)
val focusRequest = remember { FocusRequester() }
if (buttonState == SearchFabState.FAB) {
LocalFocusManager.current.clearFocus(true)
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
modifier = Modifier.requiredSize(itemsSize),
tint = MaterialTheme.colorScheme.onPrimary
)
} else {
val singleLine = true
val colors = TextFieldDefaults.colors().copy(
cursorColor = MaterialTheme.colorScheme.secondary,
focusedContainerColor = Color.Transparent,
focusedLabelColor = MaterialTheme.colorScheme.onPrimary,
focusedIndicatorColor = MaterialTheme.colorScheme.onPrimary,
)
val selectionColors = TextSelectionColors(
handleColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.4f)
)
SideEffect { focusRequest.requestFocus() }
CompositionLocalProvider(LocalTextSelectionColors provides selectionColors) {
BasicTextField(
value = searchText,
onValueChange = { onSearchTextChanged(it) },
interactionSource = interactionSource,
singleLine = singleLine,
cursorBrush = SolidColor(MaterialTheme.colorScheme.secondary),
textStyle = TextStyle.Default.copy(
fontSize = 20.sp,
color = MaterialTheme.colorScheme.onPrimary
),
decorationBox = {
DecorationBox(
value = searchText,
innerTextField = it,
enabled = true,
interactionSource = interactionSource,
singleLine = singleLine,
visualTransformation = VisualTransformation.None,
contentPadding = PaddingValues(horizontal = 0.dp),
colors = colors,
label = { Text(text = stringResource(R.string.search)) },
trailingIcon = {
IconButton(
onClick = {
keyboardController?.hide()
onCloseClicked()
},
content = {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
)
}
)
},
)
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
capitalization = KeyboardCapitalization.Sentences
),
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }),
modifier = Modifier
.fillMaxWidth()
.requiredHeight(45.dp)
.focusRequester(focusRequest)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors
)
)
}
}
}
}
}

enum class SearchFabState {
FAB, SEARCH;

fun toggle() = if (this == FAB) SEARCH else FAB
}

@Preview
@Composable
private fun SearchFabPreviewFab() = GridLauncherTheme {
var state by remember { mutableStateOf(SearchFabState.FAB) }
SearchFab(buttonState = state, searchText = "") {
state = if (state == SearchFabState.FAB) SearchFabState.SEARCH else SearchFabState.FAB
}
}

@Preview
@Composable
private fun SearchFabPreviewFabLoading() = GridLauncherTheme {
var state by remember { mutableStateOf(SearchFabState.FAB) }
SearchFab(buttonState = state, searchText = "") {
state = if (state == SearchFabState.FAB) SearchFabState.SEARCH else SearchFabState.FAB
}
}

@Preview
@Composable
private fun SearchFabPreviewSearch() = GridLauncherTheme {
var state by remember { mutableStateOf(SearchFabState.FAB) }
SearchFab(buttonState = SearchFabState.SEARCH, searchText = "123") {
state = if (state == SearchFabState.FAB) SearchFabState.SEARCH else SearchFabState.FAB
}
}

@Preview
@Composable
private fun SearchFabPreviewSearchLoading() = GridLauncherTheme {
var state by remember { mutableStateOf(SearchFabState.FAB) }
SearchFab(buttonState = SearchFabState.SEARCH, searchText = "123") {
state = if (state == SearchFabState.FAB) SearchFabState.SEARCH else SearchFabState.FAB
}
}
56 changes: 34 additions & 22 deletions app/src/main/java/tgo1014/gridlauncher/ui/home/AppListScreen.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package tgo1014.gridlauncher.ui.home

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
Expand All @@ -20,6 +17,9 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
Expand All @@ -34,7 +34,6 @@ import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -61,8 +60,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import tgo1014.gridlauncher.R
import tgo1014.gridlauncher.domain.models.App
import tgo1014.gridlauncher.ui.composables.LaunchedUnitEffect
import tgo1014.gridlauncher.ui.composables.Search
import tgo1014.gridlauncher.ui.composables.SearchFab
import tgo1014.gridlauncher.ui.composables.SearchFabState
import tgo1014.gridlauncher.ui.theme.AsyncImage
import tgo1014.gridlauncher.ui.theme.GridLauncherTheme
import tgo1014.gridlauncher.ui.theme.detectConsumedVerticalDragGestures
Expand All @@ -82,7 +81,7 @@ fun AppListScreen(
onFilterClearPressed: () -> Unit = {},
onUninstall: (App) -> Unit = {},
onBackPressed: () -> Unit = {},
) {
) = Box {
BackHandler(onBack = onBackPressed)
val lazyListState = rememberLazyListState()
val angle by animateFloatAsState(
Expand All @@ -94,13 +93,16 @@ fun AppListScreen(
},
label = "Inclination"
)
var isOnTop by remember { mutableStateOf(false) }
val isOnTop = lazyListState.canScrollBackward
val mainColor = MaterialTheme.colorScheme.secondaryContainer
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
val searchInputTextPadding = 75.dp
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(8.dp) + WindowInsets.systemBars.asPaddingValues(),
contentPadding = PaddingValues(8.dp)
+ WindowInsets.systemBars.asPaddingValues()
+ WindowInsets.ime.asPaddingValues()
+ PaddingValues(bottom = searchInputTextPadding),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxSize()
Expand All @@ -113,18 +115,6 @@ fun AppListScreen(
}
}
) {
item(key = "Search") {
DisposableEffect(Unit) {
isOnTop = true
onDispose { isOnTop = false }
}
Search(
text = state.filterString,
onTextChanged = onFilterTextChanged,
onClearPressed = onFilterClearPressed,
modifier = Modifier.animateItem()
)
}
val appList = state.appList
val listByLetter = appList
.sortedBy { it.nameFirstLetter.uppercase() }
Expand Down Expand Up @@ -237,6 +227,28 @@ fun AppListScreen(
}
}
}
val scope = rememberCoroutineScope()
var fabState by remember { mutableStateOf(SearchFabState.FAB) }
SearchFab(
buttonState = fabState,
searchText = state.filterString,
onSearchTextChanged = {
scope.launch {
lazyListState.animateScrollToItem(0)
}
onFilterTextChanged(it)
},
onCloseClicked = {
fabState = SearchFabState.FAB
onFilterClearPressed()
},
onButtonClicked = { fabState = fabState.toggle() },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp)
.navigationBarsPadding()
.imePadding(),
)
}

@Composable
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/tgo1014/gridlauncher/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package tgo1014.gridlauncher.ui.home

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
Expand Down Expand Up @@ -53,7 +52,6 @@ fun HomeScreen(
)
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun HomeScreen(
state: HomeState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class HomeScreenViewModel @Inject constructor(

private fun resetState() {
_stateFlow.update { it.copy(itemBeingEdited = null) }
onFilterTextChanged("")
onFilterCleared()
}

}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">GridLauncher</string>
<string name="search">Search</string>
</resources>

0 comments on commit 6242870

Please sign in to comment.