Skip to content

Commit aee4207

Browse files
neobuddy89joeyhuab
authored andcommitted
SystemUI: Add volume slider gradient customization
Signed-off-by: Pranav Vashi <neobuddy89@gmail.com>
1 parent 3af70d3 commit aee4207

2 files changed

Lines changed: 221 additions & 7 deletions

File tree

core/java/android/provider/Settings.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7723,6 +7723,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean
77237723
*/
77247724
public static final String QS_BRIGHTNESS_SLIDER_GRADIENT = "qs_brightness_slider_gradient";
77257725

7726+
/**
7727+
* Gradient on Volume slider
7728+
* @hide
7729+
*/
7730+
public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient";
7731+
77267732
/**
77277733
* Keys we no longer back up under the current schema, but want to continue to
77287734
* process when restoring historical backup datasets.

packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt

Lines changed: 215 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,47 @@
1616

1717
package com.android.systemui.volume.dialog.sliders.ui.compose
1818

19+
import android.database.ContentObserver
20+
import android.os.UserHandle
21+
import android.provider.Settings
1922
import androidx.compose.foundation.layout.Box
2023
import androidx.compose.foundation.layout.BoxScope
24+
import androidx.compose.foundation.layout.fillMaxHeight
2125
import androidx.compose.foundation.layout.fillMaxSize
26+
import androidx.compose.foundation.layout.fillMaxWidth
2227
import androidx.compose.foundation.layout.height
28+
import androidx.compose.foundation.layout.size
2329
import androidx.compose.foundation.layout.width
2430
import androidx.compose.material3.ExperimentalMaterial3Api
2531
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2632
import androidx.compose.material3.LocalContentColor
33+
import androidx.compose.material3.MaterialTheme
2734
import androidx.compose.material3.SliderColors
2835
import androidx.compose.material3.SliderDefaults
2936
import androidx.compose.material3.SliderState
3037
import androidx.compose.runtime.Composable
3138
import androidx.compose.runtime.CompositionLocalProvider
39+
import androidx.compose.runtime.DisposableEffect
3240
import androidx.compose.runtime.MutableState
41+
import androidx.compose.runtime.getValue
3342
import androidx.compose.runtime.mutableStateOf
3443
import androidx.compose.runtime.remember
44+
import androidx.compose.runtime.setValue
3545
import androidx.compose.ui.Modifier
46+
import androidx.compose.ui.draw.drawBehind
47+
import androidx.compose.ui.geometry.CornerRadius
48+
import androidx.compose.ui.geometry.Rect
49+
import androidx.compose.ui.geometry.RoundRect
50+
import androidx.compose.ui.graphics.Brush
51+
import androidx.compose.ui.graphics.Color
52+
import androidx.compose.ui.graphics.Path
3653
import androidx.compose.ui.layout.Layout
3754
import androidx.compose.ui.layout.Measurable
3855
import androidx.compose.ui.layout.MeasurePolicy
3956
import androidx.compose.ui.layout.MeasureResult
4057
import androidx.compose.ui.layout.MeasureScope
4158
import androidx.compose.ui.layout.layoutId
59+
import androidx.compose.ui.platform.LocalContext
4260
import androidx.compose.ui.platform.LocalLayoutDirection
4361
import androidx.compose.ui.unit.Constraints
4462
import androidx.compose.ui.unit.Dp
@@ -77,21 +95,35 @@ fun SliderTrack(
7795
Layout(
7896
measurePolicy = measurePolicy,
7997
content = {
80-
SliderDefaults.Track(
98+
99+
val gradientEnabled = rememberVolumeGradientEnabled()
100+
101+
val gStart = MaterialTheme.colorScheme.primary
102+
val gEnd = MaterialTheme.colorScheme.secondary
103+
104+
val activeBrush =
105+
if (gradientEnabled && gStart != Color(0) && gEnd != Color(0)) {
106+
listOf(gStart, gEnd)
107+
} else {
108+
null
109+
}
110+
111+
GradientSliderTrack(
81112
sliderState = sliderState,
113+
isEnabled = isEnabled,
82114
colors = colors,
83-
enabled = isEnabled,
84115
trackCornerSize = trackCornerSize,
85116
trackInsideCornerSize = trackInsideCornerSize,
86-
drawStopIndicator = null,
87117
thumbTrackGapSize = thumbTrackGapSize,
88-
drawTick = { _, _ -> },
118+
isVertical = isVertical,
119+
gradientColors = activeBrush,
89120
modifier =
90-
Modifier.then(
121+
Modifier
122+
.then(
91123
if (isVertical) {
92-
Modifier.width(trackSize)
124+
Modifier.width(trackSize).fillMaxHeight()
93125
} else {
94-
Modifier.height(trackSize)
126+
Modifier.height(trackSize).fillMaxWidth()
95127
}
96128
)
97129
.layoutId(Contents.Track),
@@ -130,6 +162,182 @@ fun SliderTrack(
130162
)
131163
}
132164

165+
private data class TrackGradient(
166+
val brush: Brush,
167+
)
168+
169+
@Composable
170+
private fun rememberVolumeGradientEnabled(): Boolean {
171+
val context = LocalContext.current
172+
val contentResolver = context.contentResolver
173+
174+
fun readEnabled(): Boolean {
175+
return try {
176+
Settings.System.getIntForUser(
177+
contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0,
178+
UserHandle.USER_CURRENT
179+
) != 0
180+
} catch (_: Throwable) {
181+
false
182+
}
183+
}
184+
185+
var enabled by remember { mutableStateOf(readEnabled()) }
186+
187+
DisposableEffect(contentResolver) {
188+
val observer = object : ContentObserver(null) {
189+
override fun onChange(selfChange: Boolean) {
190+
enabled = readEnabled()
191+
}
192+
}
193+
194+
contentResolver.registerContentObserver(
195+
Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT),
196+
false, observer, UserHandle.USER_ALL
197+
)
198+
199+
onDispose {
200+
contentResolver.unregisterContentObserver(observer)
201+
}
202+
}
203+
204+
return enabled
205+
}
206+
207+
@Composable
208+
@OptIn(ExperimentalMaterial3Api::class)
209+
private fun GradientSliderTrack(
210+
sliderState: SliderState,
211+
isEnabled: Boolean,
212+
colors: SliderColors,
213+
trackCornerSize: Dp,
214+
trackInsideCornerSize: Dp,
215+
thumbTrackGapSize: Dp,
216+
isVertical: Boolean,
217+
gradientColors: List<Color>?,
218+
modifier: Modifier = Modifier,
219+
) {
220+
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
221+
222+
val inactiveColor =
223+
if (isEnabled) colors.inactiveTrackColor else colors.disabledInactiveTrackColor
224+
val activeSolidColor =
225+
if (isEnabled) colors.activeTrackColor else colors.disabledActiveTrackColor
226+
227+
Box(
228+
modifier =
229+
modifier.drawBehind {
230+
val w = size.width
231+
val h = size.height
232+
if (w <= 0f || h <= 0f) return@drawBehind
233+
234+
val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f)
235+
val outerR = trackCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f)
236+
val innerR = trackInsideCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f)
237+
val halfGap = (thumbTrackGapSize.toPx() / 2f).coerceAtLeast(0f)
238+
239+
drawRoundRect(
240+
color = inactiveColor,
241+
size = size,
242+
cornerRadius = CornerRadius(outerR, outerR)
243+
)
244+
245+
if (!isVertical) {
246+
val splitX = if (isRtl) w * (1f - frac) else w * frac
247+
248+
val gapEff =
249+
if (isRtl) {
250+
minOf(halfGap, splitX)
251+
} else {
252+
minOf(halfGap, w - splitX)
253+
}
254+
255+
val activeStart = if (isRtl) splitX + gapEff else 0f
256+
val activeEnd = if (isRtl) w else splitX - gapEff
257+
258+
if (activeEnd <= activeStart) return@drawBehind
259+
260+
val eps = 0.5f
261+
262+
val hitsStart = activeStart <= eps
263+
val hitsEnd = activeEnd >= (w - eps)
264+
265+
val leftR =
266+
if (isRtl) {
267+
if (hitsStart) outerR else innerR // rounded only at max (when it reaches start)
268+
} else {
269+
outerR // far end always rounded
270+
}
271+
272+
val rightR =
273+
if (isRtl) {
274+
outerR // far end always rounded
275+
} else {
276+
if (hitsEnd) outerR else innerR // rounded only at max (when it reaches end)
277+
}
278+
279+
val path = Path().apply {
280+
addRoundRect(
281+
RoundRect(
282+
rect = Rect(activeStart, 0f, activeEnd, h),
283+
topLeft = CornerRadius(leftR, leftR),
284+
bottomLeft = CornerRadius(leftR, leftR),
285+
topRight = CornerRadius(rightR, rightR),
286+
bottomRight = CornerRadius(rightR, rightR),
287+
)
288+
)
289+
}
290+
291+
val brush =
292+
if (gradientColors != null) Brush.horizontalGradient(gradientColors)
293+
else Brush.linearGradient(listOf(activeSolidColor, activeSolidColor))
294+
295+
drawPath(path = path, brush = brush)
296+
} else {
297+
val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f)
298+
val splitY = h * (1f - frac)
299+
300+
val gapEff = minOf(halfGap, splitY)
301+
302+
val activeTop = splitY + gapEff
303+
val activeBottom = h
304+
if (activeBottom <= activeTop) return@drawBehind
305+
306+
val eps = 0.5f
307+
val hitsTop = activeTop <= eps
308+
309+
val topR = if (hitsTop) outerR else innerR
310+
val bottomR = outerR
311+
312+
val path = Path().apply {
313+
addRoundRect(
314+
RoundRect(
315+
rect = Rect(0f, activeTop, w, activeBottom),
316+
topLeft = CornerRadius(topR, topR),
317+
bottomLeft = CornerRadius(bottomR, bottomR),
318+
topRight = CornerRadius(topR, topR),
319+
bottomRight = CornerRadius(bottomR, bottomR),
320+
)
321+
)
322+
}
323+
324+
val brush =
325+
if (gradientColors != null) {
326+
Brush.verticalGradient(
327+
colors = gradientColors,
328+
startY = activeBottom,
329+
endY = activeTop
330+
)
331+
} else {
332+
Brush.linearGradient(listOf(activeSolidColor, activeSolidColor))
333+
}
334+
335+
drawPath(path = path, brush = brush)
336+
}
337+
}
338+
)
339+
}
340+
133341
@Composable
134342
private fun TrackIcon(
135343
icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?,

0 commit comments

Comments
 (0)