show/hide search bar with fab and settings

This commit is contained in:
Owen LeJeune
2022-02-18 17:17:57 -05:00
parent 0da9f01551
commit e346f1ebd7
4 changed files with 73 additions and 24 deletions

View File

@@ -12,7 +12,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes import com.owenlejeune.tvtime.ui.navigation.MainNavigationRoutes
import com.owenlejeune.tvtime.ui.theme.TVTimeTheme import com.owenlejeune.tvtime.ui.theme.TVTimeTheme
import com.owenlejeune.tvtime.utils.KeyBoardManager import com.owenlejeune.tvtime.utils.KeyboardManager
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -60,7 +60,7 @@ private fun AppKeyboardFocusManager() {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
DisposableEffect(key1 = context) { DisposableEffect(key1 = context) {
val keyboardManager = KeyBoardManager(context) val keyboardManager = KeyboardManager.getInstance(context)
keyboardManager.attachKeyboardDismissListener { keyboardManager.attachKeyboardDismissListener {
focusManager.clearFocus() focusManager.clearFocus()
} }

View File

@@ -18,13 +18,12 @@ import androidx.compose.material.Switch
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
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.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -274,10 +273,15 @@ fun TopLevelSwitchPreview() {
} }
@Composable @Composable
fun SearchFab() { fun SearchFab(
focusSearchBar: MutableState<Boolean> = mutableStateOf(false),
focusRequester: FocusRequester = remember { FocusRequester() }
) {
val context = LocalContext.current val context = LocalContext.current
FloatingActionButton(onClick = { FloatingActionButton(onClick = {
Toast.makeText(context, "Search Clicked!", Toast.LENGTH_SHORT).show() focusSearchBar.value = true
// focusRequester.requestFocus()
// Toast.makeText(context, "Search Clicked!", Toast.LENGTH_SHORT).show()
}) { }) {
Icon(Icons.Filled.Search, "") Icon(Icons.Filled.Search, "")
} }
@@ -422,6 +426,8 @@ fun RoundedTextField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
requestFocus: Boolean = false,
placeHolder: String = "", placeHolder: String = "",
placeHolderTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, placeHolderTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -455,7 +461,9 @@ fun RoundedTextField(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
BasicTextField( BasicTextField(
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.focusRequester(focusRequester),
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
singleLine = singleLine, singleLine = singleLine,
@@ -470,4 +478,10 @@ fun RoundedTextField(
} }
} }
} }
if (requestFocus) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
} }

View File

