Skip to content

Commit e160e4a

Browse files
committed
Add step buttons to slider preferences and improve default value handling
1 parent 16929e7 commit e160e4a

4 files changed

Lines changed: 70 additions & 15 deletions

File tree

app/src/main/java/com/drdisagree/iconify/core/preferences/PreferenceCategoryScope.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ class PreferenceCategoryScope {
142142
showResetButton: Boolean = false,
143143
showDefaultIndicator: Boolean = false,
144144
hideDefaultValue: Boolean = false,
145+
showStepButtons: Boolean = true,
145146
summary: ((PrefParam<Float>) -> PrefStringRes?)? = null,
146147
isVisible: ((PreferenceController) -> Boolean) = { true },
147148
isEnabled: ((PreferenceController) -> Boolean) = { true },
@@ -152,7 +153,7 @@ class PreferenceCategoryScope {
152153
defaultValue = defaultValue.toPrefValue(),
153154
type = PreferenceType.Slider(
154155
min, max, steps, valueLabel, applyImmediately,
155-
showResetButton, showDefaultIndicator, hideDefaultValue
156+
showResetButton, showDefaultIndicator, hideDefaultValue, showStepButtons
156157
),
157158
icon = icon,
158159
summary = summary as ((PrefParam<Any?>) -> PrefStringRes?)?,
@@ -174,6 +175,7 @@ class PreferenceCategoryScope {
174175
showResetButton: Boolean = false,
175176
showDefaultIndicator: Boolean = false,
176177
hideDefaultValue: Boolean = false,
178+
showStepButtons: Boolean = true,
177179
summary: ((PrefParam<Float>) -> PrefStringRes?)? = null,
178180
isVisible: ((PreferenceController) -> Boolean) = { true },
179181
isEnabled: ((PreferenceController) -> Boolean) = { true },
@@ -184,7 +186,7 @@ class PreferenceCategoryScope {
184186
defaultValue = defaultValue.toPrefValue(),
185187
type = PreferenceType.Slider(
186188
min, max, steps, valueLabel, applyImmediately,
187-
showResetButton, showDefaultIndicator, hideDefaultValue
189+
showResetButton, showDefaultIndicator, hideDefaultValue, showStepButtons
188190
),
189191
icon = icon,
190192
summary = summary as ((PrefParam<Any?>) -> PrefStringRes?)?,

app/src/main/java/com/drdisagree/iconify/core/preferences/PreferenceType.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ sealed class PreferenceType {
1414
val applyImmediately: Boolean,
1515
val showResetButton: Boolean = false,
1616
val showDefaultIndicator: Boolean = false,
17-
val hideDefaultValue: Boolean = false
17+
val hideDefaultValue: Boolean = false,
18+
val showStepButtons: Boolean = true
1819
) : PreferenceType()
1920

2021
data class ListPref(

app/src/main/java/com/drdisagree/iconify/core/ui/components/preferences/SliderPreferenceItem.kt

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.padding
99
import androidx.compose.foundation.layout.size
1010
import androidx.compose.foundation.layout.wrapContentHeight
1111
import androidx.compose.foundation.shape.RoundedCornerShape
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.rounded.Add
14+
import androidx.compose.material.icons.rounded.Remove
1215
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
1316
import androidx.compose.material3.Icon
1417
import androidx.compose.material3.IconButton
@@ -67,16 +70,24 @@ fun SliderPreferenceItem(
6770
} else {
6871
0.0001f
6972
}
70-
val isAtDefault = abs(sliderValue - defaultValue) <= defaultEpsilon
73+
74+
fun labelOf(value: Float) = type.valueLabel?.invoke(value)
75+
?: value.roundToInt().toString()
76+
77+
// Treat a value as default when it's within epsilon OR renders the same label as
78+
// the default (a continuous slider snaps many raw floats to one rounded "Ndp" label).
79+
fun isDefault(value: Float) =
80+
abs(value - defaultValue) <= defaultEpsilon || labelOf(value) == labelOf(defaultValue)
81+
82+
val isAtDefault = isDefault(sliderValue)
7183

7284
LaunchedEffect(persistedValue) {
7385
if (sliderValue != persistedValue) {
7486
sliderValue = persistedValue
7587
}
7688
}
7789

78-
val originalValueLabel = type.valueLabel?.invoke(sliderValue)
79-
?: sliderValue.roundToInt().toString()
90+
val originalValueLabel = labelOf(sliderValue)
8091
val valueLabel = if (type.showDefaultIndicator && isAtDefault) {
8192
if (type.hideDefaultValue) {
8293
stringResource(R.string.opt_default).replaceAll("(" to "", ")" to "")
@@ -104,8 +115,7 @@ fun SliderPreferenceItem(
104115
fun updateUiValue(newValue: Float) {
105116
if (!isEnabled || sliderValue == newValue) return
106117

107-
val newLabel = type.valueLabel?.invoke(newValue)
108-
?: newValue.roundToInt().toString()
118+
val newLabel = labelOf(newValue)
109119

110120
if (newLabel != previousLabel) {
111121
onValueChangeWithHaptic()
@@ -116,14 +126,25 @@ fun SliderPreferenceItem(
116126
}
117127

118128
fun persistValue(value: Float) {
119-
val snapped = if (abs(value - defaultValue) <= defaultEpsilon) {
120-
defaultValue
121-
} else {
122-
value
123-
}
129+
// Snap to exact default when it reads as default so the stored value stays clean
130+
// and matches what the "Default" label and reset button report.
131+
val snapped = if (isDefault(value)) defaultValue else value
124132
prefController.setFloat(prefDefinition.key, snapped)
125133
}
126134

135+
// One discrete step; matches the slider's tick spacing, or 1 for a continuous slider.
136+
val stepIncrement = if (type.steps > 0) {
137+
(type.max - type.min) / (type.steps + 1)
138+
} else {
139+
1f
140+
}
141+
142+
fun stepBy(delta: Float) {
143+
val newValue = (sliderValue + delta).coerceIn(type.min, type.max)
144+
updateUiValue(newValue)
145+
persistValue(newValue)
146+
}
147+
127148
PreferenceContainer(
128149
shape = shape,
129150
isEnabled = isEnabled,
@@ -148,8 +169,23 @@ fun SliderPreferenceItem(
148169
.fillMaxWidth()
149170
.padding(top = 6.dp),
150171
verticalAlignment = Alignment.CenterVertically,
151-
horizontalArrangement = Arrangement.spacedBy(16.dp)
172+
horizontalArrangement = Arrangement.spacedBy(8.dp)
152173
) {
174+
if (type.showStepButtons) {
175+
IconButton(
176+
enabled = isEnabled && sliderValue > type.min,
177+
shapes = IconButtonDefaults.shapes(),
178+
colors = IconButtonDefaults.filledTonalIconButtonColors(),
179+
onClick = { stepBy(-stepIncrement) },
180+
modifier = Modifier.size(28.dp)
181+
) {
182+
Icon(
183+
imageVector = Icons.Rounded.Remove,
184+
contentDescription = stringResource(R.string.btn_decrease),
185+
modifier = Modifier.size(16.dp)
186+
)
187+
}
188+
}
153189
Slider(
154190
value = sliderValue,
155191
onValueChange = { newValue ->
@@ -170,8 +206,22 @@ fun SliderPreferenceItem(
170206
modifier = Modifier
171207
.wrapContentHeight()
172208
.weight(1f)
173-
.padding(top = 4.dp)
174209
)
210+
if (type.showStepButtons) {
211+
IconButton(
212+
enabled = isEnabled && sliderValue < type.max,
213+
shapes = IconButtonDefaults.shapes(),
214+
colors = IconButtonDefaults.filledTonalIconButtonColors(),
215+
onClick = { stepBy(stepIncrement) },
216+
modifier = Modifier.size(28.dp)
217+
) {
218+
Icon(
219+
imageVector = Icons.Rounded.Add,
220+
contentDescription = stringResource(R.string.btn_increase),
221+
modifier = Modifier.size(16.dp)
222+
)
223+
}
224+
}
175225
if (type.showResetButton) {
176226
IconButton(
177227
enabled = isEnabled && !isAtDefault,

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,8 @@
11801180
<string name="btn_disable_custom_colors">Disable Custom Colors</string>
11811181
<string name="btn_enable_custom_colors">Apply Colors</string>
11821182
<string name="btn_reset">Reset</string>
1183+
<string name="btn_decrease">Decrease</string>
1184+
<string name="btn_increase">Increase</string>
11831185
<string name="btn_reboot_now">Reboot Now</string>
11841186
<string name="btn_download">Download</string>
11851187
<string name="btn_restart_systemui">Restart SystemUI</string>

0 commit comments

Comments
 (0)