From 399795cb5487429343feee80365589e3ad49bc00 Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Thu, 17 Feb 2022 16:43:47 -0500 Subject: [PATCH] add search bar --- app/build.gradle | 5 +- .../com/owenlejeune/tvtime/MainActivity.kt | 25 ++++++-- .../tvtime/ui/components/Widgets.kt | 63 +++++++++++++++++-- .../tvtime/ui/navigation/Routes.kt | 7 ++- .../owenlejeune/tvtime/ui/screens/MainView.kt | 59 ++++++++++++++--- .../ui/screens/tabs/bottom/SettingsTab.kt | 6 +- .../tvtime/utils/KeyBoardManager.kt | 49 +++++++++++++++ 7 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/KeyBoardManager.kt diff --git a/app/build.gradle b/app/build.gradle index 8ac010c..0c99fd5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,4 @@ -import com.owenlejeune.tvtime.buildsrc.Config -import com.owenlejeune.tvtime.buildsrc.Versions +import com.owenlejeune.tvtime.buildsrc.* plugins { id 'com.android.application' @@ -84,6 +83,8 @@ dependencies { //Coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation "me.onebone:toolbar-compose:2.3.1" + testImplementation "junit:junit:${Versions.junit}" androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}" androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}" diff --git a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt index 04b887f..33bbeb8 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/MainActivity.kt @@ -4,22 +4,22 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes import com.owenlejeune.tvtime.ui.theme.TVTimeTheme +import com.owenlejeune.tvtime.utils.KeyBoardManager class MainActivity : ComponentActivity() { -// private val appNavControllerProvider: (@Composable () -> NavHostController) by inject(named(NavControllers.APP)) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + AppKeyboardFocusManager() val displayUnderStatusBar = remember { mutableStateOf(false) } // WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value) // val statusBarColor = if (displayUnderStatusBar.value) { @@ -53,4 +53,19 @@ fun MyApp( @Composable fun MyAppPreview() { MyApp() +} + +@Composable +private fun AppKeyboardFocusManager() { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + DisposableEffect(key1 = context) { + val keyboardManager = KeyBoardManager(context) + keyboardManager.attachKeyboardDismissListener { + focusManager.clearFocus() + } + onDispose { + keyboardManager.release() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index ebe503c..0f14369 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -3,12 +3,15 @@ package com.owenlejeune.tvtime.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* +import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.selection.toggleable 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.material.Card import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search @@ -23,6 +26,7 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -294,8 +298,57 @@ fun RatingRing( } } -@Preview @Composable -fun RatingRingPreview() { - RatingRing(progress = 0.5f) +fun RoundedTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeHolder: String = "", + placeHolderTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant, + textStyle: TextStyle = MaterialTheme.typography.bodySmall, + cursorColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + singleLine: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + enabled: Boolean = true, + readOnly: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(50.dp), + color = backgroundColor + ) { + Box( + modifier = Modifier.padding(horizontal = 12.dp), + contentAlignment = Alignment.CenterStart + ) { + if (value.isEmpty() && placeHolder.isNotEmpty()) { + Text( + text = placeHolder, + style = textStyle, + color = placeHolderTextColor + ) + } + Row( + verticalAlignment = Alignment.CenterVertically + ) { + BasicTextField( + modifier = Modifier.weight(1f), + value = value, + onValueChange = onValueChange, + singleLine = singleLine, + textStyle = textStyle.copy(color = textColor), + cursorBrush = SolidColor(cursorColor), + maxLines = maxLines, + enabled = enabled, + readOnly = readOnly, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt index 0b86f5a..9f66a62 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/navigation/Routes.kt @@ -48,19 +48,24 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar @Composable fun BottomNavigationRoutes( appNavController: NavHostController, - navController: NavHostController + navController: NavHostController, + shouldShowSearch: MutableState ) { NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) { composable(BottomNavItem.Movies.route) { + shouldShowSearch.value = true MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE) } composable(BottomNavItem.TV.route) { + shouldShowSearch.value = true MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV) } composable(BottomNavItem.Favourites.route) { + shouldShowSearch.value = false FavouritesTab() } composable(BottomNavItem.Settings.route) { + shouldShowSearch.value = false SettingsTab() } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt index 2ce5dd9..0c91c22 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MainView.kt @@ -1,19 +1,22 @@ package com.owenlejeune.tvtime.ui.screens import androidx.compose.animation.rememberSplineBasedDecay -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.Scaffold import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.pager.ExperimentalPagerApi +import com.owenlejeune.tvtime.ui.components.RoundedTextField import com.owenlejeune.tvtime.ui.components.SearchFab import com.owenlejeune.tvtime.ui.navigation.BottomNavItem import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes @@ -31,6 +34,8 @@ fun MainAppView(appNavController: NavHostController) { TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec) } + val shouldShowSearch = remember { mutableStateOf(true) } + // todo - scroll state not remember when returing from detail screen Scaffold( @@ -45,7 +50,8 @@ fun MainAppView(appNavController: NavHostController) { topBar = { TopBar( title = appBarTitle, - scrollBehavior = scrollBehavior + scrollBehavior = scrollBehavior, + shouldShowSearch = shouldShowSearch ) }, floatingActionButton = { @@ -55,16 +61,53 @@ fun MainAppView(appNavController: NavHostController) { } ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { - BottomNavigationRoutes(appNavController = appNavController, navController = navController) + BottomNavigationRoutes(appNavController = appNavController, navController = navController, shouldShowSearch = shouldShowSearch) } } } @Composable -private fun TopBar(title: MutableState, scrollBehavior: TopAppBarScrollBehavior) { - LargeTopAppBar( - title = { Text(text = title.value) }, - scrollBehavior = scrollBehavior +private fun TopBar( + title: MutableState, + scrollBehavior: TopAppBarScrollBehavior, + hasSearchFocus: MutableState = remember { mutableStateOf(false) }, + shouldShowSearch: MutableState = remember { mutableStateOf(true) } +) { + SmallTopAppBar( + title = { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (!hasSearchFocus.value) { + Text(text = title.value) + } + if (shouldShowSearch.value) { + var textState by remember { mutableStateOf("") } + val basePadding = 8.dp + RoundedTextField( + modifier = Modifier + .padding( + end = if (hasSearchFocus.value) (basePadding * 2) else basePadding, + start = if (hasSearchFocus.value) 0.dp else basePadding + ) + + .height(35.dp) + .onFocusChanged { focusState -> + hasSearchFocus.value = focusState.isFocused + }, + value = textState, + onValueChange = { textState = it }, + placeHolder = "Search ${title.value.lowercase()}" + ) + } + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults + .largeTopAppBarColors( + scrolledContainerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.primary + ) ) } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/SettingsTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/SettingsTab.kt index 291910e..b59d7ca 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/SettingsTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/tabs/bottom/SettingsTab.kt @@ -37,7 +37,11 @@ fun SettingsTab(preferences: AppPreferences = get(AppPreferences::class.java)) { val shouldShowPalette = remember { mutableStateOf(false) } Text( text = "Show material palette", - color = MaterialTheme.colorScheme.onBackground, + color = if (usePreferences.value) { + MaterialTheme.colorScheme.onBackground + } else { + MaterialTheme.colorScheme.outline + }, modifier = Modifier .padding(12.dp) .clickable( diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/KeyBoardManager.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/KeyBoardManager.kt new file mode 100644 index 0000000..f593809 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/KeyBoardManager.kt @@ -0,0 +1,49 @@ +package com.owenlejeune.tvtime.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver + +class KeyBoardManager(context: Context) { + + private val activity = context as Activity + private var keyboardDismissListener: KeyboardDismissListener? = null + + private abstract class KeyboardDismissListener( + private val rootView: View, + private val onKeyboardDismiss: () -> Unit + ) : ViewTreeObserver.OnGlobalLayoutListener { + private var isKeyboardClosed: Boolean = false + override fun onGlobalLayout() { + val r = Rect() + rootView.getWindowVisibleDisplayFrame(r) + val screenHeight = rootView.rootView.height + val keypadHeight = screenHeight - r.bottom + if (keypadHeight > screenHeight * 0.15) { + // 0.15 ratio is right enough to determine keypad height. + isKeyboardClosed = false + } else if (!isKeyboardClosed) { + isKeyboardClosed = true + onKeyboardDismiss.invoke() + } + } + } + + fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) { + val rootView = activity.findViewById(android.R.id.content) + keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {} + keyboardDismissListener?.let { + rootView.viewTreeObserver.addOnGlobalLayoutListener(it) + } + } + + fun release() { + val rootView = activity.findViewById(android.R.id.content) + keyboardDismissListener?.let { + rootView.viewTreeObserver.removeOnGlobalLayoutListener(it) + } + keyboardDismissListener = null + } +} \ No newline at end of file