redesign detail views titles

This commit is contained in:
Owen LeJeune
2023-07-16 17:27:03 -04:00
parent 7fb31cb041
commit 174b2eb7fa
4 changed files with 132 additions and 15 deletions

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.extensions package com.owenlejeune.tvtime.extensions
import android.view.View
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
@@ -7,7 +8,11 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@@ -16,6 +21,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
fun Modifier.shimmerBackground( fun Modifier.shimmerBackground(
shape: Shape = RectangleShape, shape: Shape = RectangleShape,
@@ -42,3 +51,39 @@ fun Modifier.shimmerBackground(
) )
return@composed this.then(background(brush, shape)) return@composed this.then(background(brush, shape))
} }
fun Modifier.combinedOnVisibilityChange(
onVisible: () -> Unit,
onNotVisible: () -> Unit
): Modifier = composed {
val view = LocalView.current
var isVisible by remember { mutableStateOf<Boolean?>(null) }
LaunchedEffect(isVisible) {
when (isVisible) {
true -> onVisible()
false -> onNotVisible()
else -> {}
}
}
onGloballyPositioned { coordinates ->
isVisible = coordinates.isCompletelyVisible(view)
}
}
fun LayoutCoordinates.isCompletelyVisible(view: View): Boolean {
if (!isAttached) return false
// Window relative bounds of our compose root view that are visible on the screen
val globalRootRect = android.graphics.Rect()
if (!view.getGlobalVisibleRect(globalRootRect)) {
// we aren't visible at all.
return false
}
val bounds = boundsInWindow()
// Make sure we are completely in bounds.
return bounds.top >= globalRootRect.top &&
bounds.left >= globalRootRect.left &&
bounds.right <= globalRootRect.right &&
bounds.bottom <= globalRootRect.bottom
}

View File

