@@ -4,6 +4,8 @@ import android.content.Context
44import android.widget.FrameLayout
55import androidx.compose.foundation.gestures.awaitEachGesture
66import androidx.compose.foundation.gestures.awaitFirstDown
7+ import androidx.compose.foundation.layout.Box
8+ import androidx.compose.foundation.layout.fillMaxSize
79import androidx.compose.foundation.layout.fillMaxWidth
810import androidx.compose.material3.RangeSlider
911import androidx.compose.material3.Slider
@@ -13,11 +15,16 @@ import androidx.compose.runtime.mutableDoubleStateOf
1315import androidx.compose.runtime.mutableFloatStateOf
1416import androidx.compose.runtime.mutableStateOf
1517import androidx.compose.runtime.setValue
18+ import androidx.compose.ui.Alignment
1619import androidx.compose.ui.Modifier
20+ import androidx.compose.ui.graphics.TransformOrigin
21+ import androidx.compose.ui.graphics.graphicsLayer
1722import androidx.compose.ui.input.pointer.PointerEventPass
1823import androidx.compose.ui.input.pointer.pointerInput
24+ import androidx.compose.ui.layout.layout
1925import androidx.compose.ui.platform.ComposeView
2026import androidx.compose.ui.platform.ViewCompositionStrategy
27+ import androidx.compose.ui.unit.Constraints
2128import kotlin.math.abs
2229import kotlin.math.floor
2330import kotlin.math.pow
@@ -70,6 +77,16 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
7077 private var lowerLimitValue by mutableFloatStateOf(DEFAULT_LOWER_LIMIT )
7178 private var upperLimitValue by mutableFloatStateOf(DEFAULT_UPPER_LIMIT )
7279
80+ /* *
81+ * Whether the slider is laid out along the view's height rather than its width,
82+ * with the larger values at the top - see [sliderAxis].
83+ *
84+ * Kept as the one thing the orientation actually decides, rather than as the
85+ * string it arrives as, so that anything other than "vertical" reads as the
86+ * horizontal slider it defaults to.
87+ */
88+ private var isVertical by mutableStateOf(DEFAULT_VERTICAL )
89+
7390 /* *
7491 * Whether the user has hold of the slider. It lasts from the touch going down
7592 * to the last finger coming off, so it covers a drag that never moved a thumb
@@ -167,6 +184,10 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
167184 upperLimitValue = value.toFloat()
168185 }
169186
187+ fun setOrientation (value : String? ) {
188+ isVertical = value == ORIENTATION_VERTICAL
189+ }
190+
170191 override fun onMeasure (widthMeasureSpec : Int , heightMeasureSpec : Int ) {
171192 if (! isAttachedToWindow) {
172193 // Measuring the child here would make Compose look for a window recomposer
@@ -236,13 +257,60 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
236257
237258 @Composable
238259 private fun SliderContent () {
239- if (selectsRange) {
240- RangedSliderContent ()
241- } else {
242- SingleSliderContent ()
260+ // The slider is only ever as thick as the control itself, and a vertical one
261+ // is far narrower than the view is wide, so it sits in the middle of whatever
262+ // room JS has given it rather than against one edge.
263+ Box (modifier = Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
264+ if (selectsRange) {
265+ RangedSliderContent ()
266+ } else {
267+ SingleSliderContent ()
268+ }
243269 }
244270 }
245271
272+ /* *
273+ * Lays the slider out along the axis the orientation asks for, stretched across
274+ * the view.
275+ *
276+ * Material 3 has no vertical slider, so a vertical one here is its horizontal
277+ * slider turned a quarter turn anticlockwise, which puts the larger values at
278+ * the top. Everything the rotation encloses is the same control as ever - the
279+ * range and the step, both thumbs of a ranged slider, the gestures [slidingGestures]
280+ * listens to and the accessibility the platform's own slider comes with - it is
281+ * only measured and drawn the other way round.
282+ */
283+ private fun Modifier.sliderAxis (): Modifier =
284+ if (! isVertical) {
285+ fillMaxWidth()
286+ } else {
287+ graphicsLayer {
288+ rotationZ = - 90f
289+ // Turned about the top left corner, so that the slider ends up in the
290+ // box the layout below reports rather than somewhere off the side of it.
291+ transformOrigin = TransformOrigin (0f , 0f )
292+ }
293+ .layout { measurable, constraints ->
294+ // Inside the rotation the slider is still a horizontal control, so it is
295+ // offered the height it has to span as its width, and the other way round.
296+ val placeable =
297+ measurable.measure(
298+ Constraints (
299+ minWidth = constraints.minHeight,
300+ maxWidth = constraints.maxHeight,
301+ minHeight = constraints.minWidth,
302+ maxHeight = constraints.maxWidth,
303+ )
304+ )
305+
306+ // Turning anticlockwise about the corner carries the slider straight up
307+ // out of the view, so it is laid out one width to the left of it to come
308+ // back down into place - taking up a box with its two sides swapped.
309+ layout(placeable.height, placeable.width) { placeable.place(- placeable.width, 0 ) }
310+ }
311+ .fillMaxWidth()
312+ }
313+
246314 @Composable
247315 private fun SingleSliderContent () {
248316 val range = valueRange()
@@ -252,7 +320,7 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
252320 value = sliderValue.coerceIn(range.start, range.endInclusive),
253321 valueRange = range,
254322 onValueChange = { moved -> onSliderValueChange(moved, limits) },
255- modifier = Modifier .fillMaxWidth ().slidingGestures(),
323+ modifier = Modifier .sliderAxis ().slidingGestures(),
256324 )
257325 }
258326
@@ -343,7 +411,7 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
343411 value = selected,
344412 valueRange = range,
345413 onValueChange = { moved -> onSelectedRangeChange(from = selected, to = moved, limits = limits) },
346- modifier = Modifier .fillMaxWidth ().slidingGestures(),
414+ modifier = Modifier .sliderAxis ().slidingGestures(),
347415 )
348416 }
349417
@@ -397,6 +465,10 @@ class RNCSliderView(context: Context) : FrameLayout(context) {
397465 const val DEFAULT_STEP = 0.0
398466 const val DEFAULT_LOWER_LIMIT = 0f
399467 const val DEFAULT_UPPER_LIMIT = 1f
468+ const val DEFAULT_VERTICAL = false
469+
470+ /* * The one orientation that is not the default. */
471+ private const val ORIENTATION_VERTICAL = " vertical"
400472 }
401473}
402474
0 commit comments