Add slider demo

This commit is contained in:
Kyant0 committed 2025-09-21 17:23:14 +08:00
1 parent 4d7797e305
commit 12ea7d2357
12 files changed
+252 -14

No files matched your search

@@ -33,7 +33,7 @@ fun rememberBackdrop(
}
@Stable
open class Backdrop internal constructor(
class Backdrop internal constructor(
val graphicsLayer: GraphicsLayer,
internal val drawLayer: ContentDrawScope.() -> Unit
) : BackdropDrawer {
@@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.nativeCanvas
@@ -221,7 +222,10 @@ internal class HighlightNode(
override fun onAttach() {
val graphicsContext = requireGraphicsContext()
graphicsLayer = graphicsContext.createGraphicsLayer()
graphicsLayer =
graphicsContext.createGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Offscreen
}
}
override fun onDetach() {
Binary file not shown.
@@ -2,12 +2,16 @@ package com.kyant.backdrop.catalog
enum class CatalogDestination {
Home,
Buttons,
Slider,
BottomTabs,
ControlCenter,
Dialog,
GlassPlayground,
AdaptiveLuminanceGlass,
ControlCenter,
BottomTabs,
ScrollContainer,
LazyScrollContainer,
}
@@ -15,6 +15,7 @@ import com.kyant.backdrop.catalog.destinations.GlassPlaygroundContent
import com.kyant.backdrop.catalog.destinations.HomeContent
import com.kyant.backdrop.catalog.destinations.LazyScrollContainerContent
import com.kyant.backdrop.catalog.destinations.ScrollContainerContent
import com.kyant.backdrop.catalog.destinations.SliderContent
@Composable
fun MainContent() {
@@ -28,10 +29,11 @@ fun MainContent() {
CatalogDestination.Home -> HomeContent(onNavigate = { destination = it })
CatalogDestination.Buttons -> ButtonsContent()
CatalogDestination.Slider -> SliderContent()
CatalogDestination.BottomTabs -> BottomTabsContent()
CatalogDestination.Dialog -> DialogContent()
CatalogDestination.ControlCenter -> ControlCenterContent()
CatalogDestination.Dialog -> DialogContent()
CatalogDestination.GlassPlayground -> GlassPlaygroundContent()
CatalogDestination.AdaptiveLuminanceGlass -> AdaptiveLuminanceGlassContent()
@@ -58,7 +58,7 @@ fun LiquidButton(
content: @Composable RowScope.() -> Unit
) {
val animationScope = rememberCoroutineScope()
val scaleProgressAnimation = remember { Animatable(0f) }
val progressAnimation = remember { Animatable(0f) }
var pressStartPosition by remember { mutableStateOf(Offset.Zero) }
val offsetAnimation = remember { Animatable(Offset.Zero, Offset.VectorConverter) }
@@ -93,9 +93,9 @@ half4 main(float2 coord) {
val width = size.width
val height = size.height
val scaleProgress = scaleProgressAnimation.value
val progress = progressAnimation.value
val maxScale = 0.1f
val scale = lerp(1f, 1f + maxScale, scaleProgress)
val scale = lerp(1f, 1f + maxScale, progress)
val maxOffset = size.minDimension
val initialDerivative = 0.05f
@@ -128,7 +128,7 @@ half4 main(float2 coord) {
},
onDrawFront = if (isInteractive) {
{
val progress = scaleProgressAnimation.value.fastCoerceIn(0f, 1f)
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
if (progress > 0f) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && interactiveHighlightShader != null) {
drawRect(
@@ -179,7 +179,7 @@ half4 main(float2 coord) {
val offsetAnimationSpec = spring(1f, 300f, Offset.VisibilityThreshold)
val onDragStop: () -> Unit = {
animationScope.launch {
launch { scaleProgressAnimation.animateTo(0f, progressAnimationSpec) }
launch { progressAnimation.animateTo(0f, progressAnimationSpec) }
launch { offsetAnimation.animateTo(Offset.Zero, offsetAnimationSpec) }
}
}
@@ -187,7 +187,7 @@ half4 main(float2 coord) {
onDragStart = { down ->
pressStartPosition = down.position
animationScope.launch {
launch { scaleProgressAnimation.animateTo(1f, progressAnimationSpec) }
launch { progressAnimation.animateTo(1f, progressAnimationSpec) }
launch { offsetAnimation.snapTo(Offset.Zero) }
}
},
@@ -0,0 +1,190 @@
package com.kyant.backdrop.catalog.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastRoundToInt
import androidx.compose.ui.util.lerp
import com.kyant.backdrop.Backdrop
import com.kyant.backdrop.backdrop
import com.kyant.backdrop.drawBackdrop
import com.kyant.backdrop.effects.dispersion
import com.kyant.backdrop.effects.refraction
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.rememberBackdrop
import com.kyant.backdrop.rememberCombinedBackdropDrawer
import com.kyant.backdrop.shadow.Shadow
import com.kyant.capsule.ContinuousCapsule
import kotlinx.coroutines.launch
@Composable
fun LiquidSlider(
value: () -> Float,
onValueChange: (Float) -> Unit,
valueRange: ClosedFloatingPointRange<Float>,
backdrop: Backdrop,
modifier: Modifier = Modifier
) {
val isLightTheme = !isSystemInDarkTheme()
val accentColor =
if (isLightTheme) Color(0xFF0088FF)
else Color(0xFF0091FF)
val trackColor =
if (isLightTheme) Color(0xFF787878).copy(0.2f)
else Color(0xFF787880).copy(0.36f)
val fraction by remember {
derivedStateOf {
((value() - valueRange.start) / (valueRange.endInclusive - valueRange.start))
.fastCoerceIn(0f, 1f)
}
}
val animationScope = rememberCoroutineScope()
val progressAnimationSpec = spring(0.5f, 300f, 0.001f)
val progressAnimation = remember { Animatable(0f) }
val trackBackdrop = rememberBackdrop()
val innerShadowLayer =
rememberGraphicsLayer().apply {
compositingStrategy = CompositingStrategy.Offscreen
}
BoxWithConstraints(
modifier
.padding(horizontal = 16f.dp)
.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
val density = LocalDensity.current
val trackWidth = constraints.maxWidth
Box(Modifier.backdrop(trackBackdrop)) {
Box(
Modifier
.clip(ContinuousCapsule)
.background(trackColor)
.height(6f.dp)
.fillMaxWidth()
)
Box(
Modifier
.clip(ContinuousCapsule)
.background(accentColor)
.height(6f.dp)
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val fraction = fraction
val width = (fraction * constraints.maxWidth).fastRoundToInt()
layout(width, placeable.height) {
placeable.place(0, 0)
}
}
)
}
Box(
Modifier
.graphicsLayer {
val fraction = fraction
translationX =
(-size.width / 2f + fraction * trackWidth)
.fastCoerceIn(-size.width / 4f, trackWidth - size.width * 3f / 4f)
}
.draggable(
rememberDraggableState { delta ->
val trackWidth = trackWidth - with(density) { 40f.dp.toPx() }
val targetFraction = fraction + delta / trackWidth
val targetValue = lerp(valueRange.start, valueRange.endInclusive, targetFraction)
onValueChange(targetValue.fastCoerceIn(valueRange.start, valueRange.endInclusive))
},
Orientation.Horizontal,
startDragImmediately = true,
onDragStarted = {
animationScope.launch {
progressAnimation.animateTo(1f, progressAnimationSpec)
}
},
onDragStopped = {
animationScope.launch {
progressAnimation.animateTo(0f, progressAnimationSpec)
}
}
)
.drawBackdrop(
rememberCombinedBackdropDrawer(backdrop, trackBackdrop),
{ ContinuousCapsule },
highlight = {
val progress = progressAnimation.value
Highlight(
color = Color.Black.copy(0.3f * progress),
blendMode = BlendMode.SrcOver
)
},
shadow = { Shadow(elevation = 4f.dp, color = Color.Black.copy(0.08f)) },
layer = {
val progress = progressAnimation.value
val scale = lerp(1f, 1.5f, progress)
scaleX = scale
scaleY = scale
},
onDrawSurface = {
val shape = ContinuousCapsule
val outline = shape.createOutline(size, layoutDirection, this)
val innerShadowOffset = 4f.dp.toPx()
val innerShadowBlurRadius = 4f.dp.toPx()
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)
val progress = progressAnimation.value.fastCoerceIn(0f, 1f)
drawRect(Color.White.copy(1f - progress))
}
) {
refraction(6f.dp.toPx(), size.height / 2f)
dispersion(12f.dp.toPx(), size.height / 2f)
}
.size(40f.dp, 24f.dp)
)
}
}
@@ -56,6 +56,7 @@ import com.kyant.backdrop.effects.refraction
import com.kyant.backdrop.effects.saturation
import com.kyant.backdrop.highlight.Highlight
import com.kyant.backdrop.rememberBackdrop
import com.kyant.backdrop.rememberCombinedBackdropDrawer
import com.kyant.backdrop.shadow.Shadow
import com.kyant.capsule.ContinuousCapsule
import kotlinx.coroutines.launch
@@ -86,7 +87,9 @@ fun BottomTabsContent() {
val scaleYAnimation = remember { Animatable(0f) }
val tabOffsetAnimation = remember { Animatable(0f) }
BackdropDemoScaffold { backdrop ->
BackdropDemoScaffold(
initialPainterResId = R.drawable.system_home_screen_light
) { backdrop ->
Box(
Modifier.padding(40f.dp),
contentAlignment = Alignment.CenterStart
@@ -236,7 +239,7 @@ fun BottomTabsContent() {
}
}
.drawBackdrop(
tabsBackdrop,
rememberCombinedBackdropDrawer(backdrop, tabsBackdrop),
{ ContinuousCapsule },
highlight = {
val progress = pressAnimation.value
@@ -43,11 +43,12 @@ fun HomeContent(onNavigate: (CatalogDestination) -> Unit) {
Column {
Subtitle("Liquid glass components")
ListItem({ onNavigate(CatalogDestination.Buttons) }, "Buttons")
ListItem({ onNavigate(CatalogDestination.Slider) }, "Slider")
ListItem({ onNavigate(CatalogDestination.BottomTabs) }, "Bottom tabs")
ListItem({ onNavigate(CatalogDestination.Dialog) }, "Dialog")
Subtitle("System UIs")
ListItem({ onNavigate(CatalogDestination.ControlCenter) }, "Control center")
ListItem({ onNavigate(CatalogDestination.Dialog) }, "Dialog")
Subtitle("Experiments")
ListItem({ onNavigate(CatalogDestination.GlassPlayground) }, "Glass playground")
@@ -0,0 +1,34 @@
package com.kyant.backdrop.catalog.destinations
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.kyant.backdrop.catalog.BackdropDemoScaffold
import com.kyant.backdrop.catalog.components.LiquidSlider
@Composable
fun SliderContent() {
BackdropDemoScaffold { backdrop ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16f.dp)
) {
var value by remember { mutableFloatStateOf(50f) }
LiquidSlider(
value = { value },
onValueChange = { value = it },
valueRange = 0f..100f,
backdrop = backdrop,
Modifier.padding(horizontal = 16f.dp)
)
}
}
}