Skip to content

Commit

Permalink
Feat: headline background.
Browse files Browse the repository at this point in the history
  • Loading branch information
oxyroid committed Jun 1, 2024
1 parent 75c8268 commit 46a0636
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 14 deletions.
5 changes: 4 additions & 1 deletion androidApp/src/main/java/com/m3u/androidApp/ui/Scaffold.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMap
import androidx.compose.ui.util.fastMaxOfOrNull
import com.m3u.androidApp.ui.internal.HeadlineBackground
import com.m3u.androidApp.ui.internal.SmartphoneScaffoldImpl
import com.m3u.androidApp.ui.internal.TabletScaffoldImpl
import com.m3u.androidApp.ui.internal.TelevisionScaffoldImpl
Expand Down Expand Up @@ -143,6 +144,7 @@ internal fun MainContent(
val title = Metadata.title
val subtitle = Metadata.subtitle
val actions = Metadata.actions

Scaffold(
topBar = {
if (!tv) {
Expand Down Expand Up @@ -218,10 +220,11 @@ internal fun MainContent(
}
},
contentWindowInsets = windowInsets,
containerColor = MaterialTheme.colorScheme.background
containerColor = Color.Transparent
) { padding ->
Background {
Box {
HeadlineBackground()
content(padding)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.m3u.androidApp.ui.internal

import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.material.ktx.BlurTransformation
import com.m3u.material.ktx.Edge
import com.m3u.material.ktx.blurEdge
import com.m3u.ui.helper.Metadata
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.math.roundToInt

@Composable
fun HeadlineBackground(modifier: Modifier = Modifier) {
val context = LocalContext.current
val configuration = LocalConfiguration.current

val preferences = hiltPreferences()

val useDarkTheme =
preferences.darkMode || (preferences.followSystemTheme && isSystemInDarkTheme())

val colorScheme = MaterialTheme.colorScheme
val url = Metadata.headlineUrl
val fraction = Metadata.headlineFraction

val currentMaskColor by animateColorAsState(
targetValue = lerp(
start = if (useDarkTheme) Color.Black.copy(0.56f)
else Color.White.copy(0.56f),
stop = Color.Transparent,
fraction = fraction
),
label = "scaffold-main-content-mask-color"
)

if (!preferences.noPictureMode) {
LaunchedEffect(Unit) {
snapshotFlow { Metadata.headlineFraction }
.onEach { Log.e("TAG", "$it") }
.launchIn(this)
}
AsyncImage(
model = remember(url) {
ImageRequest.Builder(context)
.data(url)
.crossfade(800)
.memoryCachePolicy(CachePolicy.DISABLED)
.transformations(
BlurTransformation(context)
)
.build()
},
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = modifier
.fillMaxWidth()
.offset {
IntOffset(
x = 0,
y = ((configuration.screenWidthDp * Metadata.HEADLINE_ASPECT_RATIO) * -fraction).roundToInt()
)
}
.aspectRatio(Metadata.HEADLINE_ASPECT_RATIO)
.drawWithContent {
drawContent()
if (url.isNotEmpty()) {
drawRect(color = currentMaskColor, size = size)
}
}
.blurEdge(
color = colorScheme.background,
edge = Edge.Bottom,
dimen = 256f
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand All @@ -25,8 +26,11 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.util.basic.title
import com.m3u.core.wrapper.Resource
Expand All @@ -43,7 +47,6 @@ import com.m3u.i18n.R.string
import com.m3u.material.ktx.interceptVolumeEvent
import com.m3u.material.ktx.isTelevision
import com.m3u.material.ktx.thenIf
import com.m3u.material.model.LocalHazeState
import com.m3u.ui.Destination
import com.m3u.ui.EpisodesBottomSheet
import com.m3u.ui.LocalRootDestination
Expand All @@ -52,9 +55,9 @@ import com.m3u.ui.MediaSheetValue
import com.m3u.ui.helper.Action
import com.m3u.ui.helper.LocalHelper
import com.m3u.ui.helper.Metadata
import dev.chrisbanes.haze.HazeDefaults
import dev.chrisbanes.haze.haze
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds

@Composable
fun ForyouRoute(
Expand Down Expand Up @@ -99,6 +102,7 @@ fun ForyouRoute(
)
onPauseOrDispose {
Metadata.actions = emptyList()
Metadata.headlineUrl = ""
}
}
}
Expand Down Expand Up @@ -182,6 +186,9 @@ private fun ForyouScreen(
modifier: Modifier = Modifier
) {
val configuration = LocalConfiguration.current
val lifecycleOwner = LocalLifecycleOwner.current

var headlineSpec: Recommend.Spec? by remember { mutableStateOf(null) }

val actualRowCount = remember(rowCount, configuration.orientation) {
when (configuration.orientation) {
Expand All @@ -192,6 +199,19 @@ private fun ForyouScreen(
var mediaSheetValue: MediaSheetValue.ForyouScreen by remember {
mutableStateOf(MediaSheetValue.ForyouScreen())
}

LaunchedEffect(headlineSpec) {
val currentHeadlineSpec = headlineSpec
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
delay(400.milliseconds)
Metadata.headlineUrl = when (currentHeadlineSpec) {
is Recommend.UnseenSpec -> currentHeadlineSpec.stream.cover.orEmpty()
is Recommend.DiscoverSpec -> ""
else -> ""
}
}
}

Box(modifier) {
when (playlistCountsResource) {
Resource.Loading -> {
Expand All @@ -209,6 +229,7 @@ private fun ForyouScreen(
recommend = recommend,
navigateToPlaylist = navigateToPlaylist,
onClickStream = onClickStream,
onSpecChanged = { spec -> headlineSpec = spec },
modifier = Modifier.fillMaxWidth()
)
}
Expand All @@ -222,12 +243,7 @@ private fun ForyouScreen(
onLongClick = { mediaSheetValue = MediaSheetValue.ForyouScreen(it) },
header = header.takeIf { recommend.isNotEmpty() },
contentPadding = contentPadding,
modifier = Modifier
.fillMaxSize()
.haze(
LocalHazeState.current,
HazeDefaults.style(MaterialTheme.colorScheme.surface)
)
modifier = Modifier.fillMaxSize()
)
} else {
Box(Modifier.fillMaxSize()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.m3u.features.foryou.components

import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -8,8 +9,18 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
Expand All @@ -21,7 +32,14 @@ import com.m3u.data.database.model.fromLocal
import com.m3u.data.database.model.type
import com.m3u.i18n.R.string
import com.m3u.material.ktx.plus
import com.m3u.material.model.LocalHazeState
import com.m3u.material.model.LocalSpacing
import com.m3u.ui.helper.Metadata
import dev.chrisbanes.haze.HazeDefaults
import dev.chrisbanes.haze.haze
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.math.absoluteValue

@Composable
internal fun PlaylistGallery(
Expand All @@ -36,12 +54,42 @@ internal fun PlaylistGallery(
header: (@Composable () -> Unit)? = null
) {
val spacing = LocalSpacing.current
val configuration = LocalConfiguration.current
val colorScheme = MaterialTheme.colorScheme

val state = rememberLazyGridState()
val viewportStartOffset by remember {
derivedStateOf { state.layoutInfo.visibleItemsInfo.firstOrNull()?.offset?.y ?: 0 }
}
val currentHazeColor by animateColorAsState(
targetValue = lerp(
start = Color.Transparent,
stop = colorScheme.surface,
fraction = Metadata.headlineFraction
),
label = "playlist-gallery-haze-color"
)
LaunchedEffect(configuration.screenWidthDp) {
snapshotFlow { viewportStartOffset }
.onEach {
val fraction = (it.absoluteValue /
(configuration.screenWidthDp * Metadata.HEADLINE_ASPECT_RATIO))
.coerceIn(0f, 1f)
Metadata.headlineFraction = fraction
}
.launchIn(this)
}
LazyVerticalGrid(
state = state,
columns = GridCells.Fixed(rowCount),
contentPadding = PaddingValues(vertical = spacing.medium) + contentPadding,
verticalArrangement = Arrangement.spacedBy(spacing.medium),
horizontalArrangement = Arrangement.spacedBy(spacing.medium),
modifier = modifier
.haze(
LocalHazeState.current,
HazeDefaults.style(currentHazeColor)
)
) {
if (header != null) {
item(span = { GridItemSpan(rowCount) }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal class Recommend(
override fun get(index: Int): Spec = specs[index]

@Immutable
interface Spec
sealed interface Spec

@Immutable
data class DiscoverSpec(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.m3u.features.foryou.components.recommend

import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
Expand All @@ -15,10 +14,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.tv.material3.Carousel as TvCarousel
import com.m3u.core.wrapper.eventOf
import com.m3u.data.database.model.Playlist
import com.m3u.data.database.model.Stream
Expand All @@ -28,26 +28,36 @@ import com.m3u.material.model.LocalSpacing
import com.m3u.ui.Events
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import androidx.tv.material3.Carousel as TvCarousel

@OptIn(ExperimentalAnimationApi::class)
@Composable
internal fun RecommendGallery(
recommend: Recommend,
onClickStream: (Stream) -> Unit,
navigateToPlaylist: (Playlist) -> Unit,
onSpecChanged: (Recommend.Spec?) -> Unit,
modifier: Modifier = Modifier
) {
val spacing = LocalSpacing.current
val coroutineScope = rememberCoroutineScope()

val tv = isTelevision()

DisposableEffect(Unit) {
onDispose {
onSpecChanged(null)
}
}

if (!tv) {
val state = rememberPagerState { recommend.size }
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(spacing.medium)
) {
LaunchedEffect(state.currentPage) {
onSpecChanged(recommend[state.currentPage])
}
HorizontalPager(
state = state,
contentPadding = PaddingValues(horizontal = spacing.medium),
Expand Down
Loading

0 comments on commit 46a0636

Please sign in to comment.