Skip to content

Commit 2ff4434

Browse files
committed
切歌时的封面动画
1 parent f6e4b9f commit 2ff4434

4 files changed

Lines changed: 222 additions & 173 deletions

File tree

app/src/main/java/com/ljyh/mei/ui/component/player/Player.kt

Lines changed: 68 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import android.widget.Toast
66
import androidx.activity.compose.BackHandler
77
import androidx.annotation.OptIn
88
import androidx.annotation.RequiresApi
9+
import androidx.compose.animation.AnimatedContent
910
import androidx.compose.animation.AnimatedVisibility
1011
import androidx.compose.animation.core.Spring
1112
import androidx.compose.animation.core.animateDpAsState
1213
import androidx.compose.animation.core.animateFloat
1314
import androidx.compose.animation.core.spring
15+
import androidx.compose.animation.core.tween
1416
import androidx.compose.animation.core.updateTransition
1517
import androidx.compose.animation.fadeIn
1618
import androidx.compose.animation.fadeOut
19+
import androidx.compose.animation.scaleIn
1720
import androidx.compose.animation.slideInVertically
1821
import androidx.compose.animation.slideOutVertically
22+
import androidx.compose.animation.togetherWith
1923
import androidx.compose.foundation.background
2024
import androidx.compose.foundation.clickable
2125
import androidx.compose.foundation.isSystemInDarkTheme
@@ -28,15 +32,13 @@ import androidx.compose.foundation.layout.WindowInsetsSides
2832
import androidx.compose.foundation.layout.fillMaxSize
2933
import androidx.compose.foundation.layout.fillMaxWidth
3034
import androidx.compose.foundation.layout.height
31-
import androidx.compose.foundation.layout.navigationBars
3235
import androidx.compose.foundation.layout.offset
3336
import androidx.compose.foundation.layout.only
3437
import androidx.compose.foundation.layout.padding
3538
import androidx.compose.foundation.layout.size
3639
import androidx.compose.foundation.layout.statusBars
3740
import androidx.compose.foundation.layout.systemBars
3841
import androidx.compose.foundation.layout.width
39-
import androidx.compose.foundation.layout.windowInsetsBottomHeight
4042
import androidx.compose.foundation.layout.windowInsetsPadding
4143
import androidx.compose.foundation.shape.RoundedCornerShape
4244
import androidx.compose.material3.MaterialTheme
@@ -70,25 +72,19 @@ import coil3.size.Precision
7072
import coil3.size.Size
7173
import com.ljyh.mei.constants.DebugKey
7274
import com.ljyh.mei.constants.PlayerHorizontalPadding
73-
import com.ljyh.mei.constants.ProgressBarStyle
74-
import com.ljyh.mei.constants.ProgressBarStyleKey
7575
import com.ljyh.mei.constants.ThumbnailCornerRadius
7676
import com.ljyh.mei.constants.UseQQMusicLyricKey
7777
import com.ljyh.mei.data.network.Resource
78-
import com.ljyh.mei.playback.PlayerConnection
79-
import com.ljyh.mei.ui.component.player.component.sheet.AlbumArtistBottomSheet
8078
import com.ljyh.mei.ui.component.player.component.AppleMusicFluidBackground
81-
import com.ljyh.mei.ui.component.player.component.AppleStyleProgressSlider
82-
import com.ljyh.mei.ui.component.player.component.Controls
8379
import com.ljyh.mei.ui.component.player.component.LyricScreen
84-
import com.ljyh.mei.ui.component.player.component.PlayerActionToolbar
85-
import com.ljyh.mei.ui.component.player.component.PlayerProgressSlider
86-
import com.ljyh.mei.ui.component.player.component.sheet.PlaylistBottomSheet
87-
import com.ljyh.mei.ui.component.player.component.sheet.QQMusicSelectSheet
88-
import com.ljyh.mei.ui.component.player.component.sheet.SleepTimerSheet
80+
import com.ljyh.mei.ui.component.player.component.PlayerControlsSection
8981
import com.ljyh.mei.ui.component.player.component.Title
82+
import com.ljyh.mei.ui.component.player.component.sheet.AlbumArtistBottomSheet
9083
import com.ljyh.mei.ui.component.player.component.sheet.MoreActionsSheet
9184
import com.ljyh.mei.ui.component.player.component.sheet.PlayerActionSettingsSheet
85+
import com.ljyh.mei.ui.component.player.component.sheet.PlaylistBottomSheet
86+
import com.ljyh.mei.ui.component.player.component.sheet.QQMusicSelectSheet
87+
import com.ljyh.mei.ui.component.player.component.sheet.SleepTimerSheet
9288
import com.ljyh.mei.ui.component.playlist.AddToPlaylistSheet
9389
import com.ljyh.mei.ui.component.playlist.CreatePlaylistSheet
9490
import com.ljyh.mei.ui.component.sheet.BottomSheet
@@ -105,7 +101,6 @@ import com.ljyh.mei.utils.UnitUtils.toPx
105101
import com.ljyh.mei.utils.encrypt.QRCUtils
106102
import com.ljyh.mei.utils.lyric.createDefaultLyricData
107103
import com.ljyh.mei.utils.lyric.mergeLyrics
108-
import com.ljyh.mei.utils.rememberEnumPreference
109104
import com.ljyh.mei.utils.rememberPreference
110105
import kotlinx.coroutines.Dispatchers
111106
import kotlinx.coroutines.delay
@@ -540,6 +535,7 @@ fun BottomSheetPlayer(
540535
currentOverlay = OverlayState.AddToPlaylist(it.id)
541536
}
542537
},
538+
onMoreClick = { currentOverlay = OverlayState.MoreAction },
543539
isCompact = isCompactHeight || isLandscape
544540
)
545541
}
@@ -548,44 +544,59 @@ fun BottomSheetPlayer(
548544
}
549545
}
550546

