Add slider demo
This commit is contained in:
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.
Binary file not shown.
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user