@@ -87,7 +87,7 @@ fun DetailHeader(
Row( Row(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding(start = 16.dp), .padding(start = 16.dp, top = 16.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp), horizontalArrangement = Arrangement.spacedBy(20.dp),
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
@@ -119,7 +119,7 @@ private fun BackdropContainer(
val gradient = Brush.verticalGradient( val gradient = Brush.verticalGradient(
colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background), colors = listOf(Color.Transparent, MaterialTheme.colorScheme.background),
startY = sizeImage.value.height.toFloat() / 3, startY = 0f,//sizeImage.value.height.toFloat() / 4,
endY = sizeImage.value.height.toFloat() endY = sizeImage.value.height.toFloat()
) )
@@ -130,8 +130,8 @@ private fun BackdropContainer(
Box( Box(
modifier = Modifier modifier = Modifier
.width(sizeImage.value.width.toDp()) .width(sizeImage.value.width.toDp() + 2.dp)
.height(sizeImage.value.height.toDp()) .height(sizeImage.value.height.toDp() + 4.dp)
.background(gradient) .background(gradient)
) )
} }
@@ -187,7 +187,10 @@ fun BackdropGallery(
HorizontalPager( HorizontalPager(
count = imageCollection.backdrops.size, count = imageCollection.backdrops.size,
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onGloballyPositioned { sizeImage.value = it.size },
) { page -> ) { page ->
val backdrop = imageCollection.backdrops[page] val backdrop = imageCollection.backdrops[page]
val url = TmdbUtils.getFullBackdropPath(backdrop) val url = TmdbUtils.getFullBackdropPath(backdrop)
@@ -201,7 +204,6 @@ fun BackdropGallery(
model = model, model = model,
placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder), placeholder = rememberAsyncImagePainter(model = R.drawable.placeholder),
contentDescription = "", contentDescription = "",
modifier = Modifier.onGloballyPositioned { sizeImage.value = it.size },
contentScale = ContentScale.FillWidth contentScale = ContentScale.FillWidth
) )
} }

View File

@@ -4,7 +4,10 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -87,6 +90,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviderDetails
import com.owenlejeune.tvtime.extensions.DateFormat import com.owenlejeune.tvtime.extensions.DateFormat
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange
import com.owenlejeune.tvtime.extensions.format import com.owenlejeune.tvtime.extensions.format
import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.isIn import com.owenlejeune.tvtime.extensions.isIn
@@ -119,6 +123,7 @@ import com.owenlejeune.tvtime.ui.components.RoundedTextField
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.theme.Typography
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel import com.owenlejeune.tvtime.ui.viewmodel.SpecialFeaturesViewModel
@@ -208,6 +213,7 @@ fun MediaDetailScreen(
} }
) )
val titleViewHidden = remember { mutableStateOf(false) }
Box( Box(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -217,7 +223,15 @@ fun MediaDetailScreen(
topBar = { topBar = {
TVTTopAppBar( TVTTopAppBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
title = { Text(text = mediaItem?.title ?: "") }, title = {
AnimatedVisibility(
visible = titleViewHidden.value,
enter = fadeIn(),
exit = fadeOut()
) {
Text(text = mediaItem?.title ?: "")
}
},
appNavController = appNavController, appNavController = appNavController,
navigationIcon = { navigationIcon = {
BackButton(navController = appNavController) BackButton(navController = appNavController)
@@ -238,7 +252,8 @@ fun MediaDetailScreen(
windowSize = windowSize, windowSize = windowSize,
showImageGallery = showGalleryOverlay, showImageGallery = showGalleryOverlay,
pagerState = pagerState, pagerState = pagerState,
mainViewModel = mainViewModel mainViewModel = mainViewModel,
titleViewHidden = titleViewHidden
) )
PullRefreshIndicator( PullRefreshIndicator(
refreshing = isRefreshing.value, refreshing = isRefreshing.value,
@@ -272,6 +287,7 @@ fun MediaViewContent(
windowSize: WindowSizeClass, windowSize: WindowSizeClass,
showImageGallery: MutableState<Boolean>, showImageGallery: MutableState<Boolean>,
pagerState: PagerState, pagerState: PagerState,
titleViewHidden: MutableState<Boolean>,
preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java) preferences: AppPreferences = KoinJavaComponent.get(AppPreferences::class.java)
) { ) {
Row( Row(
@@ -310,7 +326,8 @@ fun MediaViewContent(
type = type, type = type,
mediaItem = mediaItem, mediaItem = mediaItem,
mainViewModel = mainViewModel, mainViewModel = mainViewModel,
itemId = itemId itemId = itemId,
titleViewHidden = titleViewHidden
) )
ExternalIdsArea( ExternalIdsArea(
@@ -404,6 +421,7 @@ fun MediaViewContent(
private fun MiscTvDetails( private fun MiscTvDetails(
itemId: Int, itemId: Int,
mediaItem: DetailedItem?, mediaItem: DetailedItem?,
titleViewHidden: MutableState<Boolean>,
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
mediaItem?.let { tv -> mediaItem?.let { tv ->
@@ -421,7 +439,9 @@ private fun MiscTvDetails(
runtime = TmdbUtils.convertRuntimeToHoursMinutes(series), runtime = TmdbUtils.convertRuntimeToHoursMinutes(series),
genres = series.genres, genres = series.genres,
contentRating = contentRating, contentRating = contentRating,
type = MediaViewType.TV type = MediaViewType.TV,
title = mediaItem.title,
titleViewHidden = titleViewHidden
) )
} }
} }
@@ -430,6 +450,7 @@ private fun MiscTvDetails(
private fun MiscMovieDetails( private fun MiscMovieDetails(
itemId: Int, itemId: Int,
mediaItem: DetailedItem?, mediaItem: DetailedItem?,
titleViewHidden: MutableState<Boolean>,
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
val movie = mediaItem as? DetailedMovie val movie = mediaItem as? DetailedMovie
@@ -446,18 +467,22 @@ private fun MiscMovieDetails(
runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie), runtime = TmdbUtils.convertRuntimeToHoursMinutes(movie),
genres = movie?.genres ?: emptyList(), genres = movie?.genres ?: emptyList(),
contentRating = contentRating, contentRating = contentRating,
type = MediaViewType.MOVIE type = MediaViewType.MOVIE,
title = mediaItem?.title ?: "",
titleViewHidden = titleViewHidden
) )
} }
@Composable @Composable
private fun MiscDetails( private fun MiscDetails(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String,
year: String, year: String,
runtime: String, runtime: String,
genres: List<Genre>, genres: List<Genre>,
contentRating: String, contentRating: String,
type: MediaViewType type: MediaViewType,
titleViewHidden: MutableState<Boolean>,
) { ) {
val mainViewModel = viewModel<MainViewModel>() val mainViewModel = viewModel<MainViewModel>()
val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) } val detailsLoadingState = remember { mainViewModel.produceDetailsLoadingStateFor(type) }
@@ -473,6 +498,20 @@ private fun MiscDetails(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text(
text = title.takeUnless { isLoading } ?: "",
color = MaterialTheme.colorScheme.secondary,
style = Typography.headlineLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = shimmerModifier
.width(400.dp)
.combinedOnVisibilityChange(
onVisible = { titleViewHidden.value = false },
onNotVisible = { titleViewHidden.value = true }
)
)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -1434,19 +1473,22 @@ fun DetailsFor(
type: MediaViewType, type: MediaViewType,
itemId: Int, itemId: Int,
mediaItem: DetailedItem?, mediaItem: DetailedItem?,
titleViewHidden: MutableState<Boolean>,
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
if (type == MediaViewType.MOVIE) { if (type == MediaViewType.MOVIE) {
MiscMovieDetails( MiscMovieDetails(
itemId = itemId, itemId = itemId,
mediaItem = mediaItem, mediaItem = mediaItem,
mainViewModel = mainViewModel mainViewModel = mainViewModel,
titleViewHidden = titleViewHidden
) )
} else { } else {
MiscTvDetails( MiscTvDetails(
itemId = itemId, itemId = itemId,
mediaItem = mediaItem, mediaItem = mediaItem,
mainViewModel = mainViewModel mainViewModel = mainViewModel,
titleViewHidden = titleViewHidden
) )
} }
} }

View File

@@ -1,5 +1,8 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -43,6 +46,7 @@ import androidx.navigation.NavController
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailPerson
import com.owenlejeune.tvtime.extensions.combinedOnVisibilityChange
import com.owenlejeune.tvtime.ui.components.BackButton import com.owenlejeune.tvtime.ui.components.BackButton
import com.owenlejeune.tvtime.ui.components.ContentCard import com.owenlejeune.tvtime.ui.components.ContentCard
import com.owenlejeune.tvtime.ui.components.DetailHeader import com.owenlejeune.tvtime.ui.components.DetailHeader
@@ -52,6 +56,7 @@ import com.owenlejeune.tvtime.ui.components.PosterItem
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.theme.Typography
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.TmdbUtils
@@ -105,12 +110,21 @@ fun PersonDetailScreen(
} }
) )
val titleViewHidden = remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
TVTTopAppBar( TVTTopAppBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
title = { Text(text = person?.name ?: "") }, title = {
AnimatedVisibility(
visible = titleViewHidden.value,
enter = fadeIn(),
exit = fadeOut()
) {
Text(text = person?.name ?: "")
}
},
appNavController = appNavController, appNavController = appNavController,
navigationIcon = { navigationIcon = {
BackButton(navController = appNavController) BackButton(navController = appNavController)
@@ -135,6 +149,20 @@ fun PersonDetailScreen(
elevation = 0.dp elevation = 0.dp
) )
Text(
text = person?.name ?: "",
color = MaterialTheme.colorScheme.secondary,
style = Typography.headlineLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.combinedOnVisibilityChange(
onVisible = { titleViewHidden.value = false },
onNotVisible = { titleViewHidden.value = true }
)
)
ExternalIdsArea( ExternalIdsArea(
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
type = MediaViewType.PERSON, type = MediaViewType.PERSON,