mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-15 08:12:45 -05:00
show/hide search bar with fab and settings
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user