diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index bf8e75eb0..05a3cb596 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(project(":core:persistence")) api(libs.androidx.core.splashscreen) api(libs.androidx.constraintlayout.compose) + api(libs.androidx.palette.ktx) api(libs.coil.compose) api(libs.bundles.material) api(libs.bundles.accompanist) diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/BackIcon.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/BackIcon.kt index aebdc9b91..09a2830b0 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/BackIcon.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/BackIcon.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -18,7 +19,8 @@ import org.michaelbel.movies.ui.theme.MoviesTheme @Composable fun BackIcon( onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onContainerColor: Color = MaterialTheme.colorScheme.onPrimaryContainer ) { IconButton( onClick = onClick, @@ -27,7 +29,7 @@ fun BackIcon( Image( imageVector = MoviesIcons.ArrowBack, contentDescription = stringResource(MoviesContentDescription.BackIcon), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) + colorFilter = ColorFilter.tint(onContainerColor) ) } } diff --git a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/ShareIcon.kt b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/ShareIcon.kt index eb9f8f7e0..e57a78512 100644 --- a/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/ShareIcon.kt +++ b/core/ui/src/main/kotlin/org/michaelbel/movies/ui/compose/iconbutton/ShareIcon.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -28,7 +29,8 @@ import org.michaelbel.movies.ui.theme.MoviesTheme @Composable fun ShareIcon( url: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onContainerColor: Color = MaterialTheme.colorScheme.onPrimaryContainer ) { val context: Context = LocalContext.current val resultContract = rememberLauncherForActivityResult( @@ -52,7 +54,7 @@ fun ShareIcon( Image( imageVector = MoviesIcons.Share, contentDescription = stringResource(MoviesContentDescription.ShareIcon), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) + colorFilter = ColorFilter.tint(onContainerColor) ) } } diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt index 72771bcca..be417c813 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/DetailsViewModel.kt @@ -1,6 +1,10 @@ package org.michaelbel.movies.details +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle +import androidx.palette.graphics.Palette import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -10,6 +14,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.michaelbel.movies.common.ktx.require +import org.michaelbel.movies.common.theme.AppTheme import org.michaelbel.movies.common.viewmodel.BaseViewModel import org.michaelbel.movies.interactor.Interactor import org.michaelbel.movies.network.ScreenState @@ -36,12 +41,27 @@ class DetailsViewModel @Inject constructor( initialValue = NetworkStatus.Unavailable ) + val currentTheme: StateFlow = interactor.currentTheme + .stateIn( + scope = this, + started = SharingStarted.Lazily, + initialValue = AppTheme.FollowSystem + ) + + var containerColor: Int? by mutableStateOf(null) + var onContainerColor: Int? by mutableStateOf(null) + init { loadMovie() } fun retry() = loadMovie() + fun onGenerateColors(palette: Palette) { + containerColor = palette.vibrantSwatch?.rgb + onContainerColor = palette.vibrantSwatch?.bodyTextColor + } + private fun loadMovie() = launch { interactor.movieDetails(movieId).handle( success = { movieDetailsData -> diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt index e5b7ee231..90ccaf245 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsContent.kt @@ -10,13 +10,17 @@ import androidx.compose.foundation.verticalScroll 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 import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -24,14 +28,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension +import androidx.core.graphics.drawable.toBitmap +import androidx.palette.graphics.Palette +import coil.ImageLoader import coil.compose.AsyncImage import coil.request.ImageRequest +import coil.request.SuccessResult import org.michaelbel.movies.details_impl.R import org.michaelbel.movies.network.formatBackdropImage import org.michaelbel.movies.persistence.database.entity.MovieDb import org.michaelbel.movies.persistence.database.ktx.isNotEmpty import org.michaelbel.movies.ui.accessibility.MoviesContentDescription -import org.michaelbel.movies.ui.ktx.context import org.michaelbel.movies.ui.ktx.isErrorOrEmpty import org.michaelbel.movies.ui.placeholder.PlaceholderHighlight import org.michaelbel.movies.ui.placeholder.material3.fade @@ -45,12 +52,33 @@ import org.michaelbel.movies.ui.theme.MoviesTheme fun DetailsContent( movie: MovieDb, onNavigateToGallery: (Int) -> Unit, + onGenerateColors: (Palette) -> Unit, modifier: Modifier = Modifier, + isThemeAmoled: Boolean = false, + onContainerColor: Color = MaterialTheme.colorScheme.onPrimaryContainer, placeholder: Boolean = false ) { + val context = LocalContext.current val scrollState = rememberScrollState() var isNoImageVisible: Boolean by remember { mutableStateOf(false) } + if (!isThemeAmoled && !placeholder) { + LaunchedEffect(key1 = movie.backdropPath.formatBackdropImage) { + val imageRequest = ImageLoader(context).execute(ImageRequest.Builder(context) + .data(movie.backdropPath.formatBackdropImage) + .allowHardware(false) + .build()) + if (imageRequest is SuccessResult) { + val bitmap = imageRequest.drawable.toBitmap() + Palette.from(bitmap).generate { palette -> + if (palette != null) { + onGenerateColors(palette) + } + } + } + } + } + ConstraintLayout( modifier = modifier.verticalScroll(scrollState) ) { @@ -76,6 +104,10 @@ fun DetailsContent( top.linkTo(parent.top, 16.dp) end.linkTo(parent.end, 16.dp) } + .shadow( + elevation = 1.dp, + shape = MaterialTheme.shapes.small + ) .clip(MaterialTheme.shapes.small) .placeholder( visible = placeholder, @@ -132,7 +164,7 @@ fun DetailsContent( overflow = TextOverflow.Ellipsis, maxLines = 3, style = MaterialTheme.typography.titleLarge.copy( - color = MaterialTheme.colorScheme.onPrimaryContainer + color = onContainerColor ) ) @@ -155,7 +187,7 @@ fun DetailsContent( overflow = TextOverflow.Ellipsis, maxLines = 10, style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onPrimaryContainer + color = onContainerColor ) ) } @@ -172,7 +204,8 @@ private fun DetailsContentPreview( .fillMaxSize() .background(MaterialTheme.colorScheme.primaryContainer), movie = movie, - onNavigateToGallery = {} + onNavigateToGallery = {}, + onGenerateColors = {} ) } } @@ -188,7 +221,8 @@ private fun DetailsContentAmoledPreview( .fillMaxSize() .background(MaterialTheme.colorScheme.primaryContainer), movie = movie, - onNavigateToGallery = {} + onNavigateToGallery = {}, + onGenerateColors = {} ) } } \ No newline at end of file diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt index e9b04c683..671307cf3 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsLoading.kt @@ -19,6 +19,7 @@ fun DetailsLoading( modifier = modifier, movie = MovieDb.Empty, onNavigateToGallery = {}, + onGenerateColors = {}, placeholder = true ) } diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt index f7f7ede40..b508ae5b8 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsScreenContent.kt @@ -1,5 +1,8 @@ package org.michaelbel.movies.details.ui +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -12,12 +15,15 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.palette.graphics.Palette import java.net.UnknownHostException +import org.michaelbel.movies.common.theme.AppTheme import org.michaelbel.movies.details.DetailsViewModel import org.michaelbel.movies.details.ktx.movie import org.michaelbel.movies.details.ktx.movieUrl @@ -41,14 +47,21 @@ fun DetailsRoute( modifier: Modifier = Modifier, viewModel: DetailsViewModel = hiltViewModel() ) { - val detailsState: ScreenState by viewModel.detailsState.collectAsStateWithLifecycle() - val networkStatus: NetworkStatus by viewModel.networkStatus.collectAsStateWithLifecycle() + val detailsState by viewModel.detailsState.collectAsStateWithLifecycle() + val networkStatus by viewModel.networkStatus.collectAsStateWithLifecycle() + val currentTheme by viewModel.currentTheme.collectAsStateWithLifecycle() + val containerColor = viewModel.containerColor + val onContainerColor = viewModel.onContainerColor DetailsScreenContent( onBackClick = onBackClick, onNavigateToGallery = onNavigateToGallery, + onGenerateColors = viewModel::onGenerateColors, detailsState = detailsState, networkStatus = networkStatus, + containerColor = containerColor, + onContainerColor = onContainerColor, + isThemeAmoled = currentTheme is AppTheme.Amoled, onRetry = viewModel::retry, modifier = modifier ) @@ -58,8 +71,12 @@ fun DetailsRoute( private fun DetailsScreenContent( onBackClick: () -> Unit, onNavigateToGallery: (Int) -> Unit, + onGenerateColors: (Palette) -> Unit, detailsState: ScreenState, networkStatus: NetworkStatus, + containerColor: Int?, + onContainerColor: Int?, + isThemeAmoled: Boolean, onRetry: () -> Unit, modifier: Modifier = Modifier ) { @@ -69,6 +86,25 @@ private fun DetailsScreenContent( onRetry() } + val animateContainerColor = animateColorAsState( + targetValue = if (containerColor != null) Color(containerColor) else MaterialTheme.colorScheme.primaryContainer, + animationSpec = tween( + durationMillis = 300, + delayMillis = 0, + easing = LinearEasing + ), + label = "animateContainerColor" + ) + val animateOnContainerColor = animateColorAsState( + targetValue = if (onContainerColor != null) Color(onContainerColor) else MaterialTheme.colorScheme.onPrimaryContainer, + animationSpec = tween( + durationMillis = 300, + delayMillis = 0, + easing = LinearEasing + ), + label = "animateOnContainerColor" + ) + Scaffold( modifier = modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection), topBar = { @@ -77,10 +113,12 @@ private fun DetailsScreenContent( movieUrl = detailsState.movieUrl, onNavigationIconClick = onBackClick, topAppBarScrollBehavior = topAppBarScrollBehavior, + onContainerColor = animateOnContainerColor.value, + scrolledContainerColor = if (containerColor != null) Color(containerColor) else MaterialTheme.colorScheme.inversePrimary, modifier = Modifier.fillMaxWidth() ) }, - containerColor = MaterialTheme.colorScheme.primaryContainer + containerColor = animateContainerColor.value ) { innerPadding -> when (detailsState) { is ScreenState.Loading -> { @@ -98,7 +136,10 @@ private fun DetailsScreenContent( .windowInsetsPadding(displayCutoutWindowInsets) .fillMaxSize(), movie = detailsState.movie, - onNavigateToGallery = onNavigateToGallery + onContainerColor = animateOnContainerColor.value, + isThemeAmoled = isThemeAmoled, + onNavigateToGallery = onNavigateToGallery, + onGenerateColors = onGenerateColors ) } is ScreenState.Failure -> { @@ -122,8 +163,12 @@ private fun DetailsScreenContentPreview( DetailsScreenContent( onBackClick = {}, onNavigateToGallery = {}, + onGenerateColors = {}, detailsState = ScreenState.Content(movie), networkStatus = NetworkStatus.Available, + containerColor = null, + onContainerColor = null, + isThemeAmoled = false, onRetry = {}, modifier = Modifier .fillMaxSize() @@ -141,8 +186,12 @@ private fun DetailsScreenContentAmoledPreview( DetailsScreenContent( onBackClick = {}, onNavigateToGallery = {}, + onGenerateColors = {}, detailsState = ScreenState.Content(movie), networkStatus = NetworkStatus.Available, + containerColor = null, + onContainerColor = null, + isThemeAmoled = false, onRetry = {}, modifier = Modifier .fillMaxSize() diff --git a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt index f1afed1e1..96f521e94 100644 --- a/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt +++ b/feature/details-impl/src/main/kotlin/org/michaelbel/movies/details/ui/DetailsToolbar.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -28,7 +29,9 @@ fun DetailsToolbar( movieUrl: String?, onNavigationIconClick: () -> Unit, topAppBarScrollBehavior: TopAppBarScrollBehavior, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onContainerColor: Color = MaterialTheme.colorScheme.onPrimaryContainer, + scrolledContainerColor: Color = MaterialTheme.colorScheme.inversePrimary, ) { TopAppBar( title = { @@ -36,7 +39,7 @@ fun DetailsToolbar( text = movieTitle, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.titleLarge.copy( - color = MaterialTheme.colorScheme.onPrimaryContainer + color = onContainerColor ) ) }, @@ -49,7 +52,8 @@ fun DetailsToolbar( ) { if (movieUrl != null) { ShareIcon( - url = movieUrl + url = movieUrl, + onContainerColor = onContainerColor ) } } @@ -57,12 +61,13 @@ fun DetailsToolbar( navigationIcon = { BackIcon( onClick = onNavigationIconClick, - modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets) + modifier = Modifier.windowInsetsPadding(displayCutoutWindowInsets), + onContainerColor = onContainerColor ) }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - scrolledContainerColor = MaterialTheme.colorScheme.inversePrimary + containerColor = Color.Transparent, + scrolledContainerColor = scrolledContainerColor ), scrollBehavior = topAppBarScrollBehavior ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40b26b455..044f580bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,6 +46,7 @@ androidx-navigation = "2.7.6" androidx-paging = "3.2.1" androidx-datastore = "1.0.0" androidx-startup = "1.1.1" +androidx-palette-ktx = "1.0.0" androidx-room = "2.6.1" androidx-test = "1.5.2" androidx-test-ext = "1.1.5" @@ -121,6 +122,7 @@ androidx-datastore-core = { module = "androidx.datastore:datastore-core", versio androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidx-datastore" } androidx-datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "androidx-datastore" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } +androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "androidx-palette-ktx" } androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" } androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } diff --git a/instant/build.gradle.kts b/instant/build.gradle.kts index da10a3ced..7ee1713c7 100644 --- a/instant/build.gradle.kts +++ b/instant/build.gradle.kts @@ -17,12 +17,12 @@ android { } productFlavors { - create("foss") { + /*create("foss") { dimension = "version" } create("hms") { dimension = "version" - } + }*/ create("gms") { dimension = "version" } diff --git a/readme.md b/readme.md index b882dc042..f1e80bafc 100644 --- a/readme.md +++ b/readme.md @@ -54,6 +54,7 @@ TMDB_API_KEY=your_own_tmdb_api_key - [x] Amoled Theme - [x] [Material You Dynamic Colors](https://d.android.com/develop/ui/views/theming/dynamic-colors) - [x] [Themed App Icon](https://d.android.com/develop/ui/views/launch/icon_design_adaptive) +- [x] [Palette Colors API](https://d.android.com/develop/ui/views/graphics/palette-colors) - [x] [Kotlin](https://d.android.com/kotlin) - [x] [Jetpack Compose](https://d.android.com/jetpack/compose) - [x] [Accompanist](https://github.com/google/accompanist)