diff --git a/docs/components/slider.md b/docs/components/slider.md
index 95068e73..84f97ce2 100644
--- a/docs/components/slider.md
+++ b/docs/components/slider.md
@@ -2,7 +2,9 @@
`Slider` is a basic interactive component in Miuix used for selecting values within a continuous range. Users can adjust values by dragging the slider, making it suitable for scenarios such as volume adjustment, brightness control, and progress display.
-
+Miuix also provides `VerticalSlider` for vertical orientation and `RangeSlider` for selecting a range of values.
+
+
@@ -10,16 +12,43 @@
```kotlin
import top.yukonga.miuix.kmp.basic.Slider
+import top.yukonga.miuix.kmp.basic.VerticalSlider
+import top.yukonga.miuix.kmp.basic.RangeSlider
```
## Basic Usage
+### Slider
+
```kotlin
-var sliderValue by remember { mutableStateOf(0.5f) }
+var sliderValue by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = sliderValue,
- onProgressChange = { sliderValue = it }
+ value = sliderValue,
+ onValueChange = { sliderValue = it }
+)
+```
+
+### VerticalSlider
+
+```kotlin
+var sliderValue by remember { mutableFloatStateOf(0.5f) }
+
+VerticalSlider(
+ value = sliderValue,
+ onValueChange = { sliderValue = it },
+ modifier = Modifier.height(200.dp)
+)
+```
+
+### RangeSlider
+
+```kotlin
+var rangeValue by remember { mutableStateOf(0.2f..0.8f) }
+
+RangeSlider(
+ value = rangeValue,
+ onValueChange = { rangeValue = it }
)
```
@@ -28,11 +57,11 @@ Slider(
### Disabled State
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
enabled = false
)
```
@@ -42,11 +71,11 @@ Slider(
Slider supports haptic feedback, which can be customized through the `hapticEffect` parameter. See [SliderHapticEffect](../components/slider#sliderhapticeffect) for details.
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
hapticEffect = SliderHapticEffect.Step
)
```
@@ -55,19 +84,62 @@ Slider(
### Slider Properties
-| Property Name | Type | Description | Default Value | Required |
-| ---------------- | --------------------------------- | -------------------------------------- | ---------------------------------- | -------- |
-| progress | Float | Current slider progress value | - | Yes |
-| onProgressChange | (Float) -> Unit | Callback when progress value changes | - | Yes |
-| modifier | Modifier | Modifier applied to the slider | Modifier | No |
-| enabled | Boolean | Whether the slider is enabled | true | No |
-| minValue | Float | Minimum value of the slider | 0f | No |
-| maxValue | Float | Maximum value of the slider | 1f | No |
-| height | Dp | Height of the slider | SliderDefaults.MinHeight | No |
-| colors | SliderColors | Color configuration of the slider | SliderDefaults.sliderColors() | No |
-| effect | Boolean | Whether to show special effects | false | No |
-| decimalPlaces | Int | Decimal places shown in drag indicator | 2 | No |
-| hapticEffect | SliderDefaults.SliderHapticEffect | Type of haptic feedback | SliderDefaults.DefaultHapticEffect | No |
+| Property Name | Type | Description | Default Value | Required |
+| --------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | -------- |
+| value | Float | Current value of the slider. If outside of valueRange provided, value will be coerced to this range | - | Yes |
+| onValueChange | (Float) -> Unit | Callback when value changes | - | Yes |
+| modifier | Modifier | Modifier applied to the slider | Modifier | No |
+| enabled | Boolean | Whether the slider is enabled | true | No |
+| valueRange | ClosedFloatingPointRange\
| Range of values that this slider can take. The passed value will be coerced to this range | 0f..1f | No |
+| steps | Int | Amount of discrete allowable values. If 0, the slider will behave continuously. Must not be negative | 0 | No |
+| onValueChangeFinished | (() -> Unit)? | Called when value change has ended | null | No |
+| reverseDirection | Boolean | Controls the direction of slider. When false, increases left to right. When true, increases right to left (RTL) | false | No |
+| height | Dp | Height of the slider | SliderDefaults.MinHeight | No |
+| colors | SliderColors | Color configuration of the slider | SliderDefaults.sliderColors() | No |
+| effect | Boolean | Whether to show special effects | false | No |
+| hapticEffect | SliderDefaults.SliderHapticEffect | Type of haptic feedback | SliderDefaults.DefaultHapticEffect | No |
+| showKeyPoints | Boolean | Whether to show key point indicators on the slider. Only works when keyPoints is not null | false | No |
+| keyPoints | List\? | Custom key point values to display on the slider. If null, uses step positions from steps parameter. Values should be within valueRange | null | No |
+| magnetThreshold | Float | Magnetic snap threshold as a fraction (0.0 to 1.0). When slider value is within this distance from a key point, it will snap to that point. Only applies when keyPoints is set | 0.02f | No |
+
+### VerticalSlider Properties
+
+| Property Name | Type | Description | Default Value | Required |
+| --------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | -------- |
+| value | Float | Current value of the slider | - | Yes |
+| onValueChange | (Float) -> Unit | Callback when value changes | - | Yes |
+| modifier | Modifier | Modifier applied to the slider | Modifier | No |
+| enabled | Boolean | Whether the slider is enabled | true | No |
+| valueRange | ClosedFloatingPointRange\ | Range of values that this slider can take | 0f..1f | No |
+| steps | Int | Amount of discrete allowable values | 0 | No |
+| onValueChangeFinished | (() -> Unit)? | Called when value change has ended | null | No |
+| reverseDirection | Boolean | Controls the direction of slider. When false, increases bottom to top. When true, increases top to bottom | false | No |
+| width | Dp | Width of the vertical slider | SliderDefaults.MinHeight | No |
+| colors | SliderColors | Color configuration of the slider | SliderDefaults.sliderColors() | No |
+| effect | Boolean | Whether to show special effects | false | No |
+| hapticEffect | SliderDefaults.SliderHapticEffect | Type of haptic feedback | SliderDefaults.DefaultHapticEffect | No |
+| showKeyPoints | Boolean | Whether to show key point indicators on the slider. Only works when keyPoints is not null | false | No |
+| keyPoints | List\? | Custom key point values to display on the slider. If null, uses step positions from steps parameter. Values should be within valueRange | null | No |
+| magnetThreshold | Float | Magnetic snap threshold as a fraction (0.0 to 1.0). When slider value is within this distance from a key point, it will snap to that point. Only applies when keyPoints is set | 0.02f | No |
+
+### RangeSlider Properties
+
+| Property Name | Type | Description | Default Value | Required |
+| --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | -------- |
+| value | ClosedFloatingPointRange\ | Current values of the RangeSlider. If either value is outside of valueRange, it will be coerced | - | Yes |
+| onValueChange | (ClosedFloatingPointRange\) -> Unit | Lambda in which values should be updated | - | Yes |
+| modifier | Modifier | Modifier applied to the slider | Modifier | No |
+| enabled | Boolean | Whether the slider is enabled | true | No |
+| valueRange | ClosedFloatingPointRange\ | Range of values that Range Slider values can take | 0f..1f | No |
+| steps | Int | Amount of discrete allowable values | 0 | No |
+| onValueChangeFinished | (() -> Unit)? | Called when value change has ended | null | No |
+| height | Dp | Height of the slider | SliderDefaults.MinHeight | No |
+| colors | SliderColors | Color configuration of the slider | SliderDefaults.sliderColors() | No |
+| effect | Boolean | Whether to show special effects | false | No |
+| hapticEffect | SliderDefaults.SliderHapticEffect | Type of haptic feedback | SliderDefaults.DefaultHapticEffect | No |
+| showKeyPoints | Boolean | Whether to show key point indicators on the slider. Only works when keyPoints is not null | false | No |
+| keyPoints | List\? | Custom key point values to display on the slider. If null, uses step positions from steps parameter. Values should be within valueRange | null | No |
+| magnetThreshold | Float | Magnetic snap threshold as a fraction (0.0 to 1.0). When slider value is within this distance from a key point, it will snap to that point. Only applies when keyPoints is set | 0.02f | No |
### SliderDefaults Object
@@ -82,11 +154,11 @@ The SliderDefaults object provides default configurations for the Slider compone
### SliderHapticEffect
-| Value | Description |
-| ----- | ------------------------------ |
-| None | No haptic feedback |
-| Edge | Haptic feedback at 0% and 100% |
-| Step | Haptic feedback at each step |
+| Value | Description |
+| ----- | ---------------------------- |
+| None | No haptic feedback |
+| Edge | Haptic feedback at edges |
+| Step | Haptic feedback at each step |
#### Methods
@@ -96,26 +168,42 @@ The SliderDefaults object provides default configurations for the Slider compone
### SliderColors Class
-| Property Name | Type | Description |
-| ----------------------- | ----- | ------------------------------ |
-| foregroundColor | Color | Foreground color of the slider |
-| disabledForegroundColor | Color | Foreground color when disabled |
-| backgroundColor | Color | Background color of the slider |
+| Property Name | Type | Description |
+| ----------------------- | ----- | --------------------------------- |
+| foregroundColor | Color | Foreground color of the slider |
+| disabledForegroundColor | Color | Foreground color when disabled |
+| backgroundColor | Color | Background color of the slider |
+| keyPointColor | Color | Color of the key point indicators |
## Advanced Usage
### Custom Value Range
```kotlin
-var temperature by remember { mutableStateOf(25f) }
+var temperature by remember { mutableFloatStateOf(25f) }
+
+Column {
+ Text("Temperature: ${temperature.roundToInt()}°C")
+ Slider(
+ value = temperature,
+ onValueChange = { temperature = it },
+ valueRange = 16f..32f
+ )
+}
+```
+
+### Discrete Steps
+
+```kotlin
+var rating by remember { mutableFloatStateOf(3f) }
Column {
- Text("Temperature: $temperature°C")
+ Text("Rating: ${rating.roundToInt()}/5")
Slider(
- progress = temperature,
- onProgressChange = { temperature = it },
- minValue = 16f,
- maxValue = 32f
+ value = rating,
+ onValueChange = { rating = it },
+ valueRange = 1f..5f,
+ steps = 3 // Creates 5 discrete values: 1, 2, 3, 4, 5
)
}
```
@@ -123,11 +211,11 @@ Column {
### Custom Colors
```kotlin
-var volume by remember { mutableStateOf(0.7f) }
+var volume by remember { mutableFloatStateOf(0.7f) }
Slider(
- progress = volume,
- onProgressChange = { volume = it },
+ value = volume,
+ onValueChange = { volume = it },
colors = SliderDefaults.sliderColors(
foregroundColor = Color.Red,
backgroundColor = Color.LightGray
@@ -138,11 +226,11 @@ Slider(
### Custom Height and Effects
```kotlin
-var brightness by remember { mutableStateOf(0.8f) }
+var brightness by remember { mutableFloatStateOf(0.8f) }
Slider(
- progress = brightness,
- onProgressChange = { brightness = it },
+ value = brightness,
+ onValueChange = { brightness = it },
height = 40.dp,
effect = true
)
@@ -151,11 +239,96 @@ Slider(
### Slider with Haptic Feedback
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
hapticEffect = SliderDefaults.SliderHapticEffect.Step
)
```
+
+### VerticalSlider with Reverse Direction
+
+```kotlin
+var volume by remember { mutableFloatStateOf(0.5f) }
+
+VerticalSlider(
+ value = volume,
+ onValueChange = { volume = it },
+ modifier = Modifier.height(200.dp),
+ reverseDirection = true // Top to bottom
+)
+```
+
+### RangeSlider for Price Filter
+
+```kotlin
+var priceRange by remember { mutableStateOf(100f..500f) }
+
+Column {
+ Text("Price: $${priceRange.start.roundToInt()} - $${priceRange.endInclusive.roundToInt()}")
+ RangeSlider(
+ value = priceRange,
+ onValueChange = { priceRange = it },
+ valueRange = 0f..1000f,
+ steps = 99 // 101 discrete values from 0 to 1000
+ )
+}
+```
+
+### Complete Example with Value Change Callback
+
+```kotlin
+var value by remember { mutableFloatStateOf(0.5f) }
+var finalValue by remember { mutableFloatStateOf(0.5f) }
+
+Column {
+ Text("Current: $value")
+ Text("Final: $finalValue")
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ onValueChangeFinished = { finalValue = value },
+ valueRange = 0f..100f,
+ steps = 99
+ )
+}
+```
+
+### Slider with Custom Key Points
+
+```kotlin
+var value by remember { mutableFloatStateOf(50f) }
+
+Column {
+ Text("Value: ${value.roundToInt()}")
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ keyPoints = listOf(0f, 25f, 50f, 75f, 100f),
+ magnetThreshold = 0.05f, // 5% snap threshold
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step
+ )
+}
+```
+
+### RangeSlider with Key Points
+
+```kotlin
+var range by remember { mutableStateOf(20f..80f) }
+
+Column {
+ Text("Range: ${range.start.roundToInt()} - ${range.endInclusive.roundToInt()}")
+ RangeSlider(
+ value = range,
+ onValueChange = { range = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ keyPoints = listOf(0f, 20f, 40f, 60f, 80f, 100f),
+ magnetThreshold = 0.03f
+ )
+}
+```
diff --git a/docs/demo/src/commonMain/kotlin/SliderDemo.kt b/docs/demo/src/commonMain/kotlin/SliderDemo.kt
index c5835ce3..60010fa4 100644
--- a/docs/demo/src/commonMain/kotlin/SliderDemo.kt
+++ b/docs/demo/src/commonMain/kotlin/SliderDemo.kt
@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import top.yukonga.miuix.kmp.basic.RangeSlider
import top.yukonga.miuix.kmp.basic.Slider
@Composable
@@ -39,15 +40,20 @@ fun SliderDemo() {
) {
var slider1 by remember { mutableStateOf(0.5f) }
var slider2 by remember { mutableStateOf(0.7f) }
+ var slider3 by remember { mutableStateOf(0.2f..0.8f) }
Slider(
- progress = slider1,
- onProgressChange = { slider1 = it }
+ value = slider1,
+ onValueChange = { slider1 = it }
)
Slider(
- progress = slider2,
- onProgressChange = { slider2 = it },
+ value = slider2,
+ onValueChange = { slider2 = it },
enabled = false
)
+ RangeSlider(
+ value = slider3,
+ onValueChange = { slider3 = it }
+ )
}
}
}
diff --git a/docs/zh_CN/components/slider.md b/docs/zh_CN/components/slider.md
index 57193346..8026cfae 100644
--- a/docs/zh_CN/components/slider.md
+++ b/docs/zh_CN/components/slider.md
@@ -2,7 +2,9 @@
`Slider` 是 Miuix 中的基础交互组件,用于在连续的数值范围内进行选择。用户可以通过拖动滑块来调整值,适用于诸如音量调节、亮度控制、进度显示等场景。
-
+Miuix 还提供了 `VerticalSlider` 用于垂直方向的滑块,以及 `RangeSlider` 用于选择数值范围。
+
+
@@ -10,16 +12,43 @@
```kotlin
import top.yukonga.miuix.kmp.basic.Slider
+import top.yukonga.miuix.kmp.basic.VerticalSlider
+import top.yukonga.miuix.kmp.basic.RangeSlider
```
## 基本用法
+### Slider
+
```kotlin
-var sliderValue by remember { mutableStateOf(0.5f) }
+var sliderValue by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = sliderValue,
- onProgressChange = { sliderValue = it }
+ value = sliderValue,
+ onValueChange = { sliderValue = it }
+)
+```
+
+### VerticalSlider
+
+```kotlin
+var sliderValue by remember { mutableFloatStateOf(0.5f) }
+
+VerticalSlider(
+ value = sliderValue,
+ onValueChange = { sliderValue = it },
+ modifier = Modifier.height(200.dp)
+)
+```
+
+### RangeSlider
+
+```kotlin
+var rangeValue by remember { mutableStateOf(0.2f..0.8f) }
+
+RangeSlider(
+ value = rangeValue,
+ onValueChange = { rangeValue = it }
)
```
@@ -28,11 +57,11 @@ Slider(
### 禁用状态
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
enabled = false
)
```
@@ -42,11 +71,11 @@ Slider(
Slider 支持触觉反馈,可以通过 `hapticEffect` 参数自定义反馈效果,详见 [SliderHapticEffect](../components/slider#sliderhapticeffect)。
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
hapticEffect = SliderHapticEffect.Step
)
```
@@ -55,19 +84,62 @@ Slider(
### Slider 属性
-| 属性名 | 类型 | 说明 | 默认值 | 是否必须 |
-| ---------------- | --------------------------------- | ------------------------ | ---------------------------------- | -------- |
-| progress | Float | 当前滑块的进度值 | - | 是 |
-| onProgressChange | (Float) -> Unit | 进度值变化时的回调函数 | - | 是 |
-| modifier | Modifier | 应用于滑块的修饰符 | Modifier | 否 |
-| enabled | Boolean | 是否启用滑块 | true | 否 |
-| minValue | Float | 滑块的最小值 | 0f | 否 |
-| maxValue | Float | 滑块的最大值 | 1f | 否 |
-| height | Dp | 滑块的高度 | SliderDefaults.MinHeight | 否 |
-| colors | SliderColors | 滑块的颜色配置 | SliderDefaults.sliderColors() | 否 |
-| effect | Boolean | 是否显示特殊效果 | false | 否 |
-| decimalPlaces | Int | 拖动指示器中显示的小数位 | 2 | 否 |
-| hapticEffect | SliderDefaults.SliderHapticEffect | 滑块的触感反馈类型 | SliderDefaults.DefaultHapticEffect | 否 |
+| 属性名 | 类型 | 说明 | 默认值 | 是否必须 |
+| --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------- |
+| value | Float | 当前滑块的值。如果超出 valueRange,值将被强制限制在该范围内 | - | 是 |
+| onValueChange | (Float) -> Unit | 值变化时的回调函数 | - | 是 |
+| modifier | Modifier | 应用于滑块的修饰符 | Modifier | 否 |
+| enabled | Boolean | 是否启用滑块 | true | 否 |
+| valueRange | ClosedFloatingPointRange\
| 滑块可以采用的值范围。传递的值将被强制限制在此范围内 | 0f..1f | 否 |
+| steps | Int | 离散值的数量。如果为 0,滑块将连续运行。必须非负 | 0 | 否 |
+| onValueChangeFinished | (() -> Unit)? | 值变化结束时调用 | null | 否 |
+| reverseDirection | Boolean | 控制滑块的方向。false 时从左到右增加,true 时从右到左增加(适用于 RTL 布局) | false | 否 |
+| height | Dp | 滑块的高度 | SliderDefaults.MinHeight | 否 |
+| colors | SliderColors | 滑块的颜色配置 | SliderDefaults.sliderColors() | 否 |
+| effect | Boolean | 是否显示特殊效果 | false | 否 |
+| hapticEffect | SliderDefaults.SliderHapticEffect | 滑块的触感反馈类型 | SliderDefaults.DefaultHapticEffect | 否 |
+| showKeyPoints | Boolean | 是否显示关键点指示器。仅当 keyPoints 不为 null 时有效 | false | 否 |
+| keyPoints | List\? | 要在滑块上显示的自定义关键点值。如果为 null,则使用 steps 参数的步长位置。值应在 valueRange 范围内 | null | 否 |
+| magnetThreshold | Float | 磁吸对齐阈值,以分数表示 (0.0 到 1.0)。当滑块值与关键点的距离在此阈值内时,将对齐到该点。仅在设置 keyPoints 时生效 | 0.02f | 否 |
+
+### VerticalSlider 属性
+
+| 属性名 | 类型 | 说明 | 默认值 | 是否必须 |
+| --------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------- |
+| value | Float | 当前滑块的值 | - | 是 |
+| onValueChange | (Float) -> Unit | 值变化时的回调函数 | - | 是 |
+| modifier | Modifier | 应用于滑块的修饰符 | Modifier | 否 |
+| enabled | Boolean | 是否启用滑块 | true | 否 |
+| valueRange | ClosedFloatingPointRange\ | 滑块可以采用的值范围 | 0f..1f | 否 |
+| steps | Int | 离散值的数量 | 0 | 否 |
+| onValueChangeFinished | (() -> Unit)? | 值变化结束时调用 | null | 否 |
+| reverseDirection | Boolean | 控制滑块的方向。false 时从下到上增加,true 时从上到下增加 | false | 否 |
+| width | Dp | 垂直滑块的宽度 | SliderDefaults.MinHeight | 否 |
+| colors | SliderColors | 滑块的颜色配置 | SliderDefaults.sliderColors() | 否 |
+| effect | Boolean | 是否显示特殊效果 | false | 否 |
+| hapticEffect | SliderDefaults.SliderHapticEffect | 滑块的触感反馈类型 | SliderDefaults.DefaultHapticEffect | 否 |
+| showKeyPoints | Boolean | 是否显示关键点指示器。仅当 keyPoints 不为 null 时有效 | false | 否 |
+| keyPoints | List\? | 要在滑块上显示的自定义关键点值。如果为 null,则使用 steps 参数的步长位置。值应在 valueRange 范围内 | null | 否 |
+| magnetThreshold | Float | 磁吸对齐阈值,以分数表示 (0.0 到 1.0)。当滑块值与关键点的距离在此阈值内时,将对齐到该点。仅在设置 keyPoints 时生效 | 0.02f | 否 |
+
+### RangeSlider 属性
+
+| 属性名 | 类型 | 说明 | 默认值 | 是否必须 |
+| --------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------- | -------- |
+| value | ClosedFloatingPointRange\ | RangeSlider 的当前值。如果任一值超出 valueRange,将被强制限制在范围内 | - | 是 |
+| onValueChange | (ClosedFloatingPointRange\) -> Unit | 值变化时的回调函数 | - | 是 |
+| modifier | Modifier | 应用于滑块的修饰符 | Modifier | 否 |
+| enabled | Boolean | 是否启用滑块 | true | 否 |
+| valueRange | ClosedFloatingPointRange\ | Range Slider 值可以采用的范围 | 0f..1f | 否 |
+| steps | Int | 离散值的数量 | 0 | 否 |
+| onValueChangeFinished | (() -> Unit)? | 值变化结束时调用 | null | 否 |
+| height | Dp | 滑块的高度 | SliderDefaults.MinHeight | 否 |
+| colors | SliderColors | 滑块的颜色配置 | SliderDefaults.sliderColors() | 否 |
+| effect | Boolean | 是否显示特殊效果 | false | 否 |
+| hapticEffect | SliderDefaults.SliderHapticEffect | 滑块的触感反馈类型 | SliderDefaults.DefaultHapticEffect | 否 |
+| showKeyPoints | Boolean | 是否显示关键点指示器。仅当 keyPoints 不为 null 时有效 | false | 否 |
+| keyPoints | List\? | 要在滑块上显示的自定义关键点值。如果为 null,则使用 steps 参数的步长位置。值应在 valueRange 范围内 | null | 否 |
+| magnetThreshold | Float | 磁吸对齐阈值,以分数表示 (0.0 到 1.0)。当滑块值与关键点的距离在此阈值内时,将对齐到该点。仅在设置 keyPoints 时生效 | 0.02f | 否 |
### SliderDefaults 对象
@@ -82,11 +154,11 @@ SliderDefaults 对象提供了 Slider 组件的默认配置。
### SliderHapticEffect
-| 值 | 说明 |
-| ---- | -------------------------- |
-| None | 无触感反馈 |
-| Edge | 在 0% 和 100% 处有触感反馈 |
-| Step | 在每个步长处有触感反馈 |
+| 值 | 说明 |
+| ---- | ---------------------- |
+| None | 无触感反馈 |
+| Edge | 在边缘处有触感反馈 |
+| Step | 在每个步长处有触感反馈 |
#### 方法
@@ -101,21 +173,37 @@ SliderDefaults 对象提供了 Slider 组件的默认配置。
| foregroundColor | Color | 滑块的前景颜色 |
| disabledForegroundColor | Color | 禁用状态时滑块的前景颜色 |
| backgroundColor | Color | 滑块的背景颜色 |
+| keyPointColor | Color | 关键点指示器的颜色 |
## 进阶用法
### 自定义数值范围
```kotlin
-var temperature by remember { mutableStateOf(25f) }
+var temperature by remember { mutableFloatStateOf(25f) }
+
+Column {
+ Text("温度: ${temperature.roundToInt()}°C")
+ Slider(
+ value = temperature,
+ onValueChange = { temperature = it },
+ valueRange = 16f..32f
+ )
+}
+```
+
+### 离散步长
+
+```kotlin
+var rating by remember { mutableFloatStateOf(3f) }
Column {
- Text("温度: $temperature°C")
+ Text("评分: ${rating.roundToInt()}/5")
Slider(
- progress = temperature,
- onProgressChange = { temperature = it },
- minValue = 16f,
- maxValue = 32f
+ value = rating,
+ onValueChange = { rating = it },
+ valueRange = 1f..5f,
+ steps = 3 // 创建 5 个离散值: 1, 2, 3, 4, 5
)
}
```
@@ -123,11 +211,11 @@ Column {
### 自定义颜色
```kotlin
-var volume by remember { mutableStateOf(0.7f) }
+var volume by remember { mutableFloatStateOf(0.7f) }
Slider(
- progress = volume,
- onProgressChange = { volume = it },
+ value = volume,
+ onValueChange = { volume = it },
colors = SliderDefaults.sliderColors(
foregroundColor = Color.Red,
backgroundColor = Color.LightGray
@@ -138,11 +226,11 @@ Slider(
### 自定义高度和效果
```kotlin
-var brightness by remember { mutableStateOf(0.8f) }
+var brightness by remember { mutableFloatStateOf(0.8f) }
Slider(
- progress = brightness,
- onProgressChange = { brightness = it },
+ value = brightness,
+ onValueChange = { brightness = it },
height = 40.dp,
effect = true
)
@@ -151,11 +239,96 @@ Slider(
### 带触感反馈的滑块
```kotlin
-var progress by remember { mutableStateOf(0.5f) }
+var value by remember { mutableFloatStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { progress = it },
+ value = value,
+ onValueChange = { value = it },
hapticEffect = SliderDefaults.SliderHapticEffect.Step
)
```
+
+### 反向方向的 VerticalSlider
+
+```kotlin
+var volume by remember { mutableFloatStateOf(0.5f) }
+
+VerticalSlider(
+ value = volume,
+ onValueChange = { volume = it },
+ modifier = Modifier.height(200.dp),
+ reverseDirection = true // 从上到下
+)
+```
+
+### 用于价格筛选的 RangeSlider
+
+```kotlin
+var priceRange by remember { mutableStateOf(100f..500f) }
+
+Column {
+ Text("价格: ¥${priceRange.start.roundToInt()} - ¥${priceRange.endInclusive.roundToInt()}")
+ RangeSlider(
+ value = priceRange,
+ onValueChange = { priceRange = it },
+ valueRange = 0f..1000f,
+ steps = 99 // 从 0 到 1000 的 101 个离散值
+ )
+}
+```
+
+### 完整示例(含值变化回调)
+
+```kotlin
+var value by remember { mutableFloatStateOf(0.5f) }
+var finalValue by remember { mutableFloatStateOf(0.5f) }
+
+Column {
+ Text("当前值: $value")
+ Text("最终值: $finalValue")
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ onValueChangeFinished = { finalValue = value },
+ valueRange = 0f..100f,
+ steps = 99
+ )
+}
+```
+
+### 自定义关键点的滑块
+
+```kotlin
+var value by remember { mutableFloatStateOf(50f) }
+
+Column {
+ Text("值: ${value.roundToInt()}")
+ Slider(
+ value = value,
+ onValueChange = { value = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ keyPoints = listOf(0f, 25f, 50f, 75f, 100f),
+ magnetThreshold = 0.05f, // 5% 磁吸阈值
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step
+ )
+}
+```
+
+### 带关键点的 RangeSlider
+
+```kotlin
+var range by remember { mutableStateOf(20f..80f) }
+
+Column {
+ Text("范围: ${range.start.roundToInt()} - ${range.endInclusive.roundToInt()}")
+ RangeSlider(
+ value = range,
+ onValueChange = { range = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ keyPoints = listOf(0f, 20f, 40f, 60f, 80f, 100f),
+ magnetThreshold = 0.03f
+ )
+}
+```
diff --git a/example/src/commonMain/kotlin/component/OtherComponent.kt b/example/src/commonMain/kotlin/component/OtherComponent.kt
index 29a458a5..6e55b97f 100644
--- a/example/src/commonMain/kotlin/component/OtherComponent.kt
+++ b/example/src/commonMain/kotlin/component/OtherComponent.kt
@@ -9,6 +9,7 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -35,6 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import top.yukonga.miuix.kmp.basic.ButtonDefaults
@@ -47,6 +49,7 @@ import top.yukonga.miuix.kmp.basic.ColorSpace
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
+import top.yukonga.miuix.kmp.basic.RangeSlider
import top.yukonga.miuix.kmp.basic.Slider
import top.yukonga.miuix.kmp.basic.SliderDefaults
import top.yukonga.miuix.kmp.basic.SmallTitle
@@ -55,12 +58,13 @@ import top.yukonga.miuix.kmp.basic.TabRowWithContour
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
+import top.yukonga.miuix.kmp.basic.VerticalSlider
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Like
import top.yukonga.miuix.kmp.theme.MiuixTheme
+import top.yukonga.miuix.kmp.utils.PressFeedbackType
import top.yukonga.miuix.kmp.utils.toHsv
import top.yukonga.miuix.kmp.utils.toOkLab
-import top.yukonga.miuix.kmp.utils.PressFeedbackType
import kotlin.math.round
fun LazyListScope.otherComponent(
@@ -222,33 +226,292 @@ fun LazyListScope.otherComponent(
item(key = "slider") {
SmallTitle(text = "Slider")
- var progress by remember { mutableStateOf(0.5f) }
- Slider(
- progress = progress,
- onProgressChange = { newProgress -> progress = newProgress },
- decimalPlaces = 3,
+ Card(
modifier = Modifier
.padding(horizontal = 12.dp)
- .padding(bottom = 12.dp)
- )
- var progressHaptic by remember { mutableStateOf(0.5f) }
- Slider(
- progress = progressHaptic,
- onProgressChange = { newProgress -> progressHaptic = newProgress },
- hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ .padding(bottom = 6.dp)
+ ) {
+ var sliderValue by remember { mutableStateOf(0.3f) }
+ Text(
+ text = "Normal: ${(sliderValue * 100).toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ Slider(
+ value = sliderValue,
+ onValueChange = { sliderValue = it },
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var stepsValue by remember { mutableStateOf(5f) }
+ Text(
+ text = "Steps: ${stepsValue.toInt()}/8",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ Slider(
+ value = stepsValue,
+ onValueChange = { stepsValue = it },
+ valueRange = 0f..8f,
+ steps = 7,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var stepsWithKeyPointsValue by remember { mutableStateOf(5f) }
+ Text(
+ text = "Steps with Key Points: ${stepsWithKeyPointsValue.toInt()}/8",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ Slider(
+ value = stepsWithKeyPointsValue,
+ onValueChange = { stepsWithKeyPointsValue = it },
+ valueRange = 0f..8f,
+ steps = 7,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ showKeyPoints = true,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var customKeyPointsValue by remember { mutableStateOf(25f) }
+ Text(
+ text = "Custom Key Points: ${customKeyPointsValue.toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ Slider(
+ value = customKeyPointsValue,
+ onValueChange = { customKeyPointsValue = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ keyPoints = listOf(0f, 25f, 50f, 75f, 100f),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ val disabledValue by remember { mutableStateOf(0.7f) }
+ Text(
+ text = "Disabled: ${(disabledValue * 100).toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ Slider(
+ value = disabledValue,
+ onValueChange = {},
+ enabled = false,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ }
+
+ // RangeSlider
+ SmallTitle(text = "RangeSlider")
+ Card(
modifier = Modifier
.padding(horizontal = 12.dp)
- .padding(bottom = 12.dp)
- )
- val progressDisable by remember { mutableStateOf(0.5f) }
- Slider(
- progress = progressDisable,
- onProgressChange = {},
- enabled = false,
+ .padding(bottom = 6.dp)
+ ) {
+ var rangeValue by remember { mutableStateOf(0.2f..0.8f) }
+ Text(
+ text = "Range: ${(rangeValue.start * 100).toInt()}% - ${(rangeValue.endInclusive * 100).toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ RangeSlider(
+ value = rangeValue,
+ onValueChange = { rangeValue = it },
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var rangeStepsValue by remember { mutableStateOf(2f..8f) }
+ Text(
+ text = "Range with Key Points: ${rangeStepsValue.start.toInt()} - ${rangeStepsValue.endInclusive.toInt()}",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ RangeSlider(
+ value = rangeStepsValue,
+ onValueChange = { rangeStepsValue = it },
+ valueRange = 0f..8f,
+ steps = 7,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ showKeyPoints = true,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var customRangeValue by remember { mutableStateOf(20f..80f) }
+ Text(
+ text = "Custom Range Points: ${customRangeValue.start.toInt()}% - ${customRangeValue.endInclusive.toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ RangeSlider(
+ value = customRangeValue,
+ onValueChange = { customRangeValue = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ keyPoints = listOf(0f, 20f, 40f, 60f, 80f, 100f),
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ var disabledRangeValue by remember { mutableStateOf(0.3f..0.7f) }
+ Text(
+ text = "Disabled: ${(disabledRangeValue.start * 100).toInt()}% - ${(disabledRangeValue.endInclusive * 100).toInt()}%",
+ fontSize = 14.sp,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(top = 12.dp, bottom = 4.dp)
+ )
+ RangeSlider(
+ value = disabledRangeValue,
+ onValueChange = {},
+ enabled = false,
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .padding(bottom = 12.dp)
+ )
+ }
+
+ // VerticalSlider
+ SmallTitle(text = "VerticalSlider")
+ Card(
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(bottom = 6.dp)
- )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp)
+ .padding(vertical = 12.dp),
+ horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ var verticalValue1 by remember { mutableStateOf(0.3f) }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f)
+ ) {
+ VerticalSlider(
+ value = verticalValue1,
+ onValueChange = { verticalValue1 = it },
+ modifier = Modifier.size(25.dp, 160.dp)
+ )
+ Text(
+ text = "Normal\n${(verticalValue1 * 100).toInt()}%",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ var verticalValue2 by remember { mutableStateOf(5f) }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f)
+ ) {
+ VerticalSlider(
+ value = verticalValue2,
+ onValueChange = { verticalValue2 = it },
+ valueRange = 0f..8f,
+ steps = 7,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ modifier = Modifier.size(25.dp, 160.dp)
+ )
+ Text(
+ text = "Steps\n${verticalValue2.toInt()}/8",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ var verticalValue3 by remember { mutableStateOf(5f) }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f)
+ ) {
+ VerticalSlider(
+ value = verticalValue3,
+ onValueChange = { verticalValue3 = it },
+ valueRange = 0f..8f,
+ steps = 7,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ showKeyPoints = true,
+ modifier = Modifier.size(25.dp, 160.dp)
+ )
+ Text(
+ text = "Points\n${verticalValue3.toInt()}/8",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ var verticalValue4 by remember { mutableStateOf(50f) }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f)
+ ) {
+ VerticalSlider(
+ value = verticalValue4,
+ onValueChange = { verticalValue4 = it },
+ valueRange = 0f..100f,
+ showKeyPoints = true,
+ hapticEffect = SliderDefaults.SliderHapticEffect.Step,
+ keyPoints = listOf(0f, 25f, 50f, 75f, 100f),
+ modifier = Modifier.size(25.dp, 160.dp)
+ )
+ Text(
+ text = "Custom\n${verticalValue4.toInt()}%",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ val disabledVerticalValue by remember { mutableStateOf(0.7f) }
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.weight(1f)
+ ) {
+ VerticalSlider(
+ value = disabledVerticalValue,
+ onValueChange = {},
+ enabled = false,
+ modifier = Modifier.size(25.dp, 160.dp)
+ )
+ Text(
+ text = "Disabled\n${(disabledVerticalValue * 100).toInt()}%",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
}
item(key = "tabRow") {
diff --git a/example/src/commonMain/kotlin/component/TextComponent.kt b/example/src/commonMain/kotlin/component/TextComponent.kt
index 4e77d488..92c52ff5 100644
--- a/example/src/commonMain/kotlin/component/TextComponent.kt
+++ b/example/src/commonMain/kotlin/component/TextComponent.kt
@@ -510,11 +510,10 @@ fun BottomSheet(
) {
LazyColumn {
item {
- var progress by remember { mutableStateOf(0.5f) }
+ var sliderValue by remember { mutableStateOf(0.5f) }
Slider(
- progress = progress,
- onProgressChange = { newProgress -> progress = newProgress },
- decimalPlaces = 3,
+ value = sliderValue,
+ onValueChange = { sliderValue = it },
modifier = Modifier.padding(bottom = 12.dp)
)
Card(
diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist
index 7a97f58a..d9db8a03 100644
--- a/iosApp/iosApp/Info.plist
+++ b/iosApp/iosApp/Info.plist
@@ -17,7 +17,7 @@
CFBundleShortVersionString
1.0.5
CFBundleVersion
- 569
+ 574
LSRequiresIPhoneOS
CADisableMinimumFrameDurationOnPhone
diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt
index 82f03d88..ca5ccf16 100644
--- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt
+++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ColorPicker.kt
@@ -868,7 +868,7 @@ private fun ColorSlider(
size.width.toFloat(),
with(density) { sliderHeightDp.toPx() }).coerceIn(0f, 1f)
onValueChangedState.value(newValue)
- hapticState.handleHapticFeedback(newValue, hapticEffect, hapticFeedback)
+ hapticState.handleHapticFeedback(newValue, 0f..1f, hapticEffect, hapticFeedback)
}
)
}
diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt
index 6a388f64..06596ff8 100644
--- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt
+++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/Slider.kt
@@ -3,21 +3,26 @@
package top.yukonga.miuix.kmp.basic
+import androidx.annotation.IntRange
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -35,54 +40,203 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.utils.G2RoundedCornerShape
-import kotlin.math.pow
-import kotlin.math.round
+import kotlin.math.abs
/**
* A [Slider] component with Miuix style.
*
- * @param progress The current progress of the [Slider].
- * @param onProgressChange The callback to be called when the progress changes.
+ * @param value The current value of the [Slider]. If outside of [valueRange] provided, value will be coerced to this range.
+ * @param onValueChange The callback to be called when the value changes.
* @param modifier The modifier to be applied to the [Slider].
* @param enabled Whether the [Slider] is enabled.
- * @param minValue The minimum value of the [Slider]. It is required
- * that [minValue] < [maxValue].
- * @param maxValue The maximum value of the [Slider].
+ * @param valueRange Range of values that this slider can take. The passed [value] will be coerced to this range.
+ * @param steps If positive, specifies the amount of discrete allowable values between the endpoints of [valueRange].
+ * For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly distributed between 0 and 10 (i.e., 2, 4, 6, 8).
+ * If [steps] is 0, the slider will behave continuously and allow any value from the range. Must not be negative.
+ * @param onValueChangeFinished Called when value change has ended. This should not be used to update the slider value
+ * (use [onValueChange] instead), but rather to know when the user has completed selecting a new value by ending a drag or a click.
+ * @param reverseDirection Controls the direction of this slider. When false (default), slider increases from left to right.
+ * When true, slider increases from right to left (useful for RTL layouts or custom direction requirements).
* @param height The height of the [Slider].
* @param colors The [SliderColors] of the [Slider].
* @param effect Whether to show the effect of the [Slider].
- * @param decimalPlaces The number of decimal places to be displayed in the drag indicator.
* @param hapticEffect The haptic effect of the [Slider].
+ * @param showKeyPoints Whether to show the key points (step indicators) on the slider. Only works when [keyPoints] is not null.
+ * @param keyPoints Custom key point values to display on the slider. If null, uses step positions from [steps] parameter.
+ * Values should be within [valueRange]. For example, for a range of 0f..100f, you might specify listOf(0f, 25f, 50f, 75f, 100f).
+ * @param magnetThreshold The magnetic snap threshold as a fraction (0.0 to 1.0). When the slider value is within this
+ * distance from a key point, it will snap to that point. Default is 0.02 (2%). Only applies when [keyPoints] is set.
*/
@Composable
fun Slider(
- progress: Float,
- onProgressChange: (Float) -> Unit,
+ value: Float,
+ onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- minValue: Float = 0f,
- maxValue: Float = 1f,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ @IntRange(from = 0) steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ reverseDirection: Boolean = false,
height: Dp = SliderDefaults.MinHeight,
colors: SliderColors = SliderDefaults.sliderColors(),
effect: Boolean = false,
- decimalPlaces: Int = 2,
- hapticEffect: SliderDefaults.SliderHapticEffect = SliderDefaults.DefaultHapticEffect
+ hapticEffect: SliderDefaults.SliderHapticEffect = SliderDefaults.DefaultHapticEffect,
+ showKeyPoints: Boolean = false,
+ keyPoints: List? = null,
+ magnetThreshold: Float = 0.02f
) {
+ require(steps >= 0) { "steps should be >= 0" }
+ require(valueRange.start < valueRange.endInclusive) { "valueRange start should be less than end" }
+
val hapticFeedback = LocalHapticFeedback.current
- var dragOffset by remember { mutableStateOf(0f) }
+ var dragOffset by remember { mutableFloatStateOf(0f) }
var isDragging by remember { mutableStateOf(false) }
- val factor = remember(decimalPlaces) { 10f.pow(decimalPlaces) }
val hapticState = remember { SliderHapticState() }
val interactionSource = remember { MutableInteractionSource() }
val shape = remember(height) { G2RoundedCornerShape(height) }
- val calculateProgress = remember(minValue, maxValue, factor) {
- { offset: Float, width: Int ->
- val newValue = (offset / width) * (maxValue - minValue) + minValue
- (round(newValue * factor) / factor).coerceIn(minValue, maxValue)
- }
+ val coercedValue = value.coerceIn(valueRange.start, valueRange.endInclusive)
+ val stepFractions = remember(steps) { stepsToTickFractions(steps) }
+
+ val keyPointFractions = remember(keyPoints, stepFractions, valueRange, showKeyPoints) {
+ computeKeyPointFractions(keyPoints, stepFractions, valueRange, showKeyPoints)
+ }
+
+ val allKeyPointFractions = remember(keyPoints, stepFractions, valueRange) {
+ computeAllKeyPointFractions(keyPoints, stepFractions, valueRange)
+ }
+
+ val calculateValue = remember(valueRange, steps, stepFractions, allKeyPointFractions, magnetThreshold, reverseDirection) {
+ createValueCalculator(valueRange, steps, stepFractions, allKeyPointFractions, magnetThreshold, reverseDirection)
+ }
+
+ Box(
+ modifier = modifier
+ .then(
+ if (enabled) {
+ Modifier
+ .pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onDragStart = { offset ->
+ isDragging = true
+ dragOffset = offset.x
+ val calculatedValue = calculateValue(dragOffset, size.width)
+ onValueChange(calculatedValue)
+ hapticState.reset(calculatedValue)
+ },
+ onHorizontalDrag = { _, dragAmount ->
+ dragOffset = (dragOffset + dragAmount).coerceIn(0f, size.width.toFloat())
+ val calculatedValue = calculateValue(dragOffset, size.width)
+ onValueChange(calculatedValue)
+ hapticState.handleHapticFeedback(
+ calculatedValue,
+ valueRange,
+ hapticEffect,
+ hapticFeedback,
+ allKeyPointFractions,
+ hasCustomKeyPoints = keyPoints != null
+ )
+ },
+ onDragEnd = {
+ isDragging = false
+ onValueChangeFinished?.invoke()
+ }
+ )
+ }
+ .indication(interactionSource, LocalIndication.current)
+ } else Modifier
+ ),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ SliderTrack(
+ modifier = Modifier.fillMaxWidth().height(height),
+ shape = shape,
+ backgroundColor = colors.backgroundColor(),
+ foregroundColor = colors.foregroundColor(enabled),
+ effect = effect,
+ value = coercedValue,
+ valueRange = valueRange,
+ isDragging = isDragging,
+ isVertical = false,
+ showKeyPoints = showKeyPoints,
+ stepFractions = keyPointFractions,
+ keyPointColor = colors.keyPointColor()
+ )
+ }
+}
+
+/**
+ * A vertical [Slider] component with Miuix style.
+ *
+ * @param value The current value of the [Slider]. If outside of [valueRange] provided, value will be coerced to this range.
+ * @param onValueChange The callback to be called when the value changes.
+ * @param modifier The modifier to be applied to the [Slider].
+ * @param enabled Whether the [Slider] is enabled.
+ * @param valueRange Range of values that this slider can take. The passed [value] will be coerced to this range.
+ * @param steps If positive, specifies the amount of discrete allowable values between the endpoints of [valueRange].
+ * @param onValueChangeFinished Called when value change has ended.
+ * @param reverseDirection Controls the direction of this slider. When false (default), slider increases from bottom to top.
+ * When true, slider increases from top to bottom.
+ * @param width The width of the vertical [Slider].
+ * @param colors The [SliderColors] of the [Slider].
+ * @param effect Whether to show the effect of the [Slider].
+ * @param hapticEffect The haptic effect of the [Slider].
+ * @param showKeyPoints Whether to show the key points (step indicators) on the slider. Only works when [keyPoints] is not null.
+ * @param keyPoints Custom key point values to display on the slider. If null, uses step positions from [steps] parameter.
+ * Values should be within [valueRange].
+ */
+@Composable
+fun VerticalSlider(
+ value: Float,
+ onValueChange: (Float) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ @IntRange(from = 0) steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ reverseDirection: Boolean = false,
+ width: Dp = SliderDefaults.MinHeight,
+ colors: SliderColors = SliderDefaults.sliderColors(),
+ effect: Boolean = false,
+ hapticEffect: SliderDefaults.SliderHapticEffect = SliderDefaults.DefaultHapticEffect,
+ showKeyPoints: Boolean = false,
+ keyPoints: List? = null,
+ magnetThreshold: Float = 0.02f
+) {
+ require(steps >= 0) { "steps should be >= 0" }
+ require(valueRange.start < valueRange.endInclusive) { "valueRange start should be less than end" }
+
+ val hapticFeedback = LocalHapticFeedback.current
+ var dragOffset by remember { mutableFloatStateOf(0f) }
+ var isDragging by remember { mutableStateOf(false) }
+ val hapticState = remember { SliderHapticState() }
+ val interactionSource = remember { MutableInteractionSource() }
+ val shape = remember(width) { G2RoundedCornerShape(width) }
+
+ val coercedValue = value.coerceIn(valueRange.start, valueRange.endInclusive)
+ val stepFractions = remember(steps) { stepsToTickFractions(steps) }
+
+ val keyPointFractions = remember(keyPoints, stepFractions, valueRange, showKeyPoints) {
+ computeKeyPointFractions(keyPoints, stepFractions, valueRange, showKeyPoints)
+ }
+
+ val allKeyPointFractions = remember(keyPoints, stepFractions, valueRange) {
+ computeAllKeyPointFractions(keyPoints, stepFractions, valueRange)
+ }
+
+ val calculateValue = remember(valueRange, steps, stepFractions, allKeyPointFractions, magnetThreshold, reverseDirection) {
+ createValueCalculator(
+ valueRange,
+ steps,
+ stepFractions,
+ allKeyPointFractions,
+ magnetThreshold,
+ reverseDirection,
+ isVertical = true
+ )
}
Box(
@@ -90,39 +244,210 @@ fun Slider(
.then(
if (enabled) {
Modifier.pointerInput(Unit) {
- detectHorizontalDragGestures(
+ detectVerticalDragGestures(
onDragStart = { offset ->
isDragging = true
- dragOffset = offset.x
- val calculatedValue = calculateProgress(dragOffset, size.width)
- onProgressChange(calculatedValue)
+ dragOffset = offset.y
+ val calculatedValue = calculateValue(dragOffset, size.height)
+ onValueChange(calculatedValue)
hapticState.reset(calculatedValue)
},
- onHorizontalDrag = { _, dragAmount ->
- dragOffset = (dragOffset + dragAmount).coerceIn(0f, size.width.toFloat())
- val calculatedValue = calculateProgress(dragOffset, size.width)
- onProgressChange(calculatedValue)
- hapticState.handleHapticFeedback(calculatedValue, hapticEffect, hapticFeedback)
+ onVerticalDrag = { _, dragAmount ->
+ dragOffset = (dragOffset + dragAmount).coerceIn(0f, size.height.toFloat())
+ val calculatedValue = calculateValue(dragOffset, size.height)
+ onValueChange(calculatedValue)
+ hapticState.handleHapticFeedback(
+ calculatedValue,
+ valueRange,
+ hapticEffect,
+ hapticFeedback,
+ allKeyPointFractions,
+ hasCustomKeyPoints = keyPoints != null
+ )
},
onDragEnd = {
isDragging = false
+ onValueChangeFinished?.invoke()
}
)
}.indication(interactionSource, LocalIndication.current)
} else Modifier
),
- contentAlignment = Alignment.CenterStart
+ contentAlignment = Alignment.BottomCenter
) {
SliderTrack(
+ modifier = Modifier.width(width).fillMaxHeight(),
+ shape = shape,
+ backgroundColor = colors.backgroundColor(),
+ foregroundColor = colors.foregroundColor(enabled),
+ effect = effect,
+ value = coercedValue,
+ valueRange = valueRange,
+ isDragging = isDragging,
+ isVertical = true,
+ showKeyPoints = showKeyPoints,
+ stepFractions = keyPointFractions,
+ keyPointColor = colors.keyPointColor()
+ )
+ }
+}
+
+/**
+ * A [RangeSlider] component with Miuix style.
+ *
+ * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
+ * The two values are still bounded by the value range but they also cannot cross each other.
+ *
+ * @param value Current values of the RangeSlider. If either value is outside of [valueRange] provided, it will be coerced to this range.
+ * @param onValueChange Lambda in which values should be updated.
+ * @param modifier The modifier to be applied to the [RangeSlider].
+ * @param enabled Whether the [RangeSlider] is enabled.
+ * @param valueRange Range of values that Range Slider values can take. Passed [value] will be coerced to this range.
+ * @param steps If positive, specifies the amount of discrete allowable values between the endpoints of [valueRange].
+ * @param onValueChangeFinished Lambda to be invoked when value change has ended.
+ * @param height The height of the [RangeSlider].
+ * @param colors The [SliderColors] of the [RangeSlider].
+ * @param effect Whether to show the effect of the [RangeSlider].
+ * @param hapticEffect The haptic effect of the [RangeSlider].
+ * @param showKeyPoints Whether to show the key points (step indicators) on the slider. Only works when [keyPoints] is not null.
+ * @param keyPoints Custom key point values to display on the slider. If null, uses step positions from [steps] parameter.
+ * Values should be within [valueRange].
+ * @param magnetThreshold The magnetic snap threshold as a fraction (0.0 to 1.0). When the slider value is within this
+ * distance from a key point, it will snap to that point. Default is 0.02 (2%). Only applies when [keyPoints] is set.
+ */
+@Composable
+fun RangeSlider(
+ value: ClosedFloatingPointRange,
+ onValueChange: (ClosedFloatingPointRange) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ valueRange: ClosedFloatingPointRange = 0f..1f,
+ @IntRange(from = 0) steps: Int = 0,
+ onValueChangeFinished: (() -> Unit)? = null,
+ height: Dp = SliderDefaults.MinHeight,
+ colors: SliderColors = SliderDefaults.sliderColors(),
+ effect: Boolean = false,
+ hapticEffect: SliderDefaults.SliderHapticEffect = SliderDefaults.DefaultHapticEffect,
+ showKeyPoints: Boolean = false,
+ keyPoints: List? = null,
+ magnetThreshold: Float = 0.02f
+) {
+ require(steps >= 0) { "steps should be >= 0" }
+ require(valueRange.start < valueRange.endInclusive) { "valueRange start should be less than end" }
+
+ val hapticFeedback = LocalHapticFeedback.current
+ var startDragOffset by remember { mutableFloatStateOf(0f) }
+ var endDragOffset by remember { mutableFloatStateOf(0f) }
+ var isDraggingStart by remember { mutableStateOf(false) }
+ var isDraggingEnd by remember { mutableStateOf(false) }
+ val isDragging = isDraggingStart || isDraggingEnd
+ val hapticState = remember { RangeSliderHapticState() }
+ val interactionSource = remember { MutableInteractionSource() }
+ val shape = remember(height) { G2RoundedCornerShape(height) }
+
+ var currentStartValue by remember { mutableFloatStateOf(value.start) }
+ var currentEndValue by remember { mutableFloatStateOf(value.endInclusive) }
+
+ if (!isDragging) {
+ currentStartValue = value.start
+ currentEndValue = value.endInclusive
+ }
+
+ val coercedStart = currentStartValue.coerceIn(valueRange.start, valueRange.endInclusive)
+ val coercedEnd = currentEndValue.coerceIn(valueRange.start, valueRange.endInclusive)
+ val stepFractions = remember(steps) { stepsToTickFractions(steps) }
+
+ val keyPointFractions = remember(keyPoints, stepFractions, valueRange, showKeyPoints) {
+ computeKeyPointFractions(keyPoints, stepFractions, valueRange, showKeyPoints)
+ }
+
+ val allKeyPointFractions = remember(keyPoints, stepFractions, valueRange) {
+ computeAllKeyPointFractions(keyPoints, stepFractions, valueRange)
+ }
+
+ val calculateValue = remember(valueRange, steps, stepFractions, allKeyPointFractions, magnetThreshold) {
+ createValueCalculator(valueRange, steps, stepFractions, allKeyPointFractions, magnetThreshold)
+ }
+
+ Box(
+ modifier = modifier
+ .then(
+ if (enabled) {
+ Modifier
+ .pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onDragStart = { offset ->
+ val startPos =
+ (coercedStart - valueRange.start) / (valueRange.endInclusive - valueRange.start) * size.width
+ val endPos =
+ (coercedEnd - valueRange.start) / (valueRange.endInclusive - valueRange.start) * size.width
+ val diffStart = abs(offset.x - startPos)
+ val diffEnd = abs(offset.x - endPos)
+
+ if (diffStart < diffEnd) {
+ isDraggingStart = true
+ startDragOffset = offset.x
+ hapticState.resetStart(coercedStart)
+ } else {
+ isDraggingEnd = true
+ endDragOffset = offset.x
+ hapticState.resetEnd(coercedEnd)
+ }
+ },
+ onHorizontalDrag = { _, dragAmount ->
+ if (isDraggingStart) {
+ startDragOffset = (startDragOffset + dragAmount).coerceIn(0f, size.width.toFloat())
+ val newStart = calculateValue(startDragOffset, size.width).coerceAtMost(currentEndValue)
+ currentStartValue = newStart
+ onValueChange(newStart..currentEndValue)
+ hapticState.handleStartHapticFeedback(
+ newStart,
+ valueRange,
+ hapticEffect,
+ hapticFeedback,
+ allKeyPointFractions,
+ hasCustomKeyPoints = keyPoints != null
+ )
+ } else if (isDraggingEnd) {
+ endDragOffset = (endDragOffset + dragAmount).coerceIn(0f, size.width.toFloat())
+ val newEnd = calculateValue(endDragOffset, size.width).coerceAtLeast(currentStartValue)
+ currentEndValue = newEnd
+ onValueChange(currentStartValue..newEnd)
+ hapticState.handleEndHapticFeedback(
+ newEnd,
+ valueRange,
+ hapticEffect,
+ hapticFeedback,
+ allKeyPointFractions,
+ hasCustomKeyPoints = keyPoints != null
+ )
+ }
+ },
+ onDragEnd = {
+ isDraggingStart = false
+ isDraggingEnd = false
+ onValueChangeFinished?.invoke()
+ }
+ )
+ }
+ .indication(interactionSource, LocalIndication.current)
+ } else Modifier
+ ),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ RangeSliderTrack(
modifier = Modifier.fillMaxWidth().height(height),
shape = shape,
backgroundColor = colors.backgroundColor(),
foregroundColor = colors.foregroundColor(enabled),
effect = effect,
- progress = progress,
- minValue = minValue,
- maxValue = maxValue,
+ valueStart = coercedStart,
+ valueEnd = coercedEnd,
+ valueRange = valueRange,
isDragging = isDragging,
+ showKeyPoints = showKeyPoints,
+ stepFractions = keyPointFractions,
+ keyPointColor = colors.keyPointColor()
)
}
}
@@ -137,10 +462,95 @@ private fun SliderTrack(
backgroundColor: Color,
foregroundColor: Color,
effect: Boolean,
- progress: Float,
- minValue: Float,
- maxValue: Float,
+ value: Float,
+ valueRange: ClosedFloatingPointRange,
+ isDragging: Boolean,
+ isVertical: Boolean,
+ showKeyPoints: Boolean,
+ stepFractions: FloatArray,
+ keyPointColor: Color
+) {
+ val backgroundAlpha by animateFloatAsState(
+ targetValue = if (isDragging) 0.044f else 0f,
+ animationSpec = tween(150)
+ )
+
+ Canvas(
+ modifier = modifier
+ .clip(shape)
+ .background(backgroundColor)
+ .drawBehind { drawRect(Color.Black.copy(alpha = backgroundAlpha)) }
+ ) {
+ val barHeight = size.height
+ val barWidth = size.width
+ val fraction = (value - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ val cornerRadius = if (effect) {
+ if (isVertical) CornerRadius(barWidth / 2) else CornerRadius(barHeight / 2)
+ } else {
+ CornerRadius.Zero
+ }
+
+ if (isVertical) {
+ val progressHeight = barHeight * fraction
+ drawRoundRect(
+ color = foregroundColor,
+ size = Size(barWidth, progressHeight),
+ topLeft = Offset(0f, barHeight - progressHeight),
+ cornerRadius = cornerRadius
+ )
+
+ if (showKeyPoints && stepFractions.isNotEmpty()) {
+ val keyPointRadius = barWidth / 6f
+ stepFractions.forEach { stepFraction ->
+ val y = barHeight * (1f - stepFraction)
+ drawCircle(
+ color = keyPointColor,
+ radius = keyPointRadius,
+ center = Offset(barWidth / 2f, y)
+ )
+ }
+ }
+ } else {
+ val progressWidth = barWidth * fraction
+ drawRoundRect(
+ color = foregroundColor,
+ size = Size(progressWidth, barHeight),
+ topLeft = Offset(0f, 0f),
+ cornerRadius = cornerRadius
+ )
+
+ if (showKeyPoints && stepFractions.isNotEmpty()) {
+ val keyPointRadius = barHeight / 6f
+ stepFractions.forEach { stepFraction ->
+ val x = barWidth * stepFraction
+ drawCircle(
+ color = keyPointColor,
+ radius = keyPointRadius,
+ center = Offset(x, barHeight / 2f)
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Internal range slider track renderer
+ */
+@Composable
+private fun RangeSliderTrack(
+ modifier: Modifier,
+ shape: G2RoundedCornerShape,
+ backgroundColor: Color,
+ foregroundColor: Color,
+ effect: Boolean,
+ valueStart: Float,
+ valueEnd: Float,
+ valueRange: ClosedFloatingPointRange,
isDragging: Boolean,
+ showKeyPoints: Boolean,
+ stepFractions: FloatArray,
+ keyPointColor: Color
) {
val backgroundAlpha by animateFloatAsState(
targetValue = if (isDragging) 0.044f else 0f,
@@ -155,15 +565,30 @@ private fun SliderTrack(
) {
val barHeight = size.height
val barWidth = size.width
- val progressWidth = barWidth * ((progress - minValue) / (maxValue - minValue))
+ val startFraction = (valueStart - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ val endFraction = (valueEnd - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ val startX = barWidth * startFraction
+ val endX = barWidth * endFraction
val cornerRadius = if (effect) CornerRadius(barHeight / 2) else CornerRadius.Zero
drawRoundRect(
color = foregroundColor,
- size = Size(progressWidth, barHeight),
- topLeft = Offset(0f, center.y - barHeight / 2),
+ size = Size(endX - startX, barHeight),
+ topLeft = Offset(startX, 0f),
cornerRadius = cornerRadius
)
+
+ if (showKeyPoints && stepFractions.isNotEmpty()) {
+ val keyPointRadius = barHeight / 6f
+ stepFractions.forEach { stepFraction ->
+ val x = barWidth * stepFraction
+ drawCircle(
+ color = keyPointColor,
+ radius = keyPointRadius,
+ center = Offset(x, barHeight / 2f)
+ )
+ }
+ }
}
}
@@ -173,38 +598,313 @@ private fun SliderTrack(
internal class SliderHapticState {
private var edgeFeedbackTriggered: Boolean = false
private var lastStep: Float = 0f
+ private var isAtKeyPoint: Boolean = false
fun reset(currentValue: Float) {
edgeFeedbackTriggered = false
lastStep = currentValue
+ isAtKeyPoint = false
}
fun handleHapticFeedback(
currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
hapticEffect: SliderDefaults.SliderHapticEffect,
- hapticFeedback: HapticFeedback
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray = floatArrayOf(),
+ hasCustomKeyPoints: Boolean = false
) {
if (hapticEffect == SliderDefaults.SliderHapticEffect.None) return
- val isAtEdge = currentValue == 0f || currentValue == 1f
+ handleEdgeHaptic(currentValue, valueRange, hapticFeedback)
+
+ if (hapticEffect == SliderDefaults.SliderHapticEffect.Step) {
+ handleStepHaptic(currentValue, valueRange, hapticFeedback, keyPointFractions, hasCustomKeyPoints)
+ }
+ }
+
+ private fun handleEdgeHaptic(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticFeedback: HapticFeedback
+ ) {
+ val isAtEdge = currentValue == valueRange.start || currentValue == valueRange.endInclusive
if (isAtEdge && !edgeFeedbackTriggered) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
edgeFeedbackTriggered = true
} else if (!isAtEdge) {
edgeFeedbackTriggered = false
}
+ }
+
+ private fun handleStepHaptic(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray,
+ hasCustomKeyPoints: Boolean
+ ) {
+ val isNotAtEdge = currentValue != valueRange.start && currentValue != valueRange.endInclusive
+
+ if (hasCustomKeyPoints && keyPointFractions.isNotEmpty()) {
+ handleKeyPointHaptic(currentValue, valueRange, hapticFeedback, keyPointFractions, isNotAtEdge)
+ } else if (currentValue != lastStep && isNotAtEdge) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ lastStep = currentValue
+ }
+ }
+
+ private fun handleKeyPointHaptic(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray,
+ isNotAtEdge: Boolean
+ ) {
+ val fraction = (currentValue - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ val threshold = 0.005f
+
+ val nearestKeyPoint = keyPointFractions.minByOrNull { abs(it - fraction) }
+ val currentlyAtKeyPoint = nearestKeyPoint != null && abs(fraction - nearestKeyPoint) < threshold
+
+ if (currentlyAtKeyPoint && !isAtKeyPoint && isNotAtEdge) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+
+ isAtKeyPoint = currentlyAtKeyPoint
+ }
+}
+
+/**
+ * Manages haptic feedback state for the range slider.
+ */
+internal class RangeSliderHapticState {
+ private var startEdgeFeedbackTriggered: Boolean = false
+ private var endEdgeFeedbackTriggered: Boolean = false
+ private var startLastStep: Float = 0f
+ private var endLastStep: Float = 0f
+ private var startIsAtKeyPoint: Boolean = false
+ private var endIsAtKeyPoint: Boolean = false
+
+ fun resetStart(currentValue: Float) {
+ startEdgeFeedbackTriggered = false
+ startLastStep = currentValue
+ startIsAtKeyPoint = false
+ }
+
+ fun resetEnd(currentValue: Float) {
+ endEdgeFeedbackTriggered = false
+ endLastStep = currentValue
+ endIsAtKeyPoint = false
+ }
+
+ fun handleStartHapticFeedback(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticEffect: SliderDefaults.SliderHapticEffect,
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray = floatArrayOf(),
+ hasCustomKeyPoints: Boolean = false
+ ) {
+ handleHapticFeedbackInternal(
+ currentValue = currentValue,
+ valueRange = valueRange,
+ hapticEffect = hapticEffect,
+ hapticFeedback = hapticFeedback,
+ keyPointFractions = keyPointFractions,
+ edgeFeedbackTriggered = startEdgeFeedbackTriggered,
+ lastStep = startLastStep,
+ isAtKeyPoint = startIsAtKeyPoint,
+ isStartEdge = true,
+ hasCustomKeyPoints = hasCustomKeyPoints,
+ onEdgeFeedbackUpdate = { startEdgeFeedbackTriggered = it },
+ onLastStepUpdate = { startLastStep = it },
+ onKeyPointUpdate = { startIsAtKeyPoint = it }
+ )
+ }
+
+ fun handleEndHapticFeedback(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticEffect: SliderDefaults.SliderHapticEffect,
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray = floatArrayOf(),
+ hasCustomKeyPoints: Boolean = false
+ ) {
+ handleHapticFeedbackInternal(
+ currentValue = currentValue,
+ valueRange = valueRange,
+ hapticEffect = hapticEffect,
+ hapticFeedback = hapticFeedback,
+ keyPointFractions = keyPointFractions,
+ edgeFeedbackTriggered = endEdgeFeedbackTriggered,
+ lastStep = endLastStep,
+ isAtKeyPoint = endIsAtKeyPoint,
+ isStartEdge = false,
+ hasCustomKeyPoints = hasCustomKeyPoints,
+ onEdgeFeedbackUpdate = { endEdgeFeedbackTriggered = it },
+ onLastStepUpdate = { endLastStep = it },
+ onKeyPointUpdate = { endIsAtKeyPoint = it }
+ )
+ }
+
+ private fun handleHapticFeedbackInternal(
+ currentValue: Float,
+ valueRange: ClosedFloatingPointRange,
+ hapticEffect: SliderDefaults.SliderHapticEffect,
+ hapticFeedback: HapticFeedback,
+ keyPointFractions: FloatArray,
+ edgeFeedbackTriggered: Boolean,
+ lastStep: Float,
+ isAtKeyPoint: Boolean,
+ isStartEdge: Boolean,
+ hasCustomKeyPoints: Boolean,
+ onEdgeFeedbackUpdate: (Boolean) -> Unit,
+ onLastStepUpdate: (Float) -> Unit,
+ onKeyPointUpdate: (Boolean) -> Unit
+ ) {
+ if (hapticEffect == SliderDefaults.SliderHapticEffect.None) return
+
+ val targetEdge = if (isStartEdge) valueRange.start else valueRange.endInclusive
+ val isAtEdge = currentValue == targetEdge
+
+ if (isAtEdge && !edgeFeedbackTriggered) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
+ onEdgeFeedbackUpdate(true)
+ } else if (!isAtEdge) {
+ onEdgeFeedbackUpdate(false)
+ }
if (hapticEffect == SliderDefaults.SliderHapticEffect.Step) {
- val isNotAtEdge = currentValue != 0f && currentValue != 1f
- if (currentValue != lastStep && isNotAtEdge) {
+ val isNotAtEdge = currentValue != targetEdge
+
+ if (hasCustomKeyPoints && keyPointFractions.isNotEmpty()) {
+ val fraction = (currentValue - valueRange.start) / (valueRange.endInclusive - valueRange.start)
+ val threshold = 0.005f
+
+ val nearestKeyPoint = keyPointFractions.minByOrNull { abs(it - fraction) }
+ val currentlyAtKeyPoint = nearestKeyPoint != null && abs(fraction - nearestKeyPoint) < threshold
+
+ if (currentlyAtKeyPoint && !isAtKeyPoint && isNotAtEdge) {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+ }
+
+ onKeyPointUpdate(currentlyAtKeyPoint)
+ } else if (currentValue != lastStep && isNotAtEdge) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
- lastStep = currentValue
+ onLastStepUpdate(currentValue)
}
}
}
}
+private fun stepsToTickFractions(steps: Int): FloatArray {
+ return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
+}
+
+
+private fun snapValueToTick(
+ current: Float,
+ tickFractions: FloatArray,
+ minPx: Float,
+ maxPx: Float
+): Float {
+ return tickFractions
+ .minByOrNull { abs(lerp(minPx, maxPx, it) - current) }
+ ?.run { lerp(minPx, maxPx, this) }
+ ?: current
+}
+
+/**
+ * Converts point values to normalized fractions within the value range.
+ */
+private fun pointsToFractions(
+ points: List,
+ valueRange: ClosedFloatingPointRange
+): FloatArray {
+ return points.map { point ->
+ ((point - valueRange.start) / (valueRange.endInclusive - valueRange.start))
+ .coerceIn(0f, 1f)
+ }.toFloatArray()
+}
+
+/**
+ * Computes key point fractions for slider display.
+ * Filters out points too close to edges (within 2% margin).
+ */
+private fun computeKeyPointFractions(
+ keyPoints: List?,
+ stepFractions: FloatArray,
+ valueRange: ClosedFloatingPointRange,
+ showKeyPoints: Boolean
+): FloatArray {
+ val baseFractions = when {
+ keyPoints != null -> pointsToFractions(keyPoints, valueRange)
+ showKeyPoints -> stepFractions
+ else -> floatArrayOf()
+ }
+
+ return baseFractions.filter { fraction -> fraction > 0.02f && fraction < 0.98f }.toFloatArray()
+}
+
+/**
+ * Computes all key point fractions including edge points.
+ * Used for haptic feedback and magnetic snapping.
+ */
+private fun computeAllKeyPointFractions(
+ keyPoints: List?,
+ stepFractions: FloatArray,
+ valueRange: ClosedFloatingPointRange
+): FloatArray {
+ return when {
+ keyPoints != null -> pointsToFractions(keyPoints, valueRange)
+ stepFractions.isNotEmpty() -> stepFractions
+ else -> floatArrayOf()
+ }
+}
+
+/**
+ * Creates a value calculator for slider position to value conversion.
+ */
+private fun createValueCalculator(
+ valueRange: ClosedFloatingPointRange,
+ steps: Int,
+ stepFractions: FloatArray,
+ allKeyPointFractions: FloatArray,
+ magnetThreshold: Float,
+ reverseDirection: Boolean = false,
+ isVertical: Boolean = false
+): (offset: Float, size: Int) -> Float {
+ return { offset: Float, size: Int ->
+ val adjustedOffset = when {
+ isVertical -> if (reverseDirection) offset else size - offset
+ else -> if (reverseDirection) size - offset else offset
+ }
+ val fraction = (adjustedOffset / size).coerceIn(0f, 1f)
+ val newValue = lerp(valueRange.start, valueRange.endInclusive, fraction)
+
+ when {
+ steps > 0 -> snapValueToTick(newValue, stepFractions, valueRange.start, valueRange.endInclusive)
+ allKeyPointFractions.isNotEmpty() -> {
+ val closestKeyPoint = allKeyPointFractions.minByOrNull { keyPointFraction ->
+ abs(fraction - keyPointFraction)
+ }
+ if (closestKeyPoint != null && abs(fraction - closestKeyPoint) < magnetThreshold) {
+ lerp(valueRange.start, valueRange.endInclusive, closestKeyPoint)
+ } else {
+ newValue
+ }
+ }
+
+ else -> newValue
+ }
+ }
+}
+
object SliderDefaults {
+ /**
+ * The minimum height of the [Slider] and [RangeSlider].
+ */
val MinHeight = 30.dp
/**
@@ -221,17 +921,22 @@ object SliderDefaults {
Step
}
+ /**
+ * The default haptic effect of the [Slider] and [RangeSlider].
+ */
val DefaultHapticEffect = SliderHapticEffect.Edge
@Composable
fun sliderColors(
foregroundColor: Color = MiuixTheme.colorScheme.primary,
disabledForegroundColor: Color = MiuixTheme.colorScheme.disabledPrimarySlider,
- backgroundColor: Color = MiuixTheme.colorScheme.tertiaryContainerVariant
+ backgroundColor: Color = MiuixTheme.colorScheme.tertiaryContainerVariant,
+ keyPointColor: Color = Color(0x4DA3B3CD)
): SliderColors = SliderColors(
foregroundColor = foregroundColor,
disabledForegroundColor = disabledForegroundColor,
- backgroundColor = backgroundColor
+ backgroundColor = backgroundColor,
+ keyPointColor = keyPointColor
)
}
@@ -239,7 +944,8 @@ object SliderDefaults {
class SliderColors(
private val foregroundColor: Color,
private val disabledForegroundColor: Color,
- private val backgroundColor: Color
+ private val backgroundColor: Color,
+ private val keyPointColor: Color
) {
@Stable
internal fun foregroundColor(enabled: Boolean): Color =
@@ -247,4 +953,7 @@ class SliderColors(
@Stable
internal fun backgroundColor(): Color = backgroundColor
+
+ @Stable
+ internal fun keyPointColor(): Color = keyPointColor
}
\ No newline at end of file