@@ -10,8 +10,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -24,6 +26,7 @@ import com.owenlejeune.tvtime.ui.components.RoundedTextField
import com.owenlejeune.tvtime.ui.components.SearchFab import com.owenlejeune.tvtime.ui.components.SearchFab
import com.owenlejeune.tvtime.ui.navigation.BottomNavItem import com.owenlejeune.tvtime.ui.navigation.BottomNavItem
import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes import com.owenlejeune.tvtime.ui.navigation.BottomNavigationRoutes
import com.owenlejeune.tvtime.utils.KeyboardManager
import org.koin.java.KoinJavaComponent.get import org.koin.java.KoinJavaComponent.get
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@@ -39,7 +42,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec) TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec)
} }
val shouldShowSearch = remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() }
val focusSearchBar = remember { mutableStateOf(false) }
val searchableScreens = listOf(BottomNavItem.Movies.route, BottomNavItem.TV.route) val searchableScreens = listOf(BottomNavItem.Movies.route, BottomNavItem.TV.route)
// todo - scroll state not remember when returing from detail screen // todo - scroll state not remember when returing from detail screen
@@ -58,7 +62,8 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
SearchTopBar( SearchTopBar(
title = appBarTitle, title = appBarTitle,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
shouldShowSearch = shouldShowSearch requestSearchFocus = focusSearchBar,
focusRequester = focusRequester
) )
} else { } else {
TopBar( TopBar(
@@ -68,8 +73,11 @@ fun MainAppView(appNavController: NavHostController, preferences: AppPreferences
} }
}, },
floatingActionButton = { floatingActionButton = {
if (currentRoute in searchableScreens && !preferences.persistentSearch) { if (currentRoute in searchableScreens && !preferences.persistentSearch && !focusSearchBar.value) {
SearchFab() SearchFab(
focusSearchBar = focusSearchBar,
focusRequester = focusRequester
)
} }
} }
) { innerPadding -> ) { innerPadding ->
@@ -99,8 +107,8 @@ private fun TopBar(
private fun SearchTopBar( private fun SearchTopBar(
title: MutableState<String>, title: MutableState<String>,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
hasSearchFocus: MutableState<Boolean> = remember { mutableStateOf(false) }, requestSearchFocus: MutableState<Boolean> = remember { mutableStateOf(false) },
shouldShowSearch: MutableState<Boolean> = remember { mutableStateOf(false) }, focusRequester: FocusRequester = remember { FocusRequester() },
preferences: AppPreferences = get(AppPreferences::class.java) preferences: AppPreferences = get(AppPreferences::class.java)
) { ) {
SmallTopAppBar( SmallTopAppBar(
@@ -108,10 +116,11 @@ private fun SearchTopBar(
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (!hasSearchFocus.value && !(preferences.persistentSearch && preferences.hideTitle)) { val hasSearchFocus = remember { mutableStateOf(requestSearchFocus.value) }
if (!requestSearchFocus.value && !hasSearchFocus.value && !(preferences.persistentSearch && preferences.hideTitle)) {
Text(text = title.value) Text(text = title.value)
} }
if (shouldShowSearch.value || preferences.persistentSearch) { if (requestSearchFocus.value || preferences.persistentSearch) {
var textState by remember { mutableStateOf("") } var textState by remember { mutableStateOf("") }
val basePadding = 8.dp val basePadding = 8.dp
RoundedTextField( RoundedTextField(
@@ -120,11 +129,12 @@ private fun SearchTopBar(
end = if (hasSearchFocus.value || preferences.hideTitle) (basePadding * 3) else basePadding, end = if (hasSearchFocus.value || preferences.hideTitle) (basePadding * 3) else basePadding,
start = if (hasSearchFocus.value || preferences.hideTitle) 0.dp else basePadding start = if (hasSearchFocus.value || preferences.hideTitle) 0.dp else basePadding
) )
.height(35.dp) .height(35.dp)
.onFocusChanged { focusState -> .onFocusChanged { focusState ->
hasSearchFocus.value = focusState.isFocused hasSearchFocus.value = focusState.isFocused
}, },
requestFocus = requestSearchFocus.value,
focusRequester = focusRequester,
value = textState, value = textState,
onValueChange = { textState = it }, onValueChange = { textState = it },
placeHolder = "Search ${title.value}" placeHolder = "Search ${title.value}"
@@ -139,6 +149,12 @@ private fun SearchTopBar(
titleContentColor = MaterialTheme.colorScheme.primary titleContentColor = MaterialTheme.colorScheme.primary
) )
) )
val context = LocalContext.current
val keyboardManager = KeyboardManager.getInstance(context)
keyboardManager.attachKeyboardDismissListener {
requestSearchFocus.value = false
}
} }
@Composable @Composable

View File

@@ -1,15 +1,33 @@
package com.owenlejeune.tvtime.utils package com.owenlejeune.tvtime.utils
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
class KeyBoardManager(context: Context) { class KeyboardManager private constructor(context: Context) {
companion object {
@SuppressLint("StaticFieldLeak")
private var INSTANCE: KeyboardManager? = null
private val lock = Object()
fun getInstance(context: Context): KeyboardManager {
if (INSTANCE == null) {
synchronized(lock) {
if (INSTANCE == null) {
INSTANCE = KeyboardManager(context)
}
}
}
return INSTANCE!!
}
}
private val activity = context as Activity private val activity = context as Activity
private var keyboardDismissListener: KeyboardDismissListener? = null private var keyboardDismissListeners: MutableList<KeyboardDismissListener> = ArrayList()
private abstract class KeyboardDismissListener( private abstract class KeyboardDismissListener(
private val rootView: View, private val rootView: View,
@@ -33,17 +51,18 @@ class KeyBoardManager(context: Context) {
fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) { fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) {
val rootView = activity.findViewById<View>(android.R.id.content) val rootView = activity.findViewById<View>(android.R.id.content)
keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {} keyboardDismissListeners.add(object : KeyboardDismissListener(rootView, onKeyboardDismiss) {})
keyboardDismissListener?.let { keyboardDismissListeners.forEach {
rootView.viewTreeObserver.addOnGlobalLayoutListener(it) rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
} }
} }
fun release() { fun release() {
val rootView = activity.findViewById<View>(android.R.id.content) val rootView = activity.findViewById<View>(android.R.id.content)
keyboardDismissListener?.let { keyboardDismissListeners.forEach {
rootView.viewTreeObserver.removeOnGlobalLayoutListener(it) rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
} }
keyboardDismissListener = null keyboardDismissListeners.clear()
} }
} }