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
3 changes: 2 additions & 1 deletion app/src/main/java/com/theveloper/pixelplay/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -865,9 +865,10 @@ class MainActivity : ComponentActivity() {
// animating the radius re-clips this layer only — no
// recomposition and no layout pass for the bar.
val fraction = playerViewModel.playerContentExpansionFraction.value
val safeFraction = fraction.coerceIn(0f, 1f)
val topDp = when {
navBarStyle == NavBarStyle.DEFAULT -> animatedDefaultTopCornerRadius.value
navBarStyle == NavBarStyle.FULL_WIDTH -> lerp(navBarCornerRadius.dp, 26.dp, fraction)
navBarStyle == NavBarStyle.FULL_WIDTH -> lerp(navBarCornerRadius.dp, 26.dp, safeFraction)
showPlayerContentArea -> if (fraction < 0.2f) {
lerp(navBarCornerRadius.dp, 26.dp, (fraction / 0.2f).coerceIn(0f, 1f))
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.first

import com.theveloper.pixelplay.data.preferences.AlbumArtQuality
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MotionScheme

// ====== TIPOS/STATE DEL CARRUSEL (wrapper para mantener compatibilidad) ======

Expand All @@ -33,7 +35,7 @@ fun rememberRoundedParallaxCarouselState(

// ====== TU SECCIÓN: ACOPLADA AL NUEVO API ======

@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AlbumCarouselSection(
currentSong: Song?,
Expand Down Expand Up @@ -64,6 +66,9 @@ fun AlbumCarouselSection(
pageCount = { queue.size }
)

val motionScheme = remember { MotionScheme.expressive() }
val carouselAnimationSpec = remember { motionScheme.defaultSpatialSpec<Float>() }

// Calculate target size based on quality
val targetSize = remember(albumArtQuality) {
if (albumArtQuality.maxSize == 0) SafeOriginalAlbumArtSize
Expand Down Expand Up @@ -117,7 +122,7 @@ fun AlbumCarouselSection(
}
programmaticScrollInProgress = true
try {
carouselState.animateScrollToItem(effectiveTargetIndex)
carouselState.animateScrollToItem(effectiveTargetIndex, animationSpec = carouselAnimationSpec)
} finally {
programmaticScrollInProgress = false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,10 @@ class CarouselState(

suspend fun scrollToItem(item: Int) = pagerState.scrollToPage(item, 0f)

@Suppress("UNUSED_PARAMETER")
suspend fun animateScrollToItem(item: Int, animationSpec: AnimationSpec<Float> = spring()) {
if ((item == pagerState.currentPage && pagerState.currentPageOffsetFraction == 0f) || pagerState.pageCount == 0) return
val targetPage = if (pagerState.pageCount > 0) item.coerceIn(0, pagerState.pageCount - 1) else 0
pagerState.animateScrollToPage(targetPage)
pagerState.animateScrollToPage(page = targetPage, animationSpec = animationSpec)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -47,7 +48,7 @@ import coil.size.Size
import com.theveloper.pixelplay.data.model.Song
import com.theveloper.pixelplay.ui.theme.GoogleSansRounded

internal val LocalMaterialTheme = staticCompositionLocalOf<ColorScheme> { error("No ColorScheme provided") }
internal val LocalMaterialTheme = compositionLocalOf<ColorScheme> { error("No ColorScheme provided") }

val MiniPlayerHeight = 64.dp
const val ANIMATION_DURATION_MS = 255
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -102,7 +104,7 @@ private data class PlayerUiSheetSliceV2(
* This path keeps behavior parity, but now owns its own runtime wiring so we can
* profile and optimize V2 independently while preserving the Experimental switch.
*/
@androidx.annotation.OptIn(UnstableApi::class)
@androidx.annotation.OptIn(UnstableApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun UnifiedPlayerSheetV2(
playerViewModel: PlayerViewModel,
Expand Down Expand Up @@ -243,9 +245,8 @@ fun UnifiedPlayerSheetV2(
val playerContentExpansionFraction = playerViewModel.playerContentExpansionFraction
val visualOvershootScaleY = remember { Animatable(1f) }
val initialFullPlayerOffsetY = remember(density) { with(density) { 24.dp.toPx() } }
val sheetAnimationSpec = remember {
tween<Float>(durationMillis = ANIMATION_DURATION_MS, easing = FastOutSlowInEasing)
}
val motionScheme = remember { MotionScheme.expressive() }
val sheetAnimationSpec = remember { motionScheme.defaultSpatialSpec<Float>() }
val sheetAnimationMutex = remember { MutatorMutex() }
val sheetExpandedTargetY = 0f
val initialY =
Expand Down Expand Up @@ -328,18 +329,19 @@ fun UnifiedPlayerSheetV2(
showPlayerContentArea &&
previousSheetState == PlayerSheetState.EXPANDED &&
currentSheetContentState == PlayerSheetState.COLLAPSED

previousSheetState = currentSheetContentState
animatePlayerSheet(targetExpanded = targetExpanded)

scope.launch {
animatePlayerSheet(targetExpanded = targetExpanded)
}

if (showPlayerContentArea) {
scope.launch {
visualOvershootScaleY.snapTo(1f)
if (targetExpanded) {
visualOvershootScaleY.snapTo(1f)
visualOvershootScaleY.animateTo(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 50
durationMillis = 250
1.0f at 0
1.05f at 125
1.0f at 250
Expand Down Expand Up @@ -590,8 +592,24 @@ fun UnifiedPlayerSheetV2(
Surface(
modifier = Modifier
.fillMaxWidth()
.offset { IntOffset(0, visualSheetTranslationYProvider().roundToInt()) }
.height(containerHeight),
.layout { measurable, constraints ->
val translationY = visualSheetTranslationYProvider().roundToInt()
val overshoot = if (currentSheetContentState == PlayerSheetState.EXPANDED && !isDragging) {
-translationY
} else {
if (translationY < 0) -translationY else 0
}
val targetHeight = constraints.maxHeight + overshoot
val placeable = measurable.measure(
constraints.copy(
minHeight = targetHeight,
maxHeight = targetHeight
)
)
layout(constraints.maxWidth, constraints.maxHeight) {
placeable.placeRelative(0, translationY)
}
},
shadowElevation = 0.dp,
color = Color.Transparent
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
Expand Down Expand Up @@ -61,7 +63,7 @@ import com.theveloper.pixelplay.utils.formatDuration
import kotlin.math.roundToLong
import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape

@OptIn(UnstableApi::class)
@OptIn(UnstableApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ExternalPlayerOverlay(
playerViewModel: PlayerViewModel,
Expand All @@ -78,6 +80,8 @@ fun ExternalPlayerOverlay(

var sheetVisible by remember { mutableStateOf(true) }
var awaitingSong by remember { mutableStateOf(true) }
val motionScheme = remember { MotionScheme.expressive() }
val controlSpatialSpec = remember { motionScheme.fastSpatialSpec<Float>() }

val sheetShape = remember(navBarCornerRadius) {
val radiusDp = navBarCornerRadius.dp
Expand All @@ -93,13 +97,6 @@ fun ExternalPlayerOverlay(
)
}

val controlAnimationSpec = remember {
spring<Float>(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
}

LaunchedEffect(currentSong) {
if (currentSong != null) {
awaitingSong = false
Expand Down Expand Up @@ -306,7 +303,7 @@ fun ExternalPlayerOverlay(
onPlayPause = playerViewModel::playPause,
onNext = playerViewModel::nextSong,
height = 76.dp,
pressAnimationSpec = controlAnimationSpec,
pressAnimationSpec = controlSpatialSpec,
colorOtherButtons = skipContainer,
colorPlayPause = playPauseContainer,
tintPlayPauseIcon = playPauseContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,21 @@ import androidx.compose.material.icons.rounded.SkipNext
import androidx.compose.material.icons.rounded.SkipPrevious
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MotionScheme
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.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.Dp
Expand Down Expand Up @@ -74,17 +78,26 @@ fun AnimatedPlaybackControls(
) {
val isPlaying = isPlayingProvider()
var lastClicked by remember { mutableStateOf<PlaybackButtonType?>(null) }
var clickTrigger by remember { mutableStateOf(0) }
val latestIsPlayingProvider by rememberUpdatedState(newValue = isPlayingProvider)
val latestLastClicked by rememberUpdatedState(newValue = lastClicked)
val isPlayPauseLocked =
lastClicked == PlaybackButtonType.NEXT || lastClicked == PlaybackButtonType.PREVIOUS
var playPauseVisualState by remember { mutableStateOf(isPlaying) }
var pendingPlayPauseState by remember { mutableStateOf<Boolean?>(null) }
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()

LaunchedEffect(lastClicked) {
val motionScheme = remember { MotionScheme.expressive() }
val defaultSpatialDpSpec = remember { motionScheme.defaultSpatialSpec<Dp>() }

LaunchedEffect(lastClicked, clickTrigger) {
if (lastClicked != null) {
delay(releaseDelay)
val delayTime = when (lastClicked) {
PlaybackButtonType.NEXT, PlaybackButtonType.PREVIOUS -> 600L
else -> releaseDelay
}
delay(delayTime)
lastClicked = null
}
}
Expand Down Expand Up @@ -142,7 +155,11 @@ fun AnimatedPlaybackControls(
.background(colorPreviousButton)
.clickable {
lastClicked = PlaybackButtonType.PREVIOUS
onPrevious()
clickTrigger++
coroutineScope.launch {
delay(180)
onPrevious()
}
},
contentAlignment = Alignment.Center
) {
Expand All @@ -159,35 +176,32 @@ fun AnimatedPlaybackControls(
animationSpec = pressAnimationSpec,
label = "playWeight"
)
// Tween (matching the Crossfade duration) instead of a spring with
// StiffnessMedium. The old spring took ~600 ms to settle and read
// playCorner in the composition phase, recomposing AnimatedPlaybackControls
// every frame for the entire settle. A bounded 220 ms tween that completes
// alongside the icon Crossfade keeps the recomposition window small enough
// that it doesn't overlap with a subsequent sheet-collapse gesture.
val playCorner by animateDpAsState(
targetValue = if (!playPauseVisualState) playPauseCornerPlaying else playPauseCornerPaused,
animationSpec = tween(durationMillis = 220, easing = FastOutSlowInEasing),
animationSpec = defaultSpatialDpSpec,
label = "playCorner"
)
val playShape = AbsoluteSmoothCornerShape(
cornerRadiusTL = playCorner,
smoothnessAsPercentTR = 60,
cornerRadiusBL = playCorner,
smoothnessAsPercentTL = 60,
cornerRadiusTR = playCorner,
smoothnessAsPercentBL = 60,
cornerRadiusBR = playCorner,
smoothnessAsPercentBR = 60
)
Box(
modifier = Modifier
.weight(playWeight)
.fillMaxHeight()
.clip(playShape)
.graphicsLayer {
clip = true
shape = AbsoluteSmoothCornerShape(
cornerRadiusTL = playCorner,
smoothnessAsPercentTR = 60,
cornerRadiusBL = playCorner,
smoothnessAsPercentTL = 60,
cornerRadiusTR = playCorner,
smoothnessAsPercentBL = 60,
cornerRadiusBR = playCorner,
smoothnessAsPercentBR = 60
)
}
.background(colorPlayPause)
.clickable {
lastClicked = PlaybackButtonType.PLAY_PAUSE
clickTrigger++
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onPlayPause()
},
Expand All @@ -196,7 +210,8 @@ fun AnimatedPlaybackControls(
MorphingPlayPauseIcon(
isPlaying = playPauseVisualState,
tint = tintPlayPauseIcon,
size = playPauseIconSize
size = playPauseIconSize,
motionScheme = motionScheme
)
}

Expand All @@ -213,7 +228,11 @@ fun AnimatedPlaybackControls(
.background(colorNextButton)
.clickable {
lastClicked = PlaybackButtonType.NEXT
onNext()
clickTrigger++
coroutineScope.launch {
delay(180)
onNext()
}
},
contentAlignment = Alignment.Center
) {
Expand All @@ -233,10 +252,11 @@ private fun MorphingPlayPauseIcon(
isPlaying: Boolean,
tint: Color,
size: Dp,
motionScheme: MotionScheme
) {
Crossfade(
targetState = isPlaying,
animationSpec = tween(durationMillis = 220, easing = FastOutSlowInEasing),
animationSpec = motionScheme.fastEffectsSpec(),
label = "playPauseCrossfade"
) { playing ->
Icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MotionScheme
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
Expand Down Expand Up @@ -1123,9 +1124,8 @@ private fun FullPlayerControlsSection(
onRepeatToggle: () -> Unit,
onFavoriteToggle: () -> Unit
) {
val stableControlAnimationSpec = remember {
tween<Float>(durationMillis = 240, easing = FastOutSlowInEasing)
}
val motionScheme = remember { MotionScheme.expressive() }
val controlSpatialSpec = remember { motionScheme.fastSpatialSpec<Float>() }
val shouldDelay = loadingTweaks.delayAll || loadingTweaks.delayControls

DelayedContent(
Expand Down Expand Up @@ -1159,7 +1159,7 @@ private fun FullPlayerControlsSection(
onPlayPause = onPlayPause,
onNext = onNext,
height = 80.dp,
pressAnimationSpec = stableControlAnimationSpec,
pressAnimationSpec = controlSpatialSpec,
releaseDelay = 220L,
colorOtherButtons = transportSkipColors.container,
colorPlayPause = transportPlayPauseColors.container,
Expand Down
Loading
Loading