add search bar

This commit is contained in:
Owen LeJeune
2022-02-17 16:43:47 -05:00
parent c90c699a27
commit 399795cb54
7 changed files with 192 additions and 22 deletions

View File

@@ -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}"

View File

@@ -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()
}
}
} }

View File

@@ -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
)
}
}
}
} }

View File

@@ -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()
} }
} }

View File

@@ -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
)
) )
} }

View File

@@ -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(

View File

@@ -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
}
}