@@ -21,6 +21,7 @@ package me.kavishdevar.librepods.composables
2121import android.content.res.Configuration
2222import android.util.Log
2323import androidx.compose.animation.core.Animatable
24+ import androidx.compose.animation.core.FiniteAnimationSpec
2425import androidx.compose.animation.core.spring
2526import androidx.compose.foundation.background
2627import androidx.compose.foundation.gestures.Orientation
@@ -46,21 +47,18 @@ import androidx.compose.runtime.MutableFloatState
4647import androidx.compose.runtime.derivedStateOf
4748import androidx.compose.runtime.getValue
4849import androidx.compose.runtime.mutableFloatStateOf
50+ import androidx.compose.runtime.mutableStateOf
4951import androidx.compose.runtime.remember
5052import androidx.compose.runtime.rememberCoroutineScope
53+ import androidx.compose.runtime.setValue
5154import androidx.compose.ui.Alignment
5255import androidx.compose.ui.Modifier
5356import androidx.compose.ui.draw.clip
54- import androidx.compose.ui.graphics.BlendMode
55- import androidx.compose.ui.graphics.BlurEffect
5657import androidx.compose.ui.graphics.Color
57- import androidx.compose.ui.graphics.TileMode
58- import androidx.compose.ui.graphics.drawOutline
59- import androidx.compose.ui.graphics.drawscope.translate
6058import androidx.compose.ui.graphics.graphicsLayer
61- import androidx.compose.ui.graphics.layer.CompositingStrategy
62- import androidx.compose.ui.graphics.layer.drawLayer
63- import androidx.compose.ui.graphics.rememberGraphicsLayer
59+ import androidx.compose.ui.input.pointer.pointerInput
60+ import androidx.compose.ui.input.pointer.util.VelocityTracker
61+ import androidx.compose.ui.input.pointer.util.addPointerInputChange
6462import androidx.compose.ui.layout.layout
6563import androidx.compose.ui.layout.onGloballyPositioned
6664import androidx.compose.ui.layout.onSizeChanged
@@ -71,6 +69,7 @@ import androidx.compose.ui.text.font.Font
7169import androidx.compose.ui.text.font.FontFamily
7270import androidx.compose.ui.text.font.FontWeight
7371import androidx.compose.ui.tooling.preview.Preview
72+ import androidx.compose.ui.unit.Velocity
7473import androidx.compose.ui.unit.dp
7574import androidx.compose.ui.unit.sp
7675import androidx.compose.ui.util.fastCoerceIn
@@ -81,14 +80,129 @@ import com.kyant.backdrop.backdrops.layerBackdrop
8180import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
8281import com.kyant.backdrop.backdrops.rememberLayerBackdrop
8382import com.kyant.backdrop.drawBackdrop
83+ import com.kyant.backdrop.effects.blur
8484import com.kyant.backdrop.effects.refractionWithDispersion
8585import com.kyant.backdrop.highlight.Highlight
86+ import com.kyant.backdrop.shadow.InnerShadow
8687import com.kyant.backdrop.shadow.Shadow
88+ import kotlinx.coroutines.CoroutineScope
8789import kotlinx.coroutines.launch
8890import me.kavishdevar.librepods.R
91+ import me.kavishdevar.librepods.utils.inspectDragGestures
8992import kotlin.math.abs
9093import kotlin.math.roundToInt
9194
95+ @Composable
96+ fun rememberMomentumAnimation (
97+ maxScale : Float ,
98+ progressAnimationSpec : FiniteAnimationSpec <Float > =
99+ spring(1f, 1000f, 0.01f),
100+ velocityAnimationSpec : FiniteAnimationSpec <Float > =
101+ spring(0.5f, 250f, 5f),
102+ scaleXAnimationSpec : FiniteAnimationSpec <Float > =
103+ spring(0.4f, 400f, 0.01f),
104+ scaleYAnimationSpec : FiniteAnimationSpec <Float > =
105+ spring(0.6f, 400f, 0.01f)
106+ ): MomentumAnimation {
107+ val animationScope = rememberCoroutineScope()
108+ return remember(
109+ maxScale,
110+ animationScope,
111+ progressAnimationSpec,
112+ velocityAnimationSpec,
113+ scaleXAnimationSpec,
114+ scaleYAnimationSpec
115+ ) {
116+ MomentumAnimation (
117+ maxScale = maxScale,
118+ animationScope = animationScope,
119+ progressAnimationSpec = progressAnimationSpec,
120+ velocityAnimationSpec = velocityAnimationSpec,
121+ scaleXAnimationSpec = scaleXAnimationSpec,
122+ scaleYAnimationSpec = scaleYAnimationSpec
123+ )
124+ }
125+ }
126+
127+ class MomentumAnimation (
128+ val maxScale : Float ,
129+ private val animationScope : CoroutineScope ,
130+ private val progressAnimationSpec : FiniteAnimationSpec <Float >,
131+ private val velocityAnimationSpec : FiniteAnimationSpec <Float >,
132+ private val scaleXAnimationSpec : FiniteAnimationSpec <Float >,
133+ private val scaleYAnimationSpec : FiniteAnimationSpec <Float >
134+ ) {
135+
136+ private val velocityTracker = VelocityTracker ()
137+
138+ private val progressAnimation = Animatable (0f )
139+ private val velocityAnimation = Animatable (0f )
140+ private val scaleXAnimation = Animatable (1f )
141+ private val scaleYAnimation = Animatable (1f )
142+
143+ val progress: Float get() = progressAnimation.value
144+ val velocity: Float get() = velocityAnimation.value
145+ val scaleX: Float get() = scaleXAnimation.value
146+ val scaleY: Float get() = scaleYAnimation.value
147+
148+ var isDragging: Boolean by mutableStateOf(false )
149+ private set
150+
151+ val modifier: Modifier = Modifier .pointerInput(Unit ) {
152+ inspectDragGestures(
153+ onDragStart = {
154+ isDragging = true
155+ velocityTracker.resetTracking()
156+ startPressingAnimation()
157+ },
158+ onDragEnd = { change ->
159+ isDragging = false
160+ val velocity = velocityTracker.calculateVelocity()
161+ updateVelocity(velocity)
162+ velocityTracker.addPointerInputChange(change)
163+ velocityTracker.resetTracking()
164+ endPressingAnimation()
165+ settleVelocity()
166+ },
167+ onDragCancel = {
168+ isDragging = false
169+ velocityTracker.resetTracking()
170+ endPressingAnimation()
171+ settleVelocity()
172+ }
173+ ) { change, _ ->
174+ isDragging = true
175+ velocityTracker.addPointerInputChange(change)
176+ val velocity = velocityTracker.calculateVelocity()
177+ updateVelocity(velocity)
178+ }
179+ }
180+
181+ private fun updateVelocity (velocity : Velocity ) {
182+ animationScope.launch { velocityAnimation.animateTo(velocity.x, velocityAnimationSpec) }
183+ }
184+
185+ private fun settleVelocity () {
186+ animationScope.launch { velocityAnimation.animateTo(0f , velocityAnimationSpec) }
187+ }
188+
189+ fun startPressingAnimation () {
190+ animationScope.launch {
191+ launch { progressAnimation.animateTo(1f , progressAnimationSpec) }
192+ launch { scaleXAnimation.animateTo(maxScale, scaleXAnimationSpec) }
193+ launch { scaleYAnimation.animateTo(maxScale, scaleYAnimationSpec) }
194+ }
195+ }
196+
197+ fun endPressingAnimation () {
198+ animationScope.launch {
199+ launch { progressAnimation.animateTo(0f , progressAnimationSpec) }
200+ launch { scaleXAnimation.animateTo(1f , scaleXAnimationSpec) }
201+ launch { scaleYAnimation.animateTo(1f , scaleYAnimationSpec) }
202+ }
203+ }
204+ }
205+
92206@Composable
93207fun StyledSlider (
94208 label : String? = null,
@@ -122,21 +236,15 @@ fun StyledSlider(
122236 }
123237 }
124238
125- val animationScope = rememberCoroutineScope()
126- val progressAnimationSpec = spring(0.5f , 300f , 0.001f )
127- val progressAnimation = remember { Animatable (0f ) }
128- val innerShadowLayer =
129- rememberGraphicsLayer().apply {
130- compositingStrategy = CompositingStrategy .Offscreen
131- }
132-
133239 val sliderBackdrop = rememberLayerBackdrop()
134240 val trackWidthState = remember { mutableFloatStateOf(0f ) }
135241 val trackPositionState = remember { mutableFloatStateOf(0f ) }
136242 val startIconWidthState = remember { mutableFloatStateOf(0f ) }
137243 val endIconWidthState = remember { mutableFloatStateOf(0f ) }
138244 val density = LocalDensity .current
139245
246+ val momentumAnimation = rememberMomentumAnimation(maxScale = 1.5f )
247+
140248 val content = @Composable {
141249 Box (
142250 Modifier
@@ -301,10 +409,22 @@ fun StyledSlider(
301409 Box (
302410 Modifier
303411 .graphicsLayer {
412+ // val startOffset =
413+ // if (startIcon != null) startIconWidthState.floatValue + with(density) { 24.dp.toPx() } else with(density) { 12.dp.toPx() }
414+ // translationX =
415+ // startOffset + fraction * trackWidthState.floatValue - size.width / 2f
304416 val startOffset =
305- if (startIcon != null ) startIconWidthState.floatValue + with (density) { 24 .dp.toPx() } else with (density) { 12 .dp.toPx() }
417+ if (startIcon != null )
418+ startIconWidthState.floatValue + with (density) { 24 .dp.toPx() }
419+ else
420+ with (density) { 8 .dp.toPx() }
421+
306422 translationX =
307- startOffset + fraction * trackWidthState.floatValue - size.width / 2f
423+ (startOffset + fraction * trackWidthState.floatValue - size.width / 2f )
424+ .fastCoerceIn(
425+ startOffset - size.width / 4f ,
426+ startOffset + trackWidthState.floatValue - size.width * 3f / 4f
427+ )
308428 translationY = if (startLabel != null || endLabel != null ) trackPositionState.floatValue + with (density) { 26 .dp.toPx() } + size.height / 2f else trackPositionState.floatValue + with (density) { 8 .dp.toPx() }
309429 }
310430 .draggable(
@@ -326,67 +446,52 @@ fun StyledSlider(
326446 Orientation .Horizontal ,
327447 startDragImmediately = true ,
328448 onDragStarted = {
329- animationScope.launch {
330- progressAnimation.animateTo(1f , progressAnimationSpec)
331- }
449+ // Remove this block as momentumAnimation handles pressing
332450 },
333451 onDragStopped = {
334- animationScope.launch {
335- progressAnimation.animateTo(0f , progressAnimationSpec)
336- onValueChange((mutableFloatState.floatValue * 100 ).roundToInt() / 100f )
337- }
452+ // Remove this block as momentumAnimation handles pressing
453+ onValueChange((mutableFloatState.floatValue * 100 ).roundToInt() / 100f )
338454 }
339455 )
456+ .then(momentumAnimation.modifier)
340457 .drawBackdrop(
341458 rememberCombinedBackdrop(backdrop, sliderBackdrop),
342459 { RoundedCornerShape (28 .dp) },
343460 highlight = {
344- val progress = progressAnimation.value
345- Highlight .AmbientDefault .copy(alpha = progress)
461+ val progress = momentumAnimation.progress
462+ Highlight .Ambient .copy(alpha = progress)
346463 },
347464 shadow = {
348465 Shadow (
349466 radius = 4f .dp,
350467 color = Color .Black .copy(0.05f )
351468 )
352469 },
470+ innerShadow = {
471+ val progress = momentumAnimation.progress
472+ InnerShadow (
473+ radius = 4f .dp * progress,
474+ alpha = progress
475+ )
476+ },
353477 layerBlock = {
354- val progress = progressAnimation.value
355- val scale = lerp(1f , 1.5f , progress)
356- scaleX = scale
357- scaleY = scale
478+ scaleX = momentumAnimation.scaleX
479+ scaleY = momentumAnimation.scaleY
480+ val velocity = momentumAnimation.velocity / 5000f
481+ scaleX / = 1f - (velocity * 0.75f ).fastCoerceIn(- 0.15f , 0.15f )
482+ scaleY * = 1f - (velocity * 0.25f ).fastCoerceIn(- 0.15f , 0.15f )
358483 },
359484 onDrawSurface = {
360- val progress = progressAnimation.value.fastCoerceIn(0f , 1f )
361-
362- val shape = RoundedCornerShape (28 .dp)
363- val outline = shape.createOutline(size, layoutDirection, this )
364- val innerShadowOffset = 4f .dp.toPx()
365- val innerShadowBlurRadius = 4f .dp.toPx()
366-
367- innerShadowLayer.alpha = progress
368- innerShadowLayer.renderEffect =
369- BlurEffect (
370- innerShadowBlurRadius,
371- innerShadowBlurRadius,
372- TileMode .Decal
373- )
374- innerShadowLayer.record {
375- drawOutline(outline, Color .Black .copy(0.2f ))
376- translate(0f , innerShadowOffset) {
377- drawOutline(
378- outline,
379- Color .Transparent ,
380- blendMode = BlendMode .Clear
381- )
382- }
383- }
384- drawLayer(innerShadowLayer)
385-
386- drawRect(Color .White .copy(1f - progress))
485+ val progress = momentumAnimation.progress
486+ drawRect(Color .White .copy(alpha = 1f - progress))
387487 },
388488 effects = {
389- refractionWithDispersion(6f .dp.toPx(), size.height / 2f )
489+ val progress = momentumAnimation.progress
490+ blur(8f .dp.toPx() * (1f - progress))
491+ refractionWithDispersion(
492+ height = 6f .dp.toPx() * progress,
493+ amount = size.height / 2f * progress
494+ )
390495 }
391496 )
392497 .size(40f .dp, 24f .dp)
@@ -454,7 +559,7 @@ private fun snapIfClose(value: Float, points: List<Float>, threshold: Float = 0.
454559@Preview(uiMode = Configuration .UI_MODE_NIGHT_YES )
455560@Composable
456561fun StyledSliderPreview () {
457- val a = remember { mutableFloatStateOf(1f ) }
562+ val a = remember { mutableFloatStateOf(0.5f ) }
458563 Box (
459564 Modifier
460565 .background(if (isSystemInDarkTheme()) Color (0xFF000000 ) else Color (0xFFF0F0F0 ))
@@ -471,11 +576,11 @@ fun StyledSliderPreview() {
471576 a.floatValue = it
472577 },
473578 valueRange = 0f .. 2f ,
474- snapPoints = listOf (0f , 0.5f , 1f , 1.5f , 2f ),
579+ snapPoints = listOf (1f ),
475580 snapThreshold = 0.1f ,
476581 independent = true ,
477- startLabel = " A" ,
478- endLabel = " B" ,
582+ startIcon = " A" ,
583+ endIcon = " B" ,
479584 )
480585 }
481586 }
0 commit comments