feat; dynamic hero height adjustment mobile

This commit is contained in:
tapframe 2026-04-11 17:15:36 +05:30
parent f8e7354bee
commit 19b51c38d3
3 changed files with 71 additions and 1 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.ui.NuvioScreen
@ -44,10 +45,12 @@ import com.nuvio.app.features.watching.domain.buildPlaybackVideoId
import com.nuvio.app.features.collection.CollectionRepository
import com.nuvio.app.features.home.components.HomeCollectionRowSection
import com.nuvio.app.features.watching.domain.isReleasedBy
import com.nuvio.app.features.watchprogress.ContinueWatchingSectionStyle
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import com.nuvio.app.features.home.components.ContinueWatchingLayout
import com.nuvio.app.features.home.components.homeSectionHorizontalPaddingForWidth
import com.nuvio.app.features.home.components.rememberContinueWatchingLayout
@ -322,6 +325,21 @@ fun HomeScreen(
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val homeSectionPadding = homeSectionHorizontalPaddingForWidth(maxWidth.value)
val continueWatchingLayout = rememberContinueWatchingLayout(maxWidth.value)
val mobileHeroBelowSectionHeightHint = remember(
maxWidth.value,
continueWatchingPreferences.isVisible,
continueWatchingPreferences.style,
continueWatchingItems.isNotEmpty(),
continueWatchingLayout,
) {
heroMobileBelowSectionHeightHint(
maxWidthDp = maxWidth.value,
continueWatchingVisible = continueWatchingPreferences.isVisible,
hasContinueWatchingItems = continueWatchingItems.isNotEmpty(),
continueWatchingStyle = continueWatchingPreferences.style,
continueWatchingLayout = continueWatchingLayout,
)
}
NuvioScreen(
modifier = Modifier.fillMaxSize(),
@ -335,12 +353,14 @@ fun HomeScreen(
showHeroSkeleton -> HomeSkeletonHero(
modifier = Modifier,
viewportHeight = maxHeight,
mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint,
)
homeUiState.heroItems.isNotEmpty() -> HomeHeroSection(
items = homeUiState.heroItems,
modifier = Modifier,
viewportHeight = maxHeight,
mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint,
listState = homeListState,
onItemClick = onPosterClick,
)
@ -348,6 +368,7 @@ fun HomeScreen(
else -> HomeHeroReservedSpace(
modifier = Modifier,
viewportHeight = maxHeight,
mobileBelowSectionHeightHint = mobileHeroBelowSectionHeightHint,
)
}
}
@ -467,6 +488,22 @@ fun HomeScreen(
private const val HOME_CATALOG_PREVIEW_LIMIT = 18
private const val TRAKT_CONTINUE_WATCHING_DAYS_CAP_DEFAULT = 60
private fun heroMobileBelowSectionHeightHint(
maxWidthDp: Float,
continueWatchingVisible: Boolean,
hasContinueWatchingItems: Boolean,
continueWatchingStyle: ContinueWatchingSectionStyle,
continueWatchingLayout: ContinueWatchingLayout,
): Dp? {
if (maxWidthDp >= 600f || !continueWatchingVisible || !hasContinueWatchingItems) return null
return when (continueWatchingStyle) {
ContinueWatchingSectionStyle.Wide -> continueWatchingLayout.wideCardHeight + 56.dp
ContinueWatchingSectionStyle.Poster ->
continueWatchingLayout.posterCardHeight + continueWatchingLayout.posterTitleBlockHeight + 70.dp
}
}
internal fun buildHomeContinueWatchingItems(
visibleEntries: List<WatchProgressEntry>,
cachedInProgressByVideoId: Map<String, ContinueWatchingItem> = emptyMap(),

View file

@ -63,6 +63,9 @@ private const val HERO_SCROLL_UP_SCALE_MULTIPLIER = 0.002f
private const val HERO_SCROLL_MAX_SCALE = 1.3f
private const val HERO_SWIPE_THRESHOLD_FRACTION = 0.16f
private const val HERO_SWIPE_VELOCITY_THRESHOLD = 300f
private const val MOBILE_HERO_VIEWPORT_RATIO = 0.78f
private const val MOBILE_HERO_MIN_HEIGHT_DP = 360f
private const val MOBILE_HERO_MAX_HEIGHT_DP = 760f
internal data class HomeHeroLayout(
val isTablet: Boolean,
@ -80,6 +83,7 @@ fun HomeHeroSection(
items: List<MetaPreview>,
modifier: Modifier = Modifier,
viewportHeight: Dp? = null,
mobileBelowSectionHeightHint: Dp? = null,
listState: LazyListState? = null,
onItemClick: ((MetaPreview) -> Unit)? = null,
) {
@ -101,6 +105,7 @@ fun HomeHeroSection(
val layout = homeHeroLayout(
maxWidthDp = maxWidth.value,
viewportHeightDp = viewportHeight?.value,
mobileBelowSectionHeightHintDp = mobileBelowSectionHeightHint?.value,
)
val heroWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
val heroHeightPx = with(LocalDensity.current) { layout.heroHeight.toPx() }
@ -313,6 +318,7 @@ private fun heroPageVisibility(
fun HomeHeroReservedSpace(
modifier: Modifier = Modifier,
viewportHeight: Dp? = null,
mobileBelowSectionHeightHint: Dp? = null,
) {
BoxWithConstraints(
modifier = modifier
@ -322,6 +328,7 @@ fun HomeHeroReservedSpace(
val layout = homeHeroLayout(
maxWidthDp = maxWidth.value,
viewportHeightDp = viewportHeight?.value,
mobileBelowSectionHeightHintDp = mobileBelowSectionHeightHint?.value,
)
Spacer(
@ -414,6 +421,7 @@ private fun HeroMetaText(text: String) {
internal fun homeHeroLayout(
maxWidthDp: Float,
viewportHeightDp: Float? = null,
mobileBelowSectionHeightHintDp: Float? = null,
): HomeHeroLayout =
when {
maxWidthDp >= 1200f -> HomeHeroLayout(
@ -448,7 +456,11 @@ internal fun homeHeroLayout(
)
else -> HomeHeroLayout(
isTablet = false,
heroHeight = viewportHeightDp?.let { (it * 0.78f).dp } ?: (maxWidthDp * 1.16f).dp.coerceIn(420.dp, 760.dp),
heroHeight = mobileHeroHeight(
maxWidthDp = maxWidthDp,
viewportHeightDp = viewportHeightDp,
mobileBelowSectionHeightHintDp = mobileBelowSectionHeightHintDp,
),
contentMaxWidth = 480.dp,
contentWidthFraction = 1f,
contentHorizontalPadding = 24.dp,
@ -458,6 +470,25 @@ internal fun homeHeroLayout(
)
}
private fun mobileHeroHeight(
maxWidthDp: Float,
viewportHeightDp: Float?,
mobileBelowSectionHeightHintDp: Float?,
): Dp {
val viewportDrivenHeight = viewportHeightDp?.let { (it * MOBILE_HERO_VIEWPORT_RATIO).dp }
val widthFallbackHeight = (maxWidthDp * 1.16f).dp
val baseHeight = viewportDrivenHeight ?: widthFallbackHeight
val cappedHeight = if (viewportHeightDp != null && mobileBelowSectionHeightHintDp != null) {
val maxAllowedFromViewport = (viewportHeightDp - mobileBelowSectionHeightHintDp).dp
baseHeight.coerceAtMost(maxAllowedFromViewport)
} else {
baseHeight
}
return cappedHeight.coerceIn(MOBILE_HERO_MIN_HEIGHT_DP.dp, MOBILE_HERO_MAX_HEIGHT_DP.dp)
}
@Composable
private fun HeroMetaDot() {
Box(

View file

@ -61,6 +61,7 @@ private fun rememberHomeSkeletonBrush(): Brush {
fun HomeSkeletonHero(
modifier: Modifier = Modifier,
viewportHeight: Dp? = null,
mobileBelowSectionHeightHint: Dp? = null,
) {
val brush = rememberHomeSkeletonBrush()
@ -72,6 +73,7 @@ fun HomeSkeletonHero(
val layout = homeHeroLayout(
maxWidthDp = maxWidth.value,
viewportHeightDp = viewportHeight?.value,
mobileBelowSectionHeightHintDp = mobileBelowSectionHeightHint?.value,
)
val containerWidth = maxWidth