mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-08 11:34:18 +08:00
feat: support steps in Slider
This commit is contained in:
@@ -33,7 +33,9 @@ import androidx.compose.foundation.gestures.rememberDraggableState
|
|||||||
import androidx.compose.foundation.hoverable
|
import androidx.compose.foundation.hoverable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -65,13 +67,15 @@ import com.highcapable.flexiui.LocalShapes
|
|||||||
import com.highcapable.flexiui.LocalSizes
|
import com.highcapable.flexiui.LocalSizes
|
||||||
import com.highcapable.flexiui.utils.borderOrNot
|
import com.highcapable.flexiui.utils.borderOrNot
|
||||||
import com.highcapable.flexiui.utils.status
|
import com.highcapable.flexiui.utils.status
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class SliderColors(
|
data class SliderColors(
|
||||||
val trackInactiveColor: Color,
|
val trackInactiveColor: Color,
|
||||||
val trackActiveColor: Color,
|
val trackActiveColor: Color,
|
||||||
val thumbColor: Color
|
val thumbColor: Color,
|
||||||
|
val stepColor: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -80,8 +84,10 @@ data class SliderStyle(
|
|||||||
val thumbGain: Float,
|
val thumbGain: Float,
|
||||||
val thumbShadowSize: Dp,
|
val thumbShadowSize: Dp,
|
||||||
val thumbShape: Shape,
|
val thumbShape: Shape,
|
||||||
|
val stepShape: Shape,
|
||||||
val trackShape: Shape,
|
val trackShape: Shape,
|
||||||
val thumbBorder: BorderStroke,
|
val thumbBorder: BorderStroke,
|
||||||
|
val stepBorder: BorderStroke,
|
||||||
val trackBorder: BorderStroke,
|
val trackBorder: BorderStroke,
|
||||||
val trackWidth: Dp,
|
val trackWidth: Dp,
|
||||||
val trackHeight: Dp
|
val trackHeight: Dp
|
||||||
@@ -97,22 +103,37 @@ fun Slider(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
min: Float = 0f,
|
min: Float = 0f,
|
||||||
max: Float = 100f,
|
max: Float = 100f,
|
||||||
/*@IntRange(from = 0)*/
|
steps: Int = 0,
|
||||||
steps: Int = 0, // TODO: Implement steps
|
|
||||||
onValueChangeFinished: (() -> Unit)? = null,
|
onValueChangeFinished: (() -> Unit)? = null,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||||
) {
|
) {
|
||||||
val thumbDiameter = style.thumbRadius * 2
|
val thumbDiameter = style.thumbRadius * 2
|
||||||
val trackAdoptWidth = style.trackWidth - thumbDiameter
|
val trackAdoptWidth = style.trackWidth - thumbDiameter
|
||||||
val hovered by interactionSource.collectIsHoveredAsState()
|
val hovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
var tapped by remember { mutableStateOf(false) }
|
||||||
var dragging by remember { mutableStateOf(false) }
|
var dragging by remember { mutableStateOf(false) }
|
||||||
val animatedScale by animateFloatAsState(if (hovered || dragging) style.thumbGain else 1f)
|
val animatedScale by animateFloatAsState(if (hovered || dragging) style.thumbGain else 1f)
|
||||||
val maxOffset = with(LocalDensity.current) { (style.trackWidth - thumbDiameter).toPx() }
|
val maxOffsetX = with(LocalDensity.current) { (style.trackWidth - thumbDiameter).toPx() }
|
||||||
val offsetXFromValue = (value.coerceIn(min, max) - min) / (max - min) * maxOffset
|
var steppedOffsetXs by remember { mutableStateOf(listOf<Float>()) }
|
||||||
|
if (steps > 0) {
|
||||||
|
val pOffsetX = maxOffsetX / (steps + 1)
|
||||||
|
steppedOffsetXs = List(steps + 2) { index -> index * pOffsetX }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Float.withSteps() =
|
||||||
|
if (steps > 0)
|
||||||
|
steppedOffsetXs.minByOrNull { abs(it - this) } ?: this
|
||||||
|
else this
|
||||||
|
|
||||||
|
val offsetXFromValue = (value.coerceIn(min, max) - min) / (max - min) * maxOffsetX
|
||||||
|
val steppedOffsetXFromValue = offsetXFromValue.withSteps()
|
||||||
var absOffsetX by remember { mutableStateOf(0f) }
|
var absOffsetX by remember { mutableStateOf(0f) }
|
||||||
var offsetX by remember { mutableStateOf(offsetXFromValue) }
|
var offsetX by remember { mutableStateOf(steppedOffsetXFromValue) }
|
||||||
fun updateValue() {
|
val animatedOffsetX by animateFloatAsState(offsetX)
|
||||||
val newValue = (offsetX / maxOffset) * (max - min) + min
|
val adoptedOffsetX = if (tapped && !dragging) animatedOffsetX else offsetX
|
||||||
|
|
||||||
|
fun updateValue(offsetX: Float) {
|
||||||
|
val newValue = (offsetX / maxOffsetX) * (max - min) + min
|
||||||
onValueChange(newValue)
|
onValueChange(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +148,7 @@ fun Slider(
|
|||||||
.drawWithContent {
|
.drawWithContent {
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
color = colors.trackActiveColor,
|
color = colors.trackActiveColor,
|
||||||
size = Size(offsetX, size.height),
|
size = Size(adoptedOffsetX, size.height),
|
||||||
cornerRadius = CornerRadius(cornerSize, cornerSize)
|
cornerRadius = CornerRadius(cornerSize, cornerSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -136,11 +157,27 @@ fun Slider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Step() {
|
||||||
|
if (steps > 0) Row(
|
||||||
|
modifier = Modifier.size(trackAdoptWidth, style.trackHeight),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
for (i in 0 until steps + 2)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(style.trackHeight)
|
||||||
|
.background(colors.stepColor, style.stepShape)
|
||||||
|
.borderOrNot(style.stepBorder, style.stepShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Thumb() {
|
fun Thumb() {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(thumbDiameter)
|
modifier = Modifier.size(thumbDiameter)
|
||||||
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
.offset { IntOffset(adoptedOffsetX.roundToInt(), 0) }
|
||||||
.scale(animatedScale)
|
.scale(animatedScale)
|
||||||
.shadow(style.thumbShadowSize, style.thumbShape)
|
.shadow(style.thumbShadowSize, style.thumbShape)
|
||||||
.background(colors.thumbColor, style.thumbShape)
|
.background(colors.thumbColor, style.thumbShape)
|
||||||
@@ -151,20 +188,23 @@ fun Slider(
|
|||||||
val absDelta = delta * animatedScale
|
val absDelta = delta * animatedScale
|
||||||
absOffsetX += absDelta
|
absOffsetX += absDelta
|
||||||
when {
|
when {
|
||||||
absOffsetX in 0f..maxOffset -> offsetX += absDelta
|
absOffsetX in 0f..maxOffsetX -> offsetX += absDelta
|
||||||
absOffsetX < 0f -> offsetX = 0f
|
absOffsetX < 0f -> offsetX = 0f
|
||||||
absOffsetX > maxOffset -> offsetX = maxOffset
|
absOffsetX > maxOffsetX -> offsetX = maxOffsetX
|
||||||
}
|
}
|
||||||
updateValue()
|
updateValue(offsetX.withSteps())
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onDragStarted = {
|
onDragStarted = {
|
||||||
|
tapped = false
|
||||||
dragging = true
|
dragging = true
|
||||||
absOffsetX = offsetX
|
absOffsetX = offsetX
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
dragging = false
|
dragging = false
|
||||||
|
val steppedOffsetX = offsetX.withSteps()
|
||||||
|
if (offsetX != steppedOffsetX) offsetX = steppedOffsetX
|
||||||
onValueChangeFinished?.invoke()
|
onValueChangeFinished?.invoke()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -176,16 +216,17 @@ fun Slider(
|
|||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
if (enabled) detectTapGestures(
|
if (enabled) detectTapGestures(
|
||||||
onTap = { offset ->
|
onTap = { offset ->
|
||||||
|
tapped = true
|
||||||
val tapedOffsetX = offset.x - style.thumbRadius.toPx()
|
val tapedOffsetX = offset.x - style.thumbRadius.toPx()
|
||||||
offsetX = tapedOffsetX.coerceIn(0f, maxOffset)
|
offsetX = tapedOffsetX.withSteps().coerceIn(0f, maxOffsetX)
|
||||||
updateValue()
|
updateValue(offsetX)
|
||||||
onValueChangeFinished?.invoke()
|
onValueChangeFinished?.invoke()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
Track {}
|
Track { Step() }
|
||||||
Thumb()
|
Thumb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,7 +247,8 @@ object Slider {
|
|||||||
private fun defaultSliderColors() = SliderColors(
|
private fun defaultSliderColors() = SliderColors(
|
||||||
trackInactiveColor = LocalColors.current.themeTertiary,
|
trackInactiveColor = LocalColors.current.themeTertiary,
|
||||||
trackActiveColor = LocalColors.current.themePrimary,
|
trackActiveColor = LocalColors.current.themePrimary,
|
||||||
thumbColor = LocalColors.current.themePrimary
|
thumbColor = LocalColors.current.themePrimary,
|
||||||
|
stepColor = LocalColors.current.themeSecondary
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -216,8 +258,10 @@ private fun defaultSliderStyle() = SliderStyle(
|
|||||||
thumbGain = DefaultThumbGain,
|
thumbGain = DefaultThumbGain,
|
||||||
thumbShadowSize = DefaultThumbShadowSize,
|
thumbShadowSize = DefaultThumbShadowSize,
|
||||||
thumbShape = CircleShape,
|
thumbShape = CircleShape,
|
||||||
|
stepShape = CircleShape,
|
||||||
trackShape = LocalShapes.current.primary,
|
trackShape = LocalShapes.current.primary,
|
||||||
thumbBorder = defaultSliderBorder(),
|
thumbBorder = defaultSliderBorder(),
|
||||||
|
stepBorder = defaultSliderBorder(),
|
||||||
trackBorder = defaultSliderBorder(),
|
trackBorder = defaultSliderBorder(),
|
||||||
trackWidth = DefaultTrackWidth,
|
trackWidth = DefaultTrackWidth,
|
||||||
trackHeight = DefaultTrackHeight
|
trackHeight = DefaultTrackHeight
|
||||||
|
Reference in New Issue
Block a user