diff --git a/app/src/main/java/com/imashnake/animite/features/home/HomeScreen.kt b/app/src/main/java/com/imashnake/animite/features/home/HomeScreen.kt
index cde0f838..66b7857e 100644
--- a/app/src/main/java/com/imashnake/animite/features/home/HomeScreen.kt
+++ b/app/src/main/java/com/imashnake/animite/features/home/HomeScreen.kt
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -61,6 +60,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.imashnake.animite.R
import com.imashnake.animite.core.ui.LocalPaddings
+import com.imashnake.animite.core.ui.layouts.BannerLayout
@Destination
@Composable
@@ -87,88 +87,72 @@ fun HomeScreen(
when {
rows.all { it.first is Resource.Success } -> {
val scrollState = rememberScrollState()
- TranslucentStatusBarLayout(
- scrollState = scrollState,
- distanceUntilAnimated = dimensionResource(R.dimen.banner_height)
- ) {
+ TranslucentStatusBarLayout(scrollState) {
Box(
modifier = Modifier
.verticalScroll(scrollState)
.navigationBarsPadding()
) {
- Box {
- Image(
- painter = painterResource(R.drawable.background),
- contentDescription = null,
- modifier = Modifier
- .fillMaxWidth()
- .height(dimensionResource(R.dimen.banner_height))
- .bannerParallax(scrollState),
- contentScale = ContentScale.Crop,
- alignment = Alignment.TopCenter
- )
+ BannerLayout(
+ banner = { bannerModifier ->
+ Box {
+ Image(
+ painter = painterResource(R.drawable.background),
+ contentDescription = null,
+ modifier = bannerModifier.bannerParallax(scrollState),
+ contentScale = ContentScale.Crop,
+ alignment = Alignment.TopCenter
+ )
- Box(
- modifier = Modifier
- .background(
- Brush.verticalGradient(
- listOf(
- Color.Transparent,
- MaterialTheme.colorScheme.secondaryContainer.copy(
- alpha = 0.5f
+ Box(
+ modifier = bannerModifier
+ .background(
+ Brush.verticalGradient(
+ listOf(
+ Color.Transparent,
+ MaterialTheme.colorScheme.secondaryContainer.copy(
+ alpha = 0.5f
+ )
+ )
)
)
- )
- )
- .fillMaxWidth()
- .height(dimensionResource(R.dimen.banner_height))
- ) { }
+ ) { }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.BottomCenter),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text = stringResource(R.string.okaeri),
- color = MaterialTheme.colorScheme.onSecondaryContainer,
- style = MaterialTheme.typography.displayMedium,
- modifier = Modifier
- .padding(
- start = LocalPaddings.current.large,
- bottom = LocalPaddings.current.medium
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(R.string.okaeri),
+ color = MaterialTheme.colorScheme.onSecondaryContainer,
+ style = MaterialTheme.typography.displayMedium,
+ modifier = Modifier
+ .padding(
+ start = LocalPaddings.current.large,
+ bottom = LocalPaddings.current.medium
+ )
+ .landscapeCutoutPadding()
+ .weight(1f, fill = false),
+ maxLines = 1
)
- .landscapeCutoutPadding()
- .weight(1f, fill = false),
- maxLines = 1
- )
- MediaTypeSelector(
- modifier = Modifier
- .padding(
- end = LocalPaddings.current.large,
- bottom = LocalPaddings.current.medium
+ MediaTypeSelector(
+ modifier = Modifier
+ .padding(
+ end = LocalPaddings.current.large,
+ bottom = LocalPaddings.current.medium
+ )
+ .landscapeCutoutPadding(),
+ selectedOption = homeMediaType,
+ viewModel = viewModel
)
- .landscapeCutoutPadding(),
- selectedOption = homeMediaType,
- viewModel = viewModel
- )
- }
- }
-
- Column {
- Spacer(Modifier.size(dimensionResource(R.dimen.banner_height)))
-
- Column(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .padding(vertical = LocalPaddings.current.large)
- // TODO: Move this one out of Home when we can pass modifiers in.
- .padding(bottom = dimensionResource(R.dimen.navigation_bar_height)),
- verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.large)
- ) {
+ }
+ }
+ },
+ content = {
rows.fastForEach { row ->
HomeRow(
list = row.first.data.orEmpty(),
@@ -187,8 +171,12 @@ fun HomeScreen(
}
)
}
- }
- }
+ },
+ contentModifier = Modifier.padding(
+ top = LocalPaddings.current.large,
+ bottom = dimensionResource(R.dimen.navigation_bar_height)
+ )
+ )
}
}
}
diff --git a/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt b/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt
index 967ff453..9bcc79f8 100644
--- a/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt
+++ b/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt
@@ -6,6 +6,8 @@ import android.net.Uri
import android.text.method.LinkMovementMethod
import android.util.Log
import android.widget.TextView
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -19,11 +21,9 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.displayCutout
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
@@ -73,6 +73,7 @@ import com.imashnake.animite.core.extensions.bannerParallax
import com.imashnake.animite.core.extensions.landscapeCutoutPadding
import com.imashnake.animite.core.ui.LocalPaddings
import com.imashnake.animite.core.ui.NestedScrollableContent
+import com.imashnake.animite.core.ui.layouts.BannerLayout
import com.imashnake.animite.core.ui.layouts.TranslucentStatusBarLayout
import com.imashnake.animite.dev.internal.Constants
import com.imashnake.animite.features.ui.MediaSmall
@@ -90,14 +91,12 @@ fun MediaPage(
viewModel: MediaPageViewModel = hiltViewModel()
) {
val scrollState = rememberScrollState()
- val bannerHeight = dimensionResource(R.dimen.banner_height)
val media = viewModel.uiState
MaterialTheme(colorScheme = rememberColorSchemeFor(color = media.color)) {
TranslucentStatusBarLayout(
scrollState = scrollState,
- distanceUntilAnimated = bannerHeight,
modifier = Modifier.background(MaterialTheme.colorScheme.background)
) {
Box(
@@ -105,109 +104,107 @@ fun MediaPage(
.fillMaxSize()
.verticalScroll(scrollState)
) {
- MediaBanner(
- imageUrl = media.bannerImage,
- tintColor = Color(media.color ?: 0).copy(alpha = 0.25f),
- modifier = Modifier
- .height(bannerHeight)
- .fillMaxWidth()
- .bannerParallax(scrollState)
- )
-
- Column(
- modifier = Modifier
- .fillMaxHeight()
- .padding(top = bannerHeight)
- .background(MaterialTheme.colorScheme.background)
- .padding(bottom = LocalPaddings.current.large)
- .navigationBarsPadding(),
- verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.large)
- ) {
- MediaDetails(
- title = media.title.orEmpty(),
- description = media.description.orEmpty(),
- // TODO: Can we do something about this Modifier chain?
- // Fix this in a follow up PR with `BannerLayout`.
- modifier = Modifier
- .padding(
- start = LocalPaddings.current.large
- + dimensionResource(R.dimen.media_card_width)
- + LocalPaddings.current.large,
- top = LocalPaddings.current.medium,
- end = LocalPaddings.current.large
- )
- .landscapeCutoutPadding()
- .height(
- WindowInsets.statusBars
- .asPaddingValues()
- .calculateTopPadding()
- + dimensionResource(R.dimen.media_card_top_padding)
- + dimensionResource(R.dimen.media_card_height)
- - dimensionResource(R.dimen.banner_height)
- - LocalPaddings.current.medium
- )
- .fillMaxSize()
- )
-
- if (!media.ranks.isNullOrEmpty()) {
- MediaRankings(
- rankings = media.ranks,
+ BannerLayout(
+ banner = { bannerModifier ->
+ MediaBanner(
+ imageUrl = media.bannerImage,
+ tintColor = Color(media.color ?: 0).copy(alpha = 0.25f),
+ modifier = bannerModifier.bannerParallax(scrollState)
+ )
+ },
+ content = {
+ MediaDetails(
+ title = media.title.orEmpty(),
+ description = media.description.orEmpty(),
modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = LocalPaddings.current.large)
+ .padding(
+ start = LocalPaddings.current.large
+ + dimensionResource(R.dimen.media_card_width)
+ + LocalPaddings.current.large,
+ end = LocalPaddings.current.large
+ )
.landscapeCutoutPadding()
+ .height(dimensionResource(R.dimen.media_details_height))
)
- }
- if (!media.genres.isNullOrEmpty()) {
- MediaGenres(
- genres = media.genres,
- contentPadding = PaddingValues(
- start = LocalPaddings.current.large + if (
- LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
- ) {
- WindowInsets.displayCutout.asPaddingValues()
- .calculateLeftPadding(LayoutDirection.Ltr)
- } else 0.dp,
- end = LocalPaddings.current.large
- ),
- color = Color(media.color ?: 0xFF152232.toInt()),
- )
- }
+ if (!media.ranks.isNullOrEmpty()) {
+ MediaRankings(
+ rankings = media.ranks,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = LocalPaddings.current.large)
+ .landscapeCutoutPadding()
+ )
+ }
- if (!media.characters.isNullOrEmpty()) {
- MediaCharacters(
- characters = media.characters,
- contentPadding = PaddingValues(horizontal = LocalPaddings.current.large)
- )
- }
+ if (!media.genres.isNullOrEmpty()) {
+ MediaGenres(
+ genres = media.genres,
+ contentPadding = PaddingValues(
+ start = LocalPaddings.current.large + if (
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
+ ) {
+ WindowInsets.displayCutout.asPaddingValues()
+ .calculateLeftPadding(LayoutDirection.Ltr)
+ } else 0.dp,
+ end = LocalPaddings.current.large
+ ),
+ color = Color(media.color ?: 0xFF152232.toInt()),
+ )
+ }
- if (media.trailer != null) {
- MediaTrailer(
- trailer = media.trailer,
- modifier = Modifier
- .padding(horizontal = LocalPaddings.current.large)
- .landscapeCutoutPadding()
- )
- }
- }
+ if (!media.characters.isNullOrEmpty()) {
+ MediaCharacters(
+ characters = media.characters,
+ contentPadding = PaddingValues(horizontal = LocalPaddings.current.large)
+ )
+ }
+
+ if (media.trailer != null) {
+ MediaTrailer(
+ trailer = media.trailer,
+ modifier = Modifier
+ .padding(horizontal = LocalPaddings.current.large)
+ .landscapeCutoutPadding()
+ )
+ }
+ },
+ contentModifier = Modifier.padding(top = LocalPaddings.current.medium)
+ )
+
+ // TODO: https://developer.android.com/jetpack/compose/animation/quick-guide#concurrent-animations
+ val offset by animateDpAsState(
+ targetValue = if (scrollState.value == 0) {
+ 0.dp
+ } else {
+ dimensionResource(R.dimen.media_card_height) - dimensionResource(R.dimen.media_details_height)
+ },
+ animationSpec = tween(durationMillis = 750),
+ label = "media_card_height"
+ )
- Box(
+ MediaSmall(
+ image = media.coverImage,
+ label = null,
+ onClick = {},
modifier = Modifier
.statusBarsPadding()
+ // TODO: Try using `AlignmentLine`s.
.padding(
- top = dimensionResource(R.dimen.media_card_top_padding),
+ top = dimensionResource(R.dimen.media_details_height)
+ + LocalPaddings.current.medium
+ + dimensionResource(coreR.dimen.banner_height)
+ - WindowInsets.statusBars
+ .asPaddingValues()
+ .calculateTopPadding()
+ - dimensionResource(R.dimen.media_card_height)
+ + offset,
start = LocalPaddings.current.large
)
.landscapeCutoutPadding()
- ) {
- MediaSmall(
- image = media.coverImage,
- label = null,
- onClick = {},
- modifier = Modifier.width(dimensionResource(R.dimen.media_card_width))
- )
- }
+ .height(dimensionResource(R.dimen.media_card_height) - offset)
+ .width(dimensionResource(R.dimen.media_card_width))
+ )
}
}
}
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e07e6db8..5f7d8bda 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -6,10 +6,9 @@
65dp
- 168dp
140dp
200dp
- 96dp
+ 172dp
96dp
30dp
diff --git a/core/src/main/kotlin/com/imashnake/animite/core/ui/ScrollableText.kt b/core/src/main/kotlin/com/imashnake/animite/core/ui/NestedScrollableContent.kt
similarity index 97%
rename from core/src/main/kotlin/com/imashnake/animite/core/ui/ScrollableText.kt
rename to core/src/main/kotlin/com/imashnake/animite/core/ui/NestedScrollableContent.kt
index 577821de..9d43572c 100644
--- a/core/src/main/kotlin/com/imashnake/animite/core/ui/ScrollableText.kt
+++ b/core/src/main/kotlin/com/imashnake/animite/core/ui/NestedScrollableContent.kt
@@ -23,7 +23,7 @@ fun NestedScrollableContent(
modifier: Modifier = Modifier,
gradientSize: Dp = dimensionResource(R.dimen.edge_gradient_size),
gradientColor: Color = MaterialTheme.colorScheme.background,
- content: @Composable (modifier: Modifier) -> Unit,
+ content: @Composable (Modifier) -> Unit,
) {
Box(modifier) {
content(Modifier.verticalScroll(rememberScrollState()).padding(vertical = gradientSize))
diff --git a/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/BannerLayout.kt b/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/BannerLayout.kt
new file mode 100644
index 00000000..23d6a972
--- /dev/null
+++ b/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/BannerLayout.kt
@@ -0,0 +1,57 @@
+package com.imashnake.animite.core.ui.layouts
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import com.imashnake.animite.core.R
+import com.imashnake.animite.core.ui.LocalPaddings
+
+/**
+ * Most screens and pages follow a banner-style layout in Animite.
+ *
+ * @param banner A banner [Composable] that is usually an image with
+ * [com.imashnake.animite.core.extensions.bannerParallax] and other components.
+ * @param content The content that appears in a [Column] below the banner.
+ * @param modifier Modifier for [BannerLayout].
+ * @param bannerHeight The height of the banner in [Dp]s.
+ * @param bannerModifier Modifier for [banner]. Use this if a [Composable] in [banner] should have
+ * the dimensions of the banner.
+ * @param contentModifier Modifier for [content].
+ */
+@Composable
+fun BannerLayout(
+ banner: @Composable (Modifier) -> Unit,
+ content: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ bannerHeight: Dp = dimensionResource(R.dimen.banner_height),
+ bannerModifier: Modifier = Modifier
+ .height(bannerHeight)
+ .fillMaxWidth(),
+ contentModifier: Modifier = Modifier,
+) {
+ Box(modifier) {
+ banner(bannerModifier)
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = bannerHeight)
+ .background(MaterialTheme.colorScheme.background)
+ .padding(bottom = LocalPaddings.current.large)
+ .navigationBarsPadding()
+ .then(contentModifier),
+ verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.large)
+ ) { content() }
+ }
+}
diff --git a/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/TranslucentStatusBarLayout.kt b/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/TranslucentStatusBarLayout.kt
index cd265d9c..861a81d1 100644
--- a/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/TranslucentStatusBarLayout.kt
+++ b/core/src/main/kotlin/com/imashnake/animite/core/ui/layouts/TranslucentStatusBarLayout.kt
@@ -12,14 +12,16 @@ import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
+import com.imashnake.animite.core.R
@Composable
@Suppress("LongParameterList")
fun TranslucentStatusBarLayout(
scrollState: ScrollState,
- distanceUntilAnimated: Dp,
modifier: Modifier = Modifier,
+ distanceUntilAnimated: Dp = dimensionResource(R.dimen.banner_height),
targetAlpha: Float = ContentAlpha.medium,
targetColor: Color = MaterialTheme.colorScheme.background,
content: @Composable () -> Unit
@@ -28,20 +30,24 @@ fun TranslucentStatusBarLayout(
val distanceUntilAnimatedPx = with(LocalDensity.current) { distanceUntilAnimated.toPx() }
val statusBarInsets = WindowInsets.statusBars
Box(
- Modifier.drawWithContent {
- drawContent()
- drawRect(
- color = targetColor.copy(
- alpha = targetAlpha * if (scrollState.value < distanceUntilAnimatedPx) {
- scrollState.value.toFloat() / distanceUntilAnimatedPx
- } else 1f
- ),
- size = Size(
- width = size.width,
- height = statusBarInsets.getTop(this).toFloat()
+ Modifier
+ .drawWithContent {
+ drawContent()
+ drawRect(
+ color = targetColor.copy(
+ alpha = targetAlpha * if (scrollState.value < distanceUntilAnimatedPx) {
+ scrollState.value.toFloat() / distanceUntilAnimatedPx
+ } else 1f
+ ),
+ size = Size(
+ width = size.width,
+ height = statusBarInsets
+ .getTop(this)
+ .toFloat()
+ )
)
- )
- }.then(modifier)
+ }
+ .then(modifier)
) {
content()
}
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index 3a928496..7b4b72bd 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -14,4 +14,6 @@
16dp
56dp
+
+ 168dp