This commit is contained in:
vfsfitvnm 2022-07-22 21:08:31 +02:00
parent 0e793c956c
commit 6474b52490
5 changed files with 261 additions and 241 deletions

View file

@ -1,6 +1,5 @@
package it.vfsfitvnm.vimusic package it.vfsfitvnm.vimusic
import android.annotation.SuppressLint
import android.content.* import android.content.*
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -24,7 +23,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.ExperimentalTextApi import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.valentinilk.shimmer.LocalShimmerTheme import com.valentinilk.shimmer.LocalShimmerTheme
import com.valentinilk.shimmer.defaultShimmerTheme import com.valentinilk.shimmer.defaultShimmerTheme
@ -43,7 +43,6 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.views.PlayerView import it.vfsfitvnm.vimusic.ui.views.PlayerView
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val serviceConnection = object : ServiceConnection { private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@ -70,10 +69,7 @@ class MainActivity : ComponentActivity() {
super.onStop() super.onStop()
} }
@SuppressLint("BatteryLife") @OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class,
ExperimentalTextApi::class
)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -85,7 +81,8 @@ class MainActivity : ComponentActivity() {
var appearance by remember(isSystemInDarkTheme) { var appearance by remember(isSystemInDarkTheme) {
with(preferences) { with(preferences) {
val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System) val colorPaletteMode = getEnum(colorPaletteModeKey, ColorPaletteMode.System)
val thumbnailRoundness = getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Light) val thumbnailRoundness =
getEnum(thumbnailRoundnessKey, ThumbnailRoundness.Light)
mutableStateOf( mutableStateOf(
Appearance( Appearance(
@ -102,7 +99,8 @@ class MainActivity : ComponentActivity() {
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
when (key) { when (key) {
colorPaletteModeKey -> { colorPaletteModeKey -> {
val colorPaletteMode = sharedPreferences.getEnum(key, ColorPaletteMode.System) val colorPaletteMode =
sharedPreferences.getEnum(key, ColorPaletteMode.System)
appearance = appearance.copy( appearance = appearance.copy(
colorPalette = colorPaletteMode.palette(isSystemInDarkTheme), colorPalette = colorPaletteMode.palette(isSystemInDarkTheme),
@ -110,7 +108,8 @@ class MainActivity : ComponentActivity() {
) )
} }
thumbnailRoundnessKey -> { thumbnailRoundnessKey -> {
val thumbnailRoundness = sharedPreferences.getEnum(key, ThumbnailRoundness.Light) val thumbnailRoundness =
sharedPreferences.getEnum(key, ThumbnailRoundness.Light)
appearance = appearance.copy( appearance = appearance.copy(
thumbnailShape = thumbnailRoundness.shape() thumbnailShape = thumbnailRoundness.shape()
@ -130,7 +129,8 @@ class MainActivity : ComponentActivity() {
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val rippleTheme = remember(appearance.colorPalette.text, appearance.colorPalette.isDark) { val rippleTheme =
remember(appearance.colorPalette.text, appearance.colorPalette.isDark) {
object : RippleTheme { object : RippleTheme {
@Composable @Composable
override fun defaultColor(): Color = RippleTheme.defaultRippleColor( override fun defaultColor(): Color = RippleTheme.defaultRippleColor(
@ -165,7 +165,10 @@ class MainActivity : ComponentActivity() {
} }
SideEffect { SideEffect {
systemUiController.setSystemBarsColor(appearance.colorPalette.background, !appearance.colorPalette.isDark) systemUiController.setSystemBarsColor(
appearance.colorPalette.background,
!appearance.colorPalette.isDark
)
} }
CompositionLocalProvider( CompositionLocalProvider(
@ -185,15 +188,26 @@ class MainActivity : ComponentActivity() {
) { ) {
when (val uri = uri) { when (val uri = uri) {
null -> { null -> {
val playerBottomSheetState = rememberBottomSheetState(
lowerBound = Dimensions.collapsedPlayer, upperBound = maxHeight
)
HomeScreen() HomeScreen()
PlayerView( PlayerView(
layoutState = rememberBottomSheetState( layoutState = playerBottomSheetState,
lowerBound = Dimensions.collapsedPlayer, upperBound = maxHeight
),
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
) )
binder?.player?.let { player ->
ExpandPlayerOnPlaylistChange(
player = player,
expand = {
playerBottomSheetState.expand(tween(500))
}
)
}
} }
else -> IntentUriScreen(uri = uri) else -> IntentUriScreen(uri = uri)
} }
@ -215,3 +229,16 @@ class MainActivity : ComponentActivity() {
} }
val LocalPlayerServiceBinder = staticCompositionLocalOf<PlayerService.Binder?> { null } val LocalPlayerServiceBinder = staticCompositionLocalOf<PlayerService.Binder?> { null }
@Composable
fun ExpandPlayerOnPlaylistChange(player: Player, expand: () -> Unit) {
DisposableEffect(player, expand) {
player.listener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED) {
expand()
}
}
})
}
}

View file

@ -1,22 +1,20 @@
package it.vfsfitvnm.vimusic.ui.components package it.vfsfitvnm.vimusic.ui.components
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.*
import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.DraggableState import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@ -27,39 +25,20 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@Composable @Composable
fun BottomSheet( fun BottomSheet(
state: BottomSheetState, state: BottomSheetState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
peekHeight: Dp = 0.dp, peekHeight: Dp = 0.dp,
elevation: Dp = 8.dp, elevation: Dp = 8.dp,
shape: Shape = RectangleShape,
handleOutsideInteractionsWhenExpanded: Boolean = false,
collapsedContent: @Composable BoxScope.() -> Unit, collapsedContent: @Composable BoxScope.() -> Unit,
content: @Composable BoxScope.() -> Unit content: @Composable BoxScope.() -> Unit
) { ) {
Box {
if (handleOutsideInteractionsWhenExpanded && !state.isCollapsed) {
Spacer(
modifier = Modifier
.pointerInput(state) {
detectTapGestures {
state.collapse()
}
}
.draggableBottomSheet(state)
.drawBehind {
drawRect(color = Color.Black.copy(alpha = 0.5f * state.progress))
}
.fillMaxSize()
)
}
Box( Box(
modifier = modifier modifier = modifier
.offset { .offset {
@ -68,9 +47,39 @@ fun BottomSheet(
.coerceAtLeast(0) .coerceAtLeast(0)
IntOffset(x = 0, y = y) IntOffset(x = 0, y = y)
} }
.shadow(elevation = elevation, shape = shape) .shadow(elevation = elevation)
.clip(shape) .pointerInput(state) {
.draggableBottomSheet(state) var initialValue = 0.dp
val velocityTracker = VelocityTracker()
detectVerticalDragGestures(
onDragStart = {
initialValue = state.value
},
onVerticalDrag = { change, dragAmount ->
velocityTracker.addPointerInputChange(change)
state.dispatchRawDelta(dragAmount)
},
onDragEnd = {
val velocity = velocityTracker.calculateVelocity().y.absoluteValue
velocityTracker.resetTracking()
if (velocity.absoluteValue > 300 && initialValue != state.value) {
if (initialValue > state.value) {
state.collapse()
} else {
state.expand()
}
} else {
if (state.upperBound - state.value > state.value - state.lowerBound) {
state.collapse()
} else {
state.expand()
}
}
}
)
}
.pointerInput(state) { .pointerInput(state) {
if (!state.isRunning && state.isCollapsed) { if (!state.isRunning && state.isCollapsed) {
detectTapGestures { detectTapGestures {
@ -81,11 +90,25 @@ fun BottomSheet(
.fillMaxSize() .fillMaxSize()
) { ) {
if (!state.isCollapsed) { if (!state.isCollapsed) {
BackHandler(onBack = state.collapse) BackHandler(onBack = state::collapseSoft)
content() content()
} }
collapsedContent() if (!state.isExpanded) {
Box(
modifier = Modifier
.graphicsLayer {
alpha = 1f - (state.progress * 16).coerceAtMost(1f)
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
onClick = state::expandSoft
)
.fillMaxWidth()
.height(state.lowerBound),
content = collapsedContent
)
} }
} }
} }
@ -93,25 +116,63 @@ fun BottomSheet(
@Stable @Stable
class BottomSheetState( class BottomSheetState(
draggableState: DraggableState, draggableState: DraggableState,
valueState: State<Dp>, private val coroutineScope: CoroutineScope,
isRunningState: State<Boolean>, private val animatable: Animatable<Dp, AnimationVector1D>,
isCollapsedState: State<Boolean>, private val onWasExpandedChanged: (Boolean) -> Unit,
isExpandedState: State<Boolean>,
progressState: State<Float>,
val lowerBound: Dp,
val upperBound: Dp,
val collapse: () -> Unit,
val expand: () -> Unit,
) : DraggableState by draggableState { ) : DraggableState by draggableState {
val value by valueState val lowerBound: Dp
get() = animatable.lowerBound!!
val isRunning by isRunningState val upperBound: Dp
get() = animatable.upperBound!!
val isCollapsed by isCollapsedState val value by animatable.asState()
val isExpanded by isExpandedState val isRunning by derivedStateOf {
animatable.isRunning
}
val progress by progressState val isCollapsed by derivedStateOf {
value == animatable.lowerBound
}
val isExpanded by derivedStateOf {
value == animatable.upperBound
}
val progress by derivedStateOf {
1f - (animatable.upperBound!! - animatable.value) / (animatable.upperBound!! - animatable.lowerBound!!)
}
fun collapse(animationSpec: AnimationSpec<Dp>) {
onWasExpandedChanged(false)
coroutineScope.launch {
animatable.animateTo(animatable.lowerBound!!, animationSpec)
}
}
fun expand(animationSpec: AnimationSpec<Dp>) {
onWasExpandedChanged(true)
coroutineScope.launch {
animatable.animateTo(animatable.upperBound!!, animationSpec)
}
}
fun collapse() {
collapse(SpringSpec())
}
fun expand() {
expand(SpringSpec())
}
fun collapseSoft() {
collapse(tween(300))
}
fun expandSoft() {
expand(tween(300))
}
fun nestedScrollConnection(initialIsTopReached: Boolean = true): NestedScrollConnection { fun nestedScrollConnection(initialIsTopReached: Boolean = true): NestedScrollConnection {
return object : NestedScrollConnection { return object : NestedScrollConnection {
@ -148,7 +209,7 @@ class BottomSheetState(
if (available.y.absoluteValue > 1000) { if (available.y.absoluteValue > 1000) {
collapse() collapse()
} else { } else {
if (upperBound - value > value - lowerBound) { if (animatable.upperBound!! - value > value - animatable.lowerBound!!) {
collapse() collapse()
} else { } else {
expand() expand()
@ -179,79 +240,23 @@ fun rememberBottomSheetState(lowerBound: Dp, upperBound: Dp): BottomSheetState {
mutableStateOf(false) mutableStateOf(false)
} }
val animatable = remember(lowerBound, upperBound) { return remember(lowerBound, upperBound, coroutineScope) {
val animatable =
Animatable(if (wasExpanded) upperBound else lowerBound, Dp.VectorConverter).also { Animatable(if (wasExpanded) upperBound else lowerBound, Dp.VectorConverter).also {
it.updateBounds(lowerBound.coerceAtMost(upperBound), upperBound) it.updateBounds(lowerBound.coerceAtMost(upperBound), upperBound)
} }
}
return remember(animatable, coroutineScope) {
BottomSheetState( BottomSheetState(
draggableState = DraggableState { delta -> draggableState = DraggableState { delta ->
coroutineScope.launch { coroutineScope.launch {
animatable.snapTo(animatable.value - with(density) { delta.toDp() }) animatable.snapTo(animatable.value - with(density) { delta.toDp() })
} }
}, },
valueState = animatable.asState(), onWasExpandedChanged = {
lowerBound = lowerBound, wasExpanded = it
upperBound = upperBound,
isRunningState = derivedStateOf {
animatable.isRunning
}, },
isCollapsedState = derivedStateOf { coroutineScope = coroutineScope,
animatable.value == lowerBound animatable = animatable
},
isExpandedState = derivedStateOf {
animatable.value == upperBound
},
progressState = derivedStateOf {
1f - (upperBound - animatable.value) / (upperBound - lowerBound)
},
collapse = {
wasExpanded = false
coroutineScope.launch {
animatable.animateTo(animatable.lowerBound!!)
}
},
expand = {
wasExpanded = true
coroutineScope.launch {
animatable.animateTo(animatable.upperBound!!)
}
}
) )
} }
} }
private fun Modifier.draggableBottomSheet(state: BottomSheetState) = pointerInput(state) {
var initialValue = 0.dp
val velocityTracker = VelocityTracker()
detectVerticalDragGestures(
onDragStart = {
initialValue = state.value
},
onVerticalDrag = { change, dragAmount ->
velocityTracker.addPointerInputChange(change)
state.dispatchRawDelta(dragAmount)
},
onDragEnd = {
val velocity = velocityTracker.calculateVelocity().y.absoluteValue
velocityTracker.resetTracking()
if (velocity.absoluteValue > 300 && initialValue != state.value) {
if (initialValue > state.value) {
state.collapse()
} else {
state.expand()
}
} else {
if (state.upperBound - state.value > state.value - state.lowerBound) {
state.collapse()
} else {
state.expand()
}
}
}
)
}

View file

@ -196,7 +196,7 @@ fun CurrentPlaylistView(
.clickable( .clickable(
indication = rememberRipple(bounded = true), indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = layoutState.collapse onClick = layoutState::collapseSoft
) )
.shadow(elevation = 8.dp) .shadow(elevation = 8.dp)
.height(64.dp) .height(64.dp)

View file

@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
@ -17,7 +16,6 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheet
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun PlayerBottomSheet( fun PlayerBottomSheet(
@ -38,11 +36,7 @@ fun PlayerBottomSheet(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.graphicsLayer { .fillMaxSize()
alpha = 1f - (layoutState.progress * 16).coerceAtMost(1f)
}
.fillMaxWidth()
.height(layoutState.lowerBound)
.background(colorPalette.background) .background(colorPalette.background)
) { ) {
Row( Row(

View file

@ -91,17 +91,12 @@ fun PlayerView(
state = layoutState, state = layoutState,
modifier = modifier, modifier = modifier,
collapsedContent = { collapsedContent = {
if (!layoutState.isExpanded) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.height(layoutState.lowerBound)
.fillMaxWidth()
.graphicsLayer {
alpha = 1f - (layoutState.progress * 16).coerceAtMost(1f)
}
.background(colorPalette.elevatedBackground) .background(colorPalette.elevatedBackground)
.fillMaxSize()
.drawBehind { .drawBehind {
val progress = positionAndDuration.first.toFloat() / positionAndDuration.second.absoluteValue val progress = positionAndDuration.first.toFloat() / positionAndDuration.second.absoluteValue
val offset = Dimensions.thumbnails.player.songPreview.toPx() val offset = Dimensions.thumbnails.player.songPreview.toPx()
@ -177,7 +172,6 @@ fun PlayerView(
} }
} }
} }
}
) { ) {
var isShowingLyrics by rememberSaveable { var isShowingLyrics by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
@ -322,7 +316,7 @@ fun PlayerView(
}, },
onSetSleepTimer = {}, onSetSleepTimer = {},
onDismiss = menuState::hide, onDismiss = menuState::hide,
onGlobalRouteEmitted = layoutState.collapse, onGlobalRouteEmitted = layoutState::collapseSoft,
) )
} }
} }
@ -341,7 +335,7 @@ fun PlayerView(
isShowingLyrics = false isShowingLyrics = false
isShowingStatsForNerds = !isShowingStatsForNerds isShowingStatsForNerds = !isShowingStatsForNerds
}, },
onGlobalRouteEmitted = layoutState.collapse, onGlobalRouteEmitted = layoutState::collapseSoft,
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
) )