Skip to content

Commit

Permalink
Palette Colors API (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbel authored Feb 8, 2024
1 parent ab9cc11 commit 7cd2eba
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 21 deletions.
1 change: 1 addition & 0 deletions core/ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -27,7 +29,7 @@ fun BackIcon(
Image(
imageVector = MoviesIcons.ArrowBack,
contentDescription = stringResource(MoviesContentDescription.BackIcon),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
colorFilter = ColorFilter.tint(onContainerColor)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -52,7 +54,7 @@ fun ShareIcon(
Image(
imageVector = MoviesIcons.Share,
contentDescription = stringResource(MoviesContentDescription.ShareIcon),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer)
colorFilter = ColorFilter.tint(onContainerColor)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -36,12 +41,27 @@ class DetailsViewModel @Inject constructor(
initialValue = NetworkStatus.Unavailable
)

val currentTheme: StateFlow<AppTheme> = 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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,35 @@ 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
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
Expand All @@ -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)
) {
Expand All @@ -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,
Expand Down Expand Up @@ -132,7 +164,7 @@ fun DetailsContent(
overflow = TextOverflow.Ellipsis,
maxLines = 3,
style = MaterialTheme.typography.titleLarge.copy(
color = MaterialTheme.colorScheme.onPrimaryContainer
color = onContainerColor
)
)

Expand All @@ -155,7 +187,7 @@ fun DetailsContent(
overflow = TextOverflow.Ellipsis,
maxLines = 10,
style = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onPrimaryContainer
color = onContainerColor
)
)
}
Expand All @@ -172,7 +204,8 @@ private fun DetailsContentPreview(
.fillMaxSize()
.background(MaterialTheme.colorScheme.primaryContainer),
movie = movie,
onNavigateToGallery = {}
onNavigateToGallery = {},
onGenerateColors = {}
)
}
}
Expand All @@ -188,7 +221,8 @@ private fun DetailsContentAmoledPreview(
.fillMaxSize()
.background(MaterialTheme.colorScheme.primaryContainer),
movie = movie,
onNavigateToGallery = {}
onNavigateToGallery = {},
onGenerateColors = {}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ fun DetailsLoading(
modifier = modifier,
movie = MovieDb.Empty,
onNavigateToGallery = {},
onGenerateColors = {},
placeholder = true
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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
) {
Expand All @@ -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 = {
Expand All @@ -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 -> {
Expand All @@ -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 -> {
Expand All @@ -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()
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 7cd2eba

Please sign in to comment.