|
|
|
|
@@ -21,6 +21,7 @@ package me.kavishdevar.librepods.composables
|
|
|
|
|
import android.content.res.Configuration
|
|
|
|
|
import android.util.Log
|
|
|
|
|
import androidx.compose.animation.core.Animatable
|
|
|
|
|
import androidx.compose.animation.core.FiniteAnimationSpec
|
|
|
|
|
import androidx.compose.animation.core.spring
|
|
|
|
|
import androidx.compose.foundation.background
|
|
|
|
|
import androidx.compose.foundation.gestures.Orientation
|
|
|
|
|
@@ -46,21 +47,18 @@ import androidx.compose.runtime.MutableFloatState
|
|
|
|
|
import androidx.compose.runtime.derivedStateOf
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
|
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
|
|
import androidx.compose.runtime.remember
|
|
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
|
|
import androidx.compose.runtime.setValue
|
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
import androidx.compose.ui.draw.clip
|
|
|
|
|
import androidx.compose.ui.graphics.BlendMode
|
|
|
|
|
import androidx.compose.ui.graphics.BlurEffect
|
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
|
import androidx.compose.ui.graphics.TileMode
|
|
|
|
|
import androidx.compose.ui.graphics.drawOutline
|
|
|
|
|
import androidx.compose.ui.graphics.drawscope.translate
|
|
|
|
|
import androidx.compose.ui.graphics.graphicsLayer
|
|
|
|
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
|
|
|
|
import androidx.compose.ui.graphics.layer.drawLayer
|
|
|
|
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
|
|
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
|
|
|
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
|
|
|
|
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
|
|
|
|
import androidx.compose.ui.layout.layout
|
|
|
|
|
import androidx.compose.ui.layout.onGloballyPositioned
|
|
|
|
|
import androidx.compose.ui.layout.onSizeChanged
|
|
|
|
|
@@ -71,6 +69,7 @@ import androidx.compose.ui.text.font.Font
|
|
|
|
|
import androidx.compose.ui.text.font.FontFamily
|
|
|
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
|
|
|
import androidx.compose.ui.tooling.preview.Preview
|
|
|
|
|
import androidx.compose.ui.unit.Velocity
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
import androidx.compose.ui.unit.sp
|
|
|
|
|
import androidx.compose.ui.util.fastCoerceIn
|
|
|
|
|
@@ -81,14 +80,129 @@ import com.kyant.backdrop.backdrops.layerBackdrop
|
|
|
|
|
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
|
|
|
|
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
|
|
|
|
import com.kyant.backdrop.drawBackdrop
|
|
|
|
|
import com.kyant.backdrop.effects.blur
|
|
|
|
|
import com.kyant.backdrop.effects.refractionWithDispersion
|
|
|
|
|
import com.kyant.backdrop.highlight.Highlight
|
|
|
|
|
import com.kyant.backdrop.shadow.InnerShadow
|
|
|
|
|
import com.kyant.backdrop.shadow.Shadow
|
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import me.kavishdevar.librepods.R
|
|
|
|
|
import me.kavishdevar.librepods.utils.inspectDragGestures
|
|
|
|
|
import kotlin.math.abs
|
|
|
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun rememberMomentumAnimation(
|
|
|
|
|
maxScale: Float,
|
|
|
|
|
progressAnimationSpec: FiniteAnimationSpec<Float> =
|
|
|
|
|
spring(1f, 1000f, 0.01f),
|
|
|
|
|
velocityAnimationSpec: FiniteAnimationSpec<Float> =
|
|
|
|
|
spring(0.5f, 250f, 5f),
|
|
|
|
|
scaleXAnimationSpec: FiniteAnimationSpec<Float> =
|
|
|
|
|
spring(0.4f, 400f, 0.01f),
|
|
|
|
|
scaleYAnimationSpec: FiniteAnimationSpec<Float> =
|
|
|
|
|
spring(0.6f, 400f, 0.01f)
|
|
|
|
|
): MomentumAnimation {
|
|
|
|
|
val animationScope = rememberCoroutineScope()
|
|
|
|
|
return remember(
|
|
|
|
|
maxScale,
|
|
|
|
|
animationScope,
|
|
|
|
|
progressAnimationSpec,
|
|
|
|
|
velocityAnimationSpec,
|
|
|
|
|
scaleXAnimationSpec,
|
|
|
|
|
scaleYAnimationSpec
|
|
|
|
|
) {
|
|
|
|
|
MomentumAnimation(
|
|
|
|
|
maxScale = maxScale,
|
|
|
|
|
animationScope = animationScope,
|
|
|
|
|
progressAnimationSpec = progressAnimationSpec,
|
|
|
|
|
velocityAnimationSpec = velocityAnimationSpec,
|
|
|
|
|
scaleXAnimationSpec = scaleXAnimationSpec,
|
|
|
|
|
scaleYAnimationSpec = scaleYAnimationSpec
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MomentumAnimation(
|
|
|
|
|
val maxScale: Float,
|
|
|
|
|
private val animationScope: CoroutineScope,
|
|
|
|
|
private val progressAnimationSpec: FiniteAnimationSpec<Float>,
|
|
|
|
|
private val velocityAnimationSpec: FiniteAnimationSpec<Float>,
|
|
|
|
|
private val scaleXAnimationSpec: FiniteAnimationSpec<Float>,
|
|
|
|
|
private val scaleYAnimationSpec: FiniteAnimationSpec<Float>
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
private val velocityTracker = VelocityTracker()
|
|
|
|
|
|
|
|
|
|
private val progressAnimation = Animatable(0f)
|
|
|
|
|
private val velocityAnimation = Animatable(0f)
|
|
|
|
|
private val scaleXAnimation = Animatable(1f)
|
|
|
|
|
private val scaleYAnimation = Animatable(1f)
|
|
|
|
|
|
|
|
|
|
val progress: Float get() = progressAnimation.value
|
|
|
|
|
val velocity: Float get() = velocityAnimation.value
|
|
|
|
|
val scaleX: Float get() = scaleXAnimation.value
|
|
|
|
|
val scaleY: Float get() = scaleYAnimation.value
|
|
|
|
|
|
|
|
|
|
var isDragging: Boolean by mutableStateOf(false)
|
|
|
|
|
private set
|
|
|
|
|
|
|
|
|
|
val modifier: Modifier = Modifier.pointerInput(Unit) {
|
|
|
|
|
inspectDragGestures(
|
|
|
|
|
onDragStart = {
|
|
|
|
|
isDragging = true
|
|
|
|
|
velocityTracker.resetTracking()
|
|
|
|
|
startPressingAnimation()
|
|
|
|
|
},
|
|
|
|
|
onDragEnd = { change ->
|
|
|
|
|
isDragging = false
|
|
|
|
|
val velocity = velocityTracker.calculateVelocity()
|
|
|
|
|
updateVelocity(velocity)
|
|
|
|
|
velocityTracker.addPointerInputChange(change)
|
|
|
|
|
velocityTracker.resetTracking()
|
|
|
|
|
endPressingAnimation()
|
|
|
|
|
settleVelocity()
|
|
|
|
|
},
|
|
|
|
|
onDragCancel = {
|
|
|
|
|
isDragging = false
|
|
|
|
|
velocityTracker.resetTracking()
|
|
|
|
|
endPressingAnimation()
|
|
|
|
|
settleVelocity()
|
|
|
|
|
}
|
|
|
|
|
) { change, _ ->
|
|
|
|
|
isDragging = true
|
|
|
|
|
velocityTracker.addPointerInputChange(change)
|
|
|
|
|
val velocity = velocityTracker.calculateVelocity()
|
|
|
|
|
updateVelocity(velocity)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun updateVelocity(velocity: Velocity) {
|
|
|
|
|
animationScope.launch { velocityAnimation.animateTo(velocity.x, velocityAnimationSpec) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun settleVelocity() {
|
|
|
|
|
animationScope.launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun startPressingAnimation() {
|
|
|
|
|
animationScope.launch {
|
|
|
|
|
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
|
|
|
|
|
launch { scaleXAnimation.animateTo(maxScale, scaleXAnimationSpec) }
|
|
|
|
|
launch { scaleYAnimation.animateTo(maxScale, scaleYAnimationSpec) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun endPressingAnimation() {
|
|
|
|
|
animationScope.launch {
|
|
|
|
|
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
|
|
|
|
|
launch { scaleXAnimation.animateTo(1f, scaleXAnimationSpec) }
|
|
|
|
|
launch { scaleYAnimation.animateTo(1f, scaleYAnimationSpec) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
fun StyledSlider(
|
|
|
|
|
label: String? = null,
|
|
|
|
|
@@ -122,14 +236,6 @@ fun StyledSlider(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val animationScope = rememberCoroutineScope()
|
|
|
|
|
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
|
|
|
|
|
val progressAnimation = remember { Animatable(0f) }
|
|
|
|
|
val innerShadowLayer =
|
|
|
|
|
rememberGraphicsLayer().apply {
|
|
|
|
|
compositingStrategy = CompositingStrategy.Offscreen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val sliderBackdrop = rememberLayerBackdrop()
|
|
|
|
|
val trackWidthState = remember { mutableFloatStateOf(0f) }
|
|
|
|
|
val trackPositionState = remember { mutableFloatStateOf(0f) }
|
|
|
|
|
@@ -137,6 +243,8 @@ fun StyledSlider(
|
|
|
|
|
val endIconWidthState = remember { mutableFloatStateOf(0f) }
|
|
|
|
|
val density = LocalDensity.current
|
|
|
|
|
|
|
|
|
|
val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f)
|
|
|
|
|
|
|
|
|
|
val content = @Composable {
|
|
|
|
|
Box(
|
|
|
|
|
Modifier
|
|
|
|
|
@@ -301,10 +409,22 @@ fun StyledSlider(
|
|
|
|
|
Box(
|
|
|
|
|
Modifier
|
|
|
|
|
.graphicsLayer {
|
|
|
|
|
// val startOffset =
|
|
|
|
|
// if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else with(density) { 12.dp.toPx() }
|
|
|
|
|
// translationX =
|
|
|
|
|
// startOffset + fraction * trackWidthState.floatValue - size.width / 2f
|
|
|
|
|
val startOffset =
|
|
|
|
|
if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else with(density) { 12.dp.toPx() }
|
|
|
|
|
if (startIcon != null)
|
|
|
|
|
startIconWidthState.floatValue + with(density) { 24.dp.toPx() }
|
|
|
|
|
else
|
|
|
|
|
with(density) { 8.dp.toPx() }
|
|
|
|
|
|
|
|
|
|
translationX =
|
|
|
|
|
startOffset + fraction * trackWidthState.floatValue - size.width / 2f
|
|
|
|
|
(startOffset + fraction * trackWidthState.floatValue - size.width / 2f)
|
|
|
|
|
.fastCoerceIn(
|
|
|
|
|
startOffset - size.width / 4f,
|
|
|
|
|
startOffset + trackWidthState.floatValue - size.width * 3f / 4f
|
|
|
|
|
)
|
|
|
|
|
translationY = if (startLabel != null || endLabel != null) trackPositionState.floatValue + with(density) { 26.dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with(density) { 8.dp.toPx() }
|
|
|
|
|
}
|
|
|
|
|
.draggable(
|
|
|
|
|
@@ -326,23 +446,20 @@ fun StyledSlider(
|
|
|
|
|
Orientation.Horizontal,
|
|
|
|
|
startDragImmediately = true,
|
|
|
|
|
onDragStarted = {
|
|
|
|
|
animationScope.launch {
|
|
|
|
|
progressAnimation.animateTo(1f, progressAnimationSpec)
|
|
|
|
|
}
|
|
|
|
|
// Remove this block as momentumAnimation handles pressing
|
|
|
|
|
},
|
|
|
|
|
onDragStopped = {
|
|
|
|
|
animationScope.launch {
|
|
|
|
|
progressAnimation.animateTo(0f, progressAnimationSpec)
|
|
|
|
|
onValueChange((mutableFloatState.floatValue * 100).roundToInt() / 100f)
|
|
|
|
|
}
|
|
|
|
|
// Remove this block as momentumAnimation handles pressing
|
|
|
|
|
onValueChange((mutableFloatState.floatValue * 100).roundToInt() / 100f)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.then(momentumAnimation.modifier)
|
|
|
|
|
.drawBackdrop(
|
|
|
|
|
rememberCombinedBackdrop(backdrop, sliderBackdrop),
|
|
|
|
|
{ RoundedCornerShape(28.dp) },
|
|
|
|
|
highlight = {
|
|
|
|
|
val progress = progressAnimation.value
|
|
|
|
|
Highlight.AmbientDefault.copy(alpha = progress)
|
|
|
|
|
val progress = momentumAnimation.progress
|
|
|
|
|
Highlight.Ambient.copy(alpha = progress)
|
|
|
|
|
},
|
|
|
|
|
shadow = {
|
|
|
|
|
Shadow(
|
|
|
|
|
@@ -350,43 +467,31 @@ fun StyledSlider(
|
|
|
|
|
color = Color.Black.copy(0.05f)
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
innerShadow = {
|
|
|
|
|
val progress = momentumAnimation.progress
|
|
|
|
|
InnerShadow(
|
|
|
|
|
radius = 4f.dp * progress,
|
|
|
|
|
alpha = progress
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
layerBlock = {
|
|
|
|
|
val progress = progressAnimation.value
|
|
|
|
|
val scale = lerp(1f, 1.5f, progress)
|
|
|
|
|
scaleX = scale
|
|
|
|
|
scaleY = scale
|
|
|
|
|
scaleX = momentumAnimation.scaleX
|
|
|
|
|
scaleY = momentumAnimation.scaleY
|
|
|
|
|
val velocity = momentumAnimation.velocity / 5000f
|
|
|
|
|
scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.15f, 0.15f)
|
|
|
|
|
scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.15f, 0.15f)
|
|
|
|
|
},
|
|
|
|
|
onDrawSurface = {
|
|
|
|
|
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
|
|
|
|
|
|
|
|
|
|
val shape = RoundedCornerShape(28.dp)
|
|
|
|
|
val outline = shape.createOutline(size, layoutDirection, this)
|
|
|
|
|
val innerShadowOffset = 4f.dp.toPx()
|
|
|
|
|
val innerShadowBlurRadius = 4f.dp.toPx()
|
|
|
|
|
|
|
|
|
|
innerShadowLayer.alpha = progress
|
|
|
|
|
innerShadowLayer.renderEffect =
|
|
|
|
|
BlurEffect(
|
|
|
|
|
innerShadowBlurRadius,
|
|
|
|
|
innerShadowBlurRadius,
|
|
|
|
|
TileMode.Decal
|
|
|
|
|
)
|
|
|
|
|
innerShadowLayer.record {
|
|
|
|
|
drawOutline(outline, Color.Black.copy(0.2f))
|
|
|
|
|
translate(0f, innerShadowOffset) {
|
|
|
|
|
drawOutline(
|
|
|
|
|
outline,
|
|
|
|
|
Color.Transparent,
|
|
|
|
|
blendMode = BlendMode.Clear
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
drawLayer(innerShadowLayer)
|
|
|
|
|
|
|
|
|
|
drawRect(Color.White.copy(1f - progress))
|
|
|
|
|
val progress = momentumAnimation.progress
|
|
|
|
|
drawRect(Color.White.copy(alpha = 1f - progress))
|
|
|
|
|
},
|
|
|
|
|
effects = {
|
|
|
|
|
refractionWithDispersion(6f.dp.toPx(), size.height / 2f)
|
|
|
|
|
val progress = momentumAnimation.progress
|
|
|
|
|
blur(8f.dp.toPx() * (1f - progress))
|
|
|
|
|
refractionWithDispersion(
|
|
|
|
|
height = 6f.dp.toPx() * progress,
|
|
|
|
|
amount = size.height / 2f * progress
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
.size(40f.dp, 24f.dp)
|
|
|
|
|
@@ -454,7 +559,7 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
|
|
|
|
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
|
|
|
@Composable
|
|
|
|
|
fun StyledSliderPreview() {
|
|
|
|
|
val a = remember { mutableFloatStateOf(1f) }
|
|
|
|
|
val a = remember { mutableFloatStateOf(0.5f) }
|
|
|
|
|
Box(
|
|
|
|
|
Modifier
|
|
|
|
|
.background(if (isSystemInDarkTheme()) Color(0xFF000000) else Color(0xFFF0F0F0))
|
|
|
|
|
@@ -471,11 +576,11 @@ fun StyledSliderPreview() {
|
|
|
|
|
a.floatValue = it
|
|
|
|
|
},
|
|
|
|
|
valueRange = 0f..2f,
|
|
|
|
|
snapPoints = listOf(0f, 0.5f, 1f, 1.5f, 2f),
|
|
|
|
|
snapPoints = listOf(1f),
|
|
|
|
|
snapThreshold = 0.1f,
|
|
|
|
|
independent = true,
|
|
|
|
|
startLabel = "A",
|
|
|
|
|
endLabel = "B",
|
|
|
|
|
startIcon = "A",
|
|
|
|
|
endIcon = "B",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|