551-
// --- Shared Element (Cover) ---
552-
if (mediaMetadata != null) {
553-
// 修改 BottomSheetPlayer 里的 AsyncImage 部分
554-
AsyncImage(
555-
model = ImageRequest.Builder(LocalContext.current)
556-
.data(mediaMetadata?.coverUrl)
557-
.size(Size.ORIGINAL)
558-
.precision(Precision.EXACT)
559-
.crossfade(true)
560-
.build(),
561-
contentDescription = "Cover",
562-
contentScale = ContentScale.Crop,
563-
onError = {
564-
},
565-
566-
modifier = Modifier
567-
.graphicsLayer {
568-
translationX = finalStart
569-
translationY = finalTop
570-
shadowElevation = mShadowElevation.toPx()
571-
shape = RoundedCornerShape(finalRadius)
572-
clip = true
573-
}
574-
.size(
575-
width = with(density) { finalSize.toDp() },
576-
height = with(density) { finalSize.toDp() }
547+
AnimatedContent(
548+
targetState = mediaMetadata, // 监听歌曲元数据变化
549+
transitionSpec = {
550+
// 定义切换动画:
551+
// 进入的图片:淡入 + 从 0.9 倍大小放大到 1.0 (这也是 Apple Music 的专辑切换效果之一)
552+
(fadeIn(animationSpec = tween(durationMillis = 400)) +
553+
scaleIn(initialScale = 0.92f, animationSpec = tween(durationMillis = 400)))
554+
.togetherWith(
555+
// 离开的图片:淡出
556+
fadeOut(animationSpec = tween(durationMillis = 400))
577557
)
578-
.clickable {
579-
if (!state.isExpanded) {
580-
state.expandSoft()
581-
} else {
582-
showLyrics = !showLyrics
583-
}
558+
},
559+
label = "CoverTransition",
560+
modifier = Modifier
561+
.graphicsLayer {
562+
translationX = finalStart
563+
translationY = finalTop
564+
shadowElevation = mShadowElevation.toPx()
565+
shape = RoundedCornerShape(finalRadius)
566+
clip = true
567+
}
568+
.size(
569+
width = with(density) { finalSize.toDp() },
570+
height = with(density) { finalSize.toDp() }
571+
)
572+
.clickable {
573+
if (!state.isExpanded) {
574+
state.expandSoft()
575+
} else {
576+
showLyrics = !showLyrics
584577
}
585-
.background(MaterialTheme.colorScheme.surfaceVariant)
586-
)
578+
}
579+
.background(MaterialTheme.colorScheme.surfaceVariant) // 占位背景色
580+
) { currentMetadata ->
581+
// 这里是动画的内容部分
582+
if (currentMetadata != null) {
583+
AsyncImage(
584+
model = ImageRequest.Builder(LocalContext.current)
585+
.data(currentMetadata.coverUrl)
586+
.size(Size.ORIGINAL)
587+
.precision(Precision.EXACT)
588+
.crossfade(true)
589+
.build(),
590+
contentDescription = "Cover",
591+
contentScale = ContentScale.Crop,
592+
modifier = Modifier.fillMaxSize()
593+
)
594+
} else {
595+
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceVariant))
596+
}
587597
}
588598

599+
589600
when (val overlay = currentOverlay) {
590601
OverlayState.None -> {}
591602
OverlayState.Playlist -> {
@@ -665,8 +676,8 @@ fun BottomSheetPlayer(
665676
)
666677
}
667678

668-
OverlayState.BottomAction ->{
669-
PlayerActionSettingsSheet(onDismiss = {currentOverlay = OverlayState.None})
679+
OverlayState.BottomAction -> {
680+
PlayerActionSettingsSheet(onDismiss = { currentOverlay = OverlayState.None })
670681
}
671682

672683
OverlayState.MoreAction -> {
@@ -675,31 +686,37 @@ fun BottomSheetPlayer(
675686
currentOverlay = OverlayState.None
676687
},
677688
onActionClick = { action ->
678-
when(action){
689+
when (action) {
679690
MoreAction.ADD_TO_PLAYLIST -> {
680691
mediaMetadata?.let {
681692
currentOverlay = OverlayState.AddToPlaylist(it.id)
682693
}
683694
}
695+
684696
MoreAction.SHARE -> {
685697
currentOverlay = OverlayState.None
686698
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show()
687699
}
700+
688701
MoreAction.DOWNLOAD -> {
689702
currentOverlay = OverlayState.None
690703
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show()
691704
}
705+
692706
MoreAction.DELETE -> {
693707
mediaMetadata?.let {
694708
playerViewModel.deleteSongById(it.id.toString())
695709
}
696710
}
711+
697712
MoreAction.VIEW_PLAYLIST -> {
698713
currentOverlay = OverlayState.Playlist
699714
}
715+
700716
MoreAction.SLEEP_TIMER -> {
701717
currentOverlay = OverlayState.SleepTimer
702718
}
719+
703720
MoreAction.BOTTOM_ACTION -> {
704721
currentOverlay = OverlayState.BottomAction
705722
}
@@ -713,92 +730,4 @@ fun BottomSheetPlayer(
713730
else -> {}
714731
}
715732
}
716-
}
717-
718-
// 稍微修改 Controls Section,让它支持紧凑模式
719-
@OptIn(UnstableApi::class)
720-
@Composable
721-
fun PlayerControlsSection(
722-
sliderPosition: Float,
723-
duration: Long,
724-
isPlaying: Boolean,
725-
playbackState: Int,
726-
playerConnection: PlayerConnection,
727-
onLyricClick: () -> Unit,
728-
onPlaylistClick: () -> Unit,
729-
onSleepTimerClick: () -> Unit,
730-
onAddToPlaylistClick: () -> Unit,
731-
isCompact: Boolean = false // 新增参数
732-
) {
733-
// 紧凑模式下间距减半
734-
val spacerHeight = if (isCompact) 8.dp else 24.dp
735-
736-
val (progressBarStyle, _) = rememberEnumPreference(
737-
key = ProgressBarStyleKey,
738-
defaultValue = ProgressBarStyle.WAVE
739-
)
740-
741-
Column(
742-
modifier = Modifier.fillMaxWidth()
743-
) {
744-
if (progressBarStyle == ProgressBarStyle.LINEAR) {
745-
AppleStyleProgressSlider(
746-
position = sliderPosition.toLong(),
747-
duration = duration,
748-
onPositionChange = { newPosition ->
749-
playerConnection.player.seekTo(newPosition)
750-
},
751-
modifier = Modifier
752-
.fillMaxWidth()
753-
.padding(horizontal = PlayerHorizontalPadding + 8.dp)
754-
)
755-
} else {
756-
PlayerProgressSlider(
757-
position = sliderPosition.toLong(),
758-
duration = duration,
759-
isPlaying = isPlaying, // 波浪进度条需要这个参数
760-
onPositionChange = { newPosition ->
761-
playerConnection.player.seekTo(newPosition)
762-
},
763-
modifier = Modifier
764-
.fillMaxWidth()
765-
.padding(horizontal = PlayerHorizontalPadding + 8.dp)
766-
)
767-
}
768-
769-
770-
Spacer(Modifier.height(spacerHeight))
771-
772-
Box(
773-
modifier = Modifier
774-
.fillMaxWidth()
775-
.padding(horizontal = PlayerHorizontalPadding)
776-
) {
777-
Controls(
778-
playerConnection = playerConnection,
779-
canSkipPrevious = true,
780-
canSkipNext = true,
781-
isPlaying = isPlaying,
782-
playbackState = playbackState,
783-
modifier = Modifier.align(Alignment.Center)
784-
)
785-
}
786-
787-
// 紧凑模式下可能需要隐藏底部 Toolbar 或者减小间距
788-
if (!isCompact) {
789-
Spacer(Modifier.height(spacerHeight))
790-
PlayerActionToolbar(
791-
modifier = Modifier.padding(horizontal = PlayerHorizontalPadding),
792-
onLyricClick = onLyricClick,
793-
onPlaylistClick = onPlaylistClick,
794-
onSleepTimerClick = onSleepTimerClick,
795-
onAddToPlaylistClick = onAddToPlaylistClick
796-
)
797-
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
798-
Spacer(Modifier.height(16.dp))
799-
} else {
800-
// 紧凑模式底部留白少一点
801-
Spacer(Modifier.height(16.dp))
802-
}
803-
}
804733
}

0 commit comments

Comments
 (0)