Skip to content

Commit

Permalink
Replace ImmersiveList with custom implementation as ImmersiveList is …
Browse files Browse the repository at this point in the history
…deprecated
  • Loading branch information
chikoski committed May 15, 2024
1 parent fb06609 commit eb3563b
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
Expand All @@ -16,13 +16,17 @@

package com.google.jetstream.presentation.screens.home

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
Expand All @@ -31,116 +35,125 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.ImmersiveList
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.jetstream.R
import com.google.jetstream.data.entities.Movie
import com.google.jetstream.data.entities.MovieList
import com.google.jetstream.presentation.common.ImmersiveListMoviesRow
import com.google.jetstream.presentation.common.ItemDirection
import com.google.jetstream.presentation.common.PosterImage
import com.google.jetstream.presentation.screens.dashboard.rememberChildPadding
import com.google.jetstream.presentation.utils.bringIntoViewIfChildrenAreFocused

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun Top10MoviesList(
movieList: MovieList,
modifier: Modifier = Modifier,
moviesState: List<Movie>,
gradientColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
onMovieClick: (movie: Movie) -> Unit
) {
var currentItemIndex by remember { mutableStateOf(0) }
var isListFocused by remember { mutableStateOf(false) }
var currentYCoord: Float? by remember { mutableStateOf(null) }
var selectedMovie by remember(movieList) { mutableStateOf(movieList.first()) }

ImmersiveList(
modifier = modifier.onGloballyPositioned { currentYCoord = it.positionInWindow().y },
background = { _, listHasFocus ->
isListFocused = listHasFocus
val gradientColor = MaterialTheme.colorScheme.surface
AnimatedVisibility(
visible = isListFocused,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
modifier = Modifier
.height(432.dp)
.gradientOverlay(gradientColor)
) {
val movie = remember(moviesState, currentItemIndex) {
moviesState[currentItemIndex]
}
val sectionTitle = if (isListFocused) {
null
} else {
stringResource(R.string.top_10_movies_title)
}

Crossfade(
targetState = movie.posterUri,
label = "posterUriCrossfade"
) { posterUri ->
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.height(432.dp),
model = ImageRequest.Builder(LocalContext.current)
.data(posterUri)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop
Box(
contentAlignment = Alignment.BottomStart,
modifier = modifier
.bringIntoViewIfChildrenAreFocused(paddingValues = PaddingValues(bottom = 96.dp))
) {
Background(
movie = selectedMovie,
visible = isListFocused,
modifier = modifier
.height(432.dp)
.gradientOverlay(gradientColor)
)
Column {
if (isListFocused) {
MovieDescription(
movie = selectedMovie,
modifier = Modifier.padding(
start = rememberChildPadding().start,
bottom = 32.dp
)
}

}
},
list = {
Column {
// TODO this causes the whole vertical list to jump
if (isListFocused) {
val movie = remember(moviesState, currentItemIndex) {
moviesState[currentItemIndex]
}
Column(
modifier = Modifier.padding(
start = rememberChildPadding().start,
bottom = 32.dp
)
) {
Text(text = movie.name, style = MaterialTheme.typography.displaySmall)
Spacer(modifier = Modifier.padding(top = 8.dp))
Text(
modifier = Modifier.fillMaxWidth(0.5f),
text = movie.description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
fontWeight = FontWeight.Light
)
}
}
ImmersiveListMoviesRow(
itemDirection = ItemDirection.Horizontal,
movies = moviesState,
title = if (isListFocused) null
else stringResource(R.string.top_10_movies_title),
showItemTitle = !isListFocused,
onMovieClick = onMovieClick,
showIndexOverImage = true,
focusedItemIndex = { focusedIndex -> currentItemIndex = focusedIndex }
)
}

ImmersiveListMoviesRow(
movieList = movieList,
itemDirection = ItemDirection.Horizontal,
title = sectionTitle,
showItemTitle = !isListFocused,
showIndexOverImage = true,
onMovieSelected = onMovieClick,
onMovieFocused = { selectedMovie = it },
modifier = Modifier.onFocusChanged {
isListFocused = it.hasFocus
}
)
}
)
}
}

@Composable
private fun Background(
movie: Movie,
visible: Boolean,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
modifier = modifier
) {
Crossfade(
targetState = movie,
label = "posterUriCrossfade",

) {
PosterImage(movie = it, modifier = Modifier.fillMaxSize())
}
}
}

@Composable
private fun MovieDescription(
movie: Movie,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = movie.name, style = MaterialTheme.typography.displaySmall)
Text(
modifier = Modifier.fillMaxWidth(0.5f),
text = movie.description,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
fontWeight = FontWeight.Light
)
}
}

fun Modifier.gradientOverlay(gradientColor: Color) = this then drawWithCache {
private fun Modifier.gradientOverlay(gradientColor: Color) = this then drawWithCache {
val horizontalGradient = Brush.horizontalGradient(
colors = listOf(
gradientColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,50 @@
package com.google.jetstream.presentation.utils

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.relocation.BringIntoViewResponder
import androidx.compose.foundation.relocation.bringIntoViewResponder
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.toSize

@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
@OptIn(ExperimentalFoundationApi::class)
internal fun Modifier.bringIntoViewIfChildrenAreFocused(): Modifier = composed(
internal fun Modifier.bringIntoViewIfChildrenAreFocused(
paddingValues: PaddingValues = PaddingValues()
): Modifier = composed(
inspectorInfo = debugInspectorInfo { name = "bringIntoViewIfChildrenAreFocused" },
factory = {
val pxOffset = with(LocalDensity.current) {
val y = (paddingValues.calculateBottomPadding() - paddingValues.calculateTopPadding())
.toPx()
Offset.Zero.copy(y = y)
}
var myRect: Rect = Rect.Zero
val responder = object : BringIntoViewResponder {
// return the current rectangle and ignoring the child rectangle received.
@ExperimentalFoundationApi
override fun calculateRectForParent(localRect: Rect): Rect {
return myRect
}

// The container is not expected to be scrollable. Hence the child is
// already in view with respect to the container.
@ExperimentalFoundationApi
override suspend fun bringChildIntoView(localRect: () -> Rect?) {
}
}

this
.onSizeChanged {
myRect = Rect(Offset.Zero, Offset(it.width.toFloat(), it.height.toFloat()))
val size = it.toSize()
myRect = Rect(pxOffset, size)
}
.bringIntoViewResponder(
remember {
object : BringIntoViewResponder {
// return the current rectangle and ignoring the child rectangle received.
@ExperimentalFoundationApi
override fun calculateRectForParent(localRect: Rect): Rect = myRect

// The container is not expected to be scrollable. Hence the child is
// already in view with respect to the container.
@ExperimentalFoundationApi
override suspend fun bringChildIntoView(localRect: () -> Rect?) {}
}
}
)
.bringIntoViewResponder(responder)
}
)

0 comments on commit eb3563b

Please sign in to comment.