mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-16 08:40:53 -05:00
add search bar
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import com.owenlejeune.tvtime.buildsrc.Config
|
import com.owenlejeune.tvtime.buildsrc.*
|
||||||
import com.owenlejeune.tvtime.buildsrc.Versions
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
@@ -84,6 +83,8 @@ dependencies {
|
|||||||
//Coroutines
|
//Coroutines
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
|
||||||
|
|
||||||
|
implementation "me.onebone:toolbar-compose:2.3.1"
|
||||||
|
|
||||||
testImplementation "junit:junit:${Versions.junit}"
|
testImplementation "junit:junit:${Versions.junit}"
|
||||||
androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}"
|
androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}"
|
androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}"
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.navigation.NavHostController
|
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
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// private val appNavControllerProvider: (@Composable () -> NavHostController) by inject(named(NavControllers.APP))
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
|
AppKeyboardFocusManager()
|
||||||
val displayUnderStatusBar = remember { mutableStateOf(false) }
|
val displayUnderStatusBar = remember { mutableStateOf(false) }
|
||||||
// WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value)
|
// WindowCompat.setDecorFitsSystemWindows(window, !displayUnderStatusBar.value)
|
||||||
// val statusBarColor = if (displayUnderStatusBar.value) {
|
// val statusBarColor = if (displayUnderStatusBar.value) {
|
||||||
@@ -53,4 +53,19 @@ fun MyApp(
|
|||||||
@Composable
|
@Composable
|
||||||
fun MyAppPreview() {
|
fun MyAppPreview() {
|
||||||
MyApp()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,15 @@ package com.owenlejeune.tvtime.ui.components
|
|||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
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.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.Card
|
||||||
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
|
||||||
@@ -23,6 +26,7 @@ import androidx.compose.ui.draw.scale
|
|||||||
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
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -294,8 +298,57 @@ fun RatingRing(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RatingRingPreview() {
|
fun RoundedTextField(
|
||||||
RatingRing(progress = 0.5f)
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,19 +48,24 @@ fun MainNavigationRoutes(navController: NavHostController, displayUnderStatusBar
|
|||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationRoutes(
|
fun BottomNavigationRoutes(
|
||||||
appNavController: NavHostController,
|
appNavController: NavHostController,
|
||||||
navController: NavHostController
|
navController: NavHostController,
|
||||||
|
shouldShowSearch: MutableState<Boolean>
|
||||||
) {
|
) {
|
||||||
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
|
NavHost(navController = navController, startDestination = BottomNavItem.Movies.route) {
|
||||||
composable(BottomNavItem.Movies.route) {
|
composable(BottomNavItem.Movies.route) {
|
||||||
|
shouldShowSearch.value = true
|
||||||
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)
|
MediaTab(appNavController = appNavController, mediaType = MediaViewType.MOVIE)
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.TV.route) {
|
composable(BottomNavItem.TV.route) {
|
||||||
|
shouldShowSearch.value = true
|
||||||
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
MediaTab(appNavController = appNavController, mediaType = MediaViewType.TV)
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Favourites.route) {
|
composable(BottomNavItem.Favourites.route) {
|
||||||
|
shouldShowSearch.value = false
|
||||||
FavouritesTab()
|
FavouritesTab()
|
||||||
}
|
}
|
||||||
composable(BottomNavItem.Settings.route) {
|
composable(BottomNavItem.Settings.route) {
|
||||||
|
shouldShowSearch.value = false
|
||||||
SettingsTab()
|
SettingsTab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
import androidx.compose.animation.rememberSplineBasedDecay
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
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.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
|
||||||
@@ -31,6 +34,8 @@ fun MainAppView(appNavController: NavHostController) {
|
|||||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec)
|
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(decayAnimationSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val shouldShowSearch = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
// todo - scroll state not remember when returing from detail screen
|
// todo - scroll state not remember when returing from detail screen
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -45,7 +50,8 @@ fun MainAppView(appNavController: NavHostController) {
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
title = appBarTitle,
|
title = appBarTitle,
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior,
|
||||||
|
shouldShowSearch = shouldShowSearch
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
@@ -55,16 +61,53 @@ fun MainAppView(appNavController: NavHostController) {
|
|||||||
}
|
}
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Box(modifier = Modifier.padding(innerPadding)) {
|
Box(modifier = Modifier.padding(innerPadding)) {
|
||||||
BottomNavigationRoutes(appNavController = appNavController, navController = navController)
|
BottomNavigationRoutes(appNavController = appNavController, navController = navController, shouldShowSearch = shouldShowSearch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(title: MutableState<String>, scrollBehavior: TopAppBarScrollBehavior) {
|
private fun TopBar(
|
||||||
LargeTopAppBar(
|
title: MutableState<String>,
|
||||||
title = { Text(text = title.value) },
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
scrollBehavior = scrollBehavior
|
hasSearchFocus: MutableState<Boolean> = remember { mutableStateOf(false) },
|
||||||
|
shouldShowSearch: MutableState<Boolean> = 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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ fun SettingsTab(preferences: AppPreferences = get(AppPreferences::class.java)) {
|
|||||||
val shouldShowPalette = remember { mutableStateOf(false) }
|
val shouldShowPalette = remember { mutableStateOf(false) }
|
||||||
Text(
|
Text(
|
||||||
text = "Show material palette",
|
text = "Show material palette",
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = if (usePreferences.value) {
|
||||||
|
MaterialTheme.colorScheme.onBackground
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|||||||
@@ -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<View>(android.R.id.content)
|
||||||
|
keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {}
|
||||||
|
keyboardDismissListener?.let {
|
||||||
|
rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
val rootView = activity.findViewById<View>(android.R.id.content)
|
||||||
|
keyboardDismissListener?.let {
|
||||||
|
rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
|
||||||
|
}
|
||||||
|
keyboardDismissListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user