Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* Width of the side navigation rail shown on tablets (Material3 `NavigationRail` container width).
* The expanding Now Playing overlay is inset by this on portrait tablets so it never draws over the
* rail (see the `startInset` passed to [ExpandingNowPlayer]).
*/
private val NavigationRailWidth = 80.dp

enum class AppDestinations(
val label: String,
val icon: ImageVector,
Expand Down Expand Up @@ -296,10 +303,17 @@ class MainActivity : ComponentActivity() {

val isWide = (windowSizeClass.minWidthDp != 0 && windowSizeClass.minHeightDp != 0)

// Whether the Now Playing side pane can actually dock beside the content (two horizontal
// partitions — expanded width, i.e. a landscape tablet). A portrait tablet is `isWide` but
// has only ONE partition, so it can't split: it falls through to the expanding overlay player
// like a phone. Derived from the directive (fresh window metrics), NOT the flaky navigator
// pane value. This — not `isWide` — is what gates which Now Playing surface is used.
val splitViewActive = customDirective.maxHorizontalPartitions > 1

// Lift the FAB above the mini player when it's showing. The mini bar is now an overlay
// (not in the Scaffold's bottomBar), so the Scaffold no longer reserves space for it.
val fabMiniInset by animateDpAsState(
targetValue = if (!isWide && playerState?.currentMediaItem?.mediaId != null) 84.dp else 0.dp,
targetValue = if (!splitViewActive && playerState?.currentMediaItem?.mediaId != null) 84.dp else 0.dp,
label = "Fab Mini Inset"
)

Expand Down Expand Up @@ -567,7 +581,7 @@ class MainActivity : ComponentActivity() {
// fold/unfold and would let the mini and the pane both show at once.
// On phones the mini is the expanding overlay and this flag only drives the
// nav-bar fuse, so it's simply "is something playing".
val miniPlayerVisible = if (isWide) {
val miniPlayerVisible = if (splitViewActive) {
playerState?.currentMediaItem?.mediaId != null &&
(discoveryViewingStation.value || hidingDiscoverDetail)
} else {
Expand Down Expand Up @@ -606,8 +620,9 @@ class MainActivity : ComponentActivity() {
)
}

if (isWide) {
// Tablets keep the stock NavigationRail; the mini player sits in the bar.
if (splitViewActive) {
// Landscape tablet: the Now Playing side pane docks, so keep the stock
// NavigationRail and let this bottom mini bar re-dock the pane.
AnimatedVisibility(
visible = miniPlayerVisible && !hideMiniForDock,
enter = slideInVertically(
Expand Down Expand Up @@ -647,7 +662,7 @@ class MainActivity : ComponentActivity() {
}
}
}
} else {
} else if (!isWide) {
// Phones: only the floating nav bar lives in the bar. The mini player is
// now the collapsed state of the expanding Now Playing surface, rendered
// as an overlay above everything (see ExpandingNowPlayer below), so it can
Expand All @@ -674,6 +689,8 @@ class MainActivity : ComponentActivity() {
)
}
}
// else: portrait tablet — the rail owns navigation and the expanding overlay
// owns the player, so the bottom bar stays empty.
}) { innerPadding ->
// The mini / expanding player is an OVERLAY (not in the Scaffold's bottomBar),
// so the Scaffold reserves no space for it. Add its footprint to the scroll
Expand All @@ -685,7 +702,7 @@ class MainActivity : ComponentActivity() {
top = innerPadding.calculateTopPadding(),
end = innerPadding.calculateEndPadding(layoutDirection),
bottom = innerPadding.calculateBottomPadding() +
(if (!isWide && playerState?.currentMediaItem?.mediaId != null) 84.dp else 0.dp)
(if (!splitViewActive && playerState?.currentMediaItem?.mediaId != null) 84.dp else 0.dp)
)
AnimatedVisibility(
radioListMode != null,
Expand Down Expand Up @@ -810,11 +827,13 @@ class MainActivity : ComponentActivity() {
)
}

// Phone Now Playing lives here — a SECOND child of the drawer content, stacked over the
// scaffold, so the settings drawer sheet draws OVER it. It used to be a sibling of the
// whole drawer and painted on top of the opened settings sheet. Re-wrapped in
// ReverseLayoutDirection because the entire drawer is flipped to sit on the right edge.
if (!isWide) {
// The expanding Now Playing surface lives here — a SECOND child of the drawer content,
// stacked over the scaffold, so the settings drawer sheet draws OVER it. It used to be a
// sibling of the whole drawer and painted on top of the opened settings sheet. Re-wrapped
// in ReverseLayoutDirection because the entire drawer is flipped to sit on the right edge.
// Shown whenever the side pane can't dock (phones AND portrait tablets) — the docking side
// pane replaces it only when the horizontal split is actually available.
if (!splitViewActive) {
// When playback ends, snap back to the mini bar so the next song opens collapsed.
LaunchedEffect(playerState?.currentMediaItem?.mediaId) {
if (playerState?.currentMediaItem?.mediaId == null) {
Expand All @@ -838,7 +857,12 @@ class MainActivity : ComponentActivity() {
play = { mediaController?.play() },
pause = { mediaController?.pause() },
stop = { mainActivityViewModel?.sharedMediaController?.mediaSession?.value?.player?.stop() },
onDismissProgress = { miniDismissProgress = it }
onDismissProgress = { miniDismissProgress = it },
// Portrait tablet keeps the side rail (no floating bottom nav bar), so the mini bar
// rests just above the system nav inset instead of reserving the nav bar footprint,
// and is inset past the rail so it never draws over it. Phones: neither applies.
reserveNavBarArea = !isWide,
startInset = if (isWide) NavigationRailWidth else 0.dp
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,25 @@ fun ExpandingNowPlayer(
pause: () -> Unit,
stop: () -> Unit,
onDismissProgress: (Float) -> Unit,
reserveNavBarArea: Boolean = true,
startInset: Dp = 0.dp,
modifier: Modifier = Modifier
) {
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val density = LocalDensity.current
val fullHeightPx = with(density) { maxHeight.toPx() }
// Bind the scope to a `this` local: its constraints give the container's full height (px). This
// also satisfies the "unused BoxWithConstraints scope" lint — the scope is otherwise only read
// inside nested lambdas (with/remember/layout), which the check doesn't count as a use.
val boxScope = this
val fullHeightPx = boxScope.constraints.maxHeight.toFloat()
val miniBarPx = with(density) { MiniBarHeight.toPx() }
val sideMarginPx = with(density) { CollapsedSideMargin.toPx() }
val startInsetPx = with(density) { startInset.toPx() }
val shadowPx = with(density) { CollapsedShadow.toPx() }
// Phones float above the bottom nav bar (reserve its 82dp footprint); tablets use a side rail and
// have no bottom bar, so the mini bar rests just above the system nav inset.
val navAreaPx = WindowInsets.navigationBars.getBottom(density) +
with(density) { NavBarArea.toPx() }
if (reserveNavBarArea) with(density) { NavBarArea.toPx() } else 0f
val collapsedY = (fullHeightPx - miniBarPx - navAreaPx).coerceAtLeast(0f)

val thresholdPx = with(density) { 5.dp.toPx() }
Expand Down Expand Up @@ -191,12 +200,15 @@ fun ExpandingNowPlayer(
.layout { measurable, constraints ->
val f = state.expansion()
val hInset = lerp(sideMarginPx, 0f, f)
val cardW = (constraints.maxWidth - 2f * hInset).roundToInt()
// Left inset that clears the side nav rail while collapsed and closes to 0 as the card fills
// the screen — so the collapsed mini bar sits beside the rail but the full player covers it.
val railInset = lerp(startInsetPx, 0f, f)
val cardW = (constraints.maxWidth - railInset - 2f * hInset).roundToInt()
.coerceIn(0, constraints.maxWidth)
val cardH = lerp(miniBarPx, fullHeightPx, f).roundToInt()
.coerceIn(0, constraints.maxHeight)
val placeable = measurable.measure(Constraints.fixed(cardW, cardH))
layout(constraints.maxWidth, cardH) { placeable.place(hInset.roundToInt(), 0) }
layout(constraints.maxWidth, cardH) { placeable.place((railInset + hInset).roundToInt(), 0) }
}
// One shape + shadow for the whole card; both flatten as it opens. translationX slides the
// whole card when the collapsed mini bar is swiped away to dismiss.
Expand All @@ -206,7 +218,11 @@ fun ExpandingNowPlayer(
// radius as the mini bar is swiped away, then flatten with the top corners as it opens.
val dismiss = (abs(dismissOffset.value) / (screenWidthPx * 0.4f)).coerceIn(0f, 1f)
val topC = lerp(CollapsedCorner, 0.dp, f)
val bottomC = lerp(lerp(CollapsedBottomCorner, CollapsedCorner, dismiss), 0.dp, f)
// Phones nest the bottom against the nav bar (tight 14dp); tablets float free with no bar
// below, so the bottom is fully rounded like the top. Both round back to [CollapsedCorner]
// on swipe-away and flatten to 0 as the card fills the screen.
val restingBottomCorner = if (reserveNavBarArea) CollapsedBottomCorner else CollapsedCorner
val bottomC = lerp(lerp(restingBottomCorner, CollapsedCorner, dismiss), 0.dp, f)
shape = RoundedCornerShape(
topStart = topC,
topEnd = topC,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand Down Expand Up @@ -186,7 +185,7 @@ fun NowPlayingContent(
.padding(bottom = innerPadding.calculateBottomPadding())
.fillMaxHeight()
.fillMaxWidth()
.padding(bottom = 32.dp)
// .padding(bottom = 32.dp)
) {
NowPlayingAlbumArt(
modifier = Modifier.fillMaxHeight(.75f),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
Expand All @@ -20,7 +19,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.palette.graphics.Palette
Expand Down Expand Up @@ -91,6 +89,6 @@ fun NowPlayingBottomBar(
isSleeping = isSleeping,
palette = palette
)
Spacer(Modifier.height(48.dp))
Spacer(Modifier.weight(0.05f))
}
}
Loading