Skip to content

Commit 8b49440

Browse files
committed
android: update styled slider thumb
1 parent 993f022 commit 8b49440

9 files changed

Lines changed: 175 additions & 67 deletions

File tree

android/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ dependencies {
6262
implementation(libs.haze)
6363
implementation(libs.haze.materials)
6464
implementation(libs.androidx.dynamicanimation)
65+
implementation(libs.androidx.compose.ui)
6566
debugImplementation(libs.androidx.compose.ui.tooling)
6667
implementation(libs.androidx.compose.foundation.layout)
6768
// compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
19.6 KB
Binary file not shown.
18.8 KB
Binary file not shown.

android/app/src/main/java/me/kavishdevar/librepods/composables/StyledButton.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ half4 main(float2 coord) {
137137
}
138138
},
139139
onDrawFront = null,
140-
highlight = { Highlight.AmbientDefault.copy(alpha = 0f) }
140+
highlight = { Highlight.Ambient.copy(alpha = 0f) }
141141
)
142142
} else {
143143
Modifier.drawBackdrop(

android/app/src/main/java/me/kavishdevar/librepods/composables/StyledIconButton.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ half4 main(float2 coord) {
124124
.drawBackdrop(
125125
backdrop = backdrop,
126126
shape = { RoundedCornerShape(56.dp) },
127-
highlight = { Highlight.AmbientDefault.copy(alpha = if (isDarkTheme) 1f else 0f) },
127+
highlight = { Highlight.Ambient.copy(alpha = if (isDarkTheme) 1f else 0f) },
128128
shadow = {
129129
Shadow(
130130
radius = 48f.dp,

android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSlider.kt

Lines changed: 168 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package me.kavishdevar.librepods.composables
2121
import android.content.res.Configuration
2222
import android.util.Log
2323
import androidx.compose.animation.core.Animatable
24+
import androidx.compose.animation.core.FiniteAnimationSpec
2425
import androidx.compose.animation.core.spring
2526
import androidx.compose.foundation.background
2627
import androidx.compose.foundation.gestures.Orientation
@@ -46,21 +47,18 @@ import androidx.compose.runtime.MutableFloatState
4647
import androidx.compose.runtime.derivedStateOf
4748
import androidx.compose.runtime.getValue
4849
import androidx.compose.runtime.mutableFloatStateOf
50+
import androidx.compose.runtime.mutableStateOf
4951
import androidx.compose.runtime.remember
5052
import androidx.compose.runtime.rememberCoroutineScope
53+
import androidx.compose.runtime.setValue
5154
import androidx.compose.ui.Alignment
5255
import androidx.compose.ui.Modifier
5356
import androidx.compose.ui.draw.clip
54-
import androidx.compose.ui.graphics.BlendMode
55-
import androidx.compose.ui.graphics.BlurEffect
5657
import 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
6058
import 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
6462
import androidx.compose.ui.layout.layout
6563
import androidx.compose.ui.layout.onGloballyPositioned
6664
import androidx.compose.ui.layout.onSizeChanged
@@ -71,6 +69,7 @@ import androidx.compose.ui.text.font.Font
7169
import androidx.compose.ui.text.font.FontFamily
7270
import androidx.compose.ui.text.font.FontWeight
7371
import androidx.compose.ui.tooling.preview.Preview
72+
import androidx.compose.ui.unit.Velocity
7473
import androidx.compose.ui.unit.dp
7574
import androidx.compose.ui.unit.sp
7675
import androidx.compose.ui.util.fastCoerceIn
@@ -81,14 +80,129 @@ import com.kyant.backdrop.backdrops.layerBackdrop
8180
import com.kyant.backdrop.backdrops.rememberCombinedBackdrop
8281
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
8382
import com.kyant.backdrop.drawBackdrop
83+
import com.kyant.backdrop.effects.blur
8484
import com.kyant.backdrop.effects.refractionWithDispersion
8585
import com.kyant.backdrop.highlight.Highlight
86+
import com.kyant.backdrop.shadow.InnerShadow
8687
import com.kyant.backdrop.shadow.Shadow
88+
import kotlinx.coroutines.CoroutineScope
8789
import kotlinx.coroutines.launch
8890
import me.kavishdevar.librepods.R
91+
import me.kavishdevar.librepods.utils.inspectDragGestures
8992
import kotlin.math.abs
9093
import 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
93207
fun 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
456561
fun 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
}

android/app/src/main/java/me/kavishdevar/librepods/composables/StyledSwitch.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ fun StyledSwitch(
176176
{ RoundedCornerShape(thumbHeight / 2) },
177177
highlight = {
178178
val progress = progressAnimation.value
179-
Highlight.AmbientDefault.copy(
179+
Highlight.Ambient.copy(
180180
alpha = progress
181181
)
182182
},

android/app/src/main/java/me/kavishdevar/librepods/screens/AirPodsSettingsScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ fun AirPodsSettingsScreen(dev: BluetoothDevice?, service: AirPodsService,
310310
exportedBackdrop = backdrop,
311311
shape = { RoundedCornerShape(0.dp) },
312312
highlight = {
313-
Highlight.AmbientDefault.copy(alpha = 0f)
313+
Highlight.Ambient.copy(alpha = 0f)
314314
}
315315
)
316316
.padding(horizontal = 8.dp),

android/gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dynamicanimation = "1.1.0"
1616
foundationLayout = "1.9.1"
1717
uiTooling = "1.9.1"
1818
mockk = "1.14.3"
19+
ui = "1.9.2"
1920

2021
[libraries]
2122
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
@@ -37,6 +38,7 @@ androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynam
3738
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
3839
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
3940
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
41+
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
4042

4143
[plugins]
4244
android-application = { id = "com.android.application", version.ref = "agp" }

0 commit comments

Comments
 (0)