From a96014d1e279c6975735a3df65eac4550e6206a4 Mon Sep 17 00:00:00 2001 From: Roman Levinzon Date: Fri, 23 Feb 2024 12:31:30 +0100 Subject: [PATCH] feat(Slider): add basic SliderLayout to position slider elements --- demo/build.gradle.kts | 2 +- .../java/io/monstarlab/mosaic/MainActivity.kt | 9 +-- .../monstarlab/mosaic/features/SliderDemo.kt | 32 ++++++++ .../io/monstarlab/mosaic/slider/Slider.kt | 62 +++++++++++++++ .../mosaic/slider/SliderDefaults.kt | 1 + .../monstarlab/mosaic/slider/SliderLayout.kt | 79 +++++++++++++++++++ .../monstarlab/mosaic/slider/SliderTrack.kt | 57 ++++++------- 7 files changed, 208 insertions(+), 34 deletions(-) create mode 100644 demo/src/main/java/io/monstarlab/mosaic/features/SliderDemo.kt create mode 100644 slider/src/main/java/io/monstarlab/mosaic/slider/Slider.kt create mode 100644 slider/src/main/java/io/monstarlab/mosaic/slider/SliderLayout.kt diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 2c88c7c..f8d0f23 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -43,7 +43,7 @@ kotlin { } dependencies { - + implementation(project(":slider")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt b/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt index af2f690..5170e19 100644 --- a/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt +++ b/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt @@ -7,10 +7,12 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import io.monstarlab.mosaic.features.SliderDemo import io.monstarlab.mosaic.ui.theme.MosaicTheme class MainActivity : ComponentActivity() { @@ -19,12 +21,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { MosaicTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding), - ) - } + SliderDemo() } } } diff --git a/demo/src/main/java/io/monstarlab/mosaic/features/SliderDemo.kt b/demo/src/main/java/io/monstarlab/mosaic/features/SliderDemo.kt new file mode 100644 index 0000000..cdae377 --- /dev/null +++ b/demo/src/main/java/io/monstarlab/mosaic/features/SliderDemo.kt @@ -0,0 +1,32 @@ +package io.monstarlab.mosaic.features + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.monstarlab.mosaic.slider.Slider +import io.monstarlab.mosaic.slider.SliderColors + +@Composable +fun SliderDemo() = Scaffold(modifier = Modifier) { + Column(modifier = Modifier.padding(it).padding(16.dp)) { + Slider( + value = 0.5f, + onValueChange = {}, + colors = SliderColors(Color.Red), + modifier = Modifier.clip(RoundedCornerShape(2.dp)) + ) + } +} + +@Preview +@Composable +private fun PreviewSliderDemo() { + SliderDemo() +} \ No newline at end of file diff --git a/slider/src/main/java/io/monstarlab/mosaic/slider/Slider.kt b/slider/src/main/java/io/monstarlab/mosaic/slider/Slider.kt new file mode 100644 index 0000000..68f8cc8 --- /dev/null +++ b/slider/src/main/java/io/monstarlab/mosaic/slider/Slider.kt @@ -0,0 +1,62 @@ +package io.monstarlab.mosaic.slider + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview + +@Composable +public fun Slider( + value: Float, + onValueChange: (Float) -> Unit, + colors: SliderColors, + modifier: Modifier = Modifier, + thumb: @Composable () -> Unit = { DefaultSliderThumb(colors = colors) } +) { + + SliderLayout( + progress = value, + thumb = thumb, + track = { + SliderTrack( + modifier = modifier, + progress = value, + colors = colors, + ) + } + ) +} + +@Composable +internal fun DefaultSliderThumb(colors: SliderColors) { + Box( + modifier = Modifier + .size(SliderDefaults.ThumbSize) + .background( + color = colors.active, + shape = CircleShape + ) + ) +} + +@Preview +@Composable +private fun PreviewSlider() { + Slider( + value = 0.5f, + onValueChange = {}, + colors = SliderColors(Color.Yellow) + ) +} \ No newline at end of file diff --git a/slider/src/main/java/io/monstarlab/mosaic/slider/SliderDefaults.kt b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderDefaults.kt index 03e6591..694e32b 100644 --- a/slider/src/main/java/io/monstarlab/mosaic/slider/SliderDefaults.kt +++ b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderDefaults.kt @@ -4,4 +4,5 @@ import androidx.compose.ui.unit.dp internal object SliderDefaults { val TrackHeight = 4.dp + val ThumbSize = 16.dp } \ No newline at end of file diff --git a/slider/src/main/java/io/monstarlab/mosaic/slider/SliderLayout.kt b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderLayout.kt new file mode 100644 index 0000000..ff47548 --- /dev/null +++ b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderLayout.kt @@ -0,0 +1,79 @@ +package io.monstarlab.mosaic.slider + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlin.math.max +import kotlin.math.roundToInt + +@Composable +internal fun SliderLayout( + progress: Float, + track: @Composable () -> Unit, + thumb: @Composable () -> Unit +) { + Layout( + content = { + Box(modifier = Modifier.layoutId(SliderLayoutElements.Track)) { + track() + } + Box(modifier = Modifier.layoutId(SliderLayoutElements.Thumb)) { + thumb() + } + }) { mesuarables, constraints -> + + val trackPlaceable = mesuarables + .first { it.layoutId == SliderLayoutElements.Track } + .measure(constraints) + + val thumbPlaceable = mesuarables + .first { it.layoutId == SliderLayoutElements.Thumb } + .measure(constraints) + + val sliderHeight = max(trackPlaceable.height, thumbPlaceable.height) + val sliderWidth = trackPlaceable.width + layout(sliderWidth, sliderHeight) { + val trackY = sliderHeight / 2 - trackPlaceable.height / 2 + val thumbY = sliderHeight / 2 - thumbPlaceable.height / 2 + val thumbX = (sliderWidth * progress).roundToInt() - thumbPlaceable.width / 2 + trackPlaceable.place(0, trackY) + thumbPlaceable.place(thumbX, thumbY) + } + } +} + + +internal enum class SliderLayoutElements { + Track, Thumb +} + + +@Preview +@Composable +private fun PreviewSliderLayout() { + SliderLayout( + progress = 0.5f, + track = { + Box( + modifier = Modifier + .background(Color.Yellow) + .fillMaxWidth() + .height(20.dp) + ) + }, + thumb = { + Box(modifier = Modifier + .background(Color.Blue, shape = CircleShape) + .size(50.dp)) + }) +} \ No newline at end of file diff --git a/slider/src/main/java/io/monstarlab/mosaic/slider/SliderTrack.kt b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderTrack.kt index c6cf7f6..e573361 100644 --- a/slider/src/main/java/io/monstarlab/mosaic/slider/SliderTrack.kt +++ b/slider/src/main/java/io/monstarlab/mosaic/slider/SliderTrack.kt @@ -1,10 +1,12 @@ package io.monstarlab.mosaic.slider import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color @@ -19,34 +21,35 @@ internal fun SliderTrack( ) { check(progress in 0f..1f) { "Invalid progress value should be between 0 and 1" } - Canvas( + Box( modifier = modifier .fillMaxWidth() - .height(SliderDefaults.TrackHeight) - ) { - val activeRectWidth = size.width * progress - drawRect( - color = colors.active, - topLeft = Offset.Zero, - size = Size(activeRectWidth, size.height) - ) + .heightIn(min = SliderDefaults.TrackHeight) + .drawBehind { + val activeRectWidth = size.width * progress + drawRect( + color = colors.active, + topLeft = Offset.Zero, + size = Size(activeRectWidth, size.height) + ) - drawRect( - color = colors.inactive, - topLeft = Offset(activeRectWidth, 0f), - size = Size(size.width - activeRectWidth, size.height) - ) + drawRect( + color = colors.inactive, + topLeft = Offset(activeRectWidth, 0f), + size = Size(size.width - activeRectWidth, size.height) + ) - if (!disabledRange.isEmpty()) { - val disabledStart = size.width * disabledRange.start - val disabledEnd = size.width * disabledRange.endInclusive - drawRect( - color = colors.disabled, - topLeft = Offset(size.width * disabledRange.start, 0f), - size = Size(disabledEnd - disabledStart, size.height) - ) - } - } + if (!disabledRange.isEmpty()) { + val disabledStart = size.width * disabledRange.start + val disabledEnd = size.width * disabledRange.endInclusive + drawRect( + color = colors.disabled, + topLeft = Offset(size.width * disabledRange.start, 0f), + size = Size(disabledEnd - disabledStart, size.height) + ) + } + } + ) } @@ -55,7 +58,7 @@ internal fun SliderTrack( private fun PreviewSliderTrack() { SliderTrack( progress = 0.5f, - colors = SliderColors(Color.Red), - disabledRange = 0.8f..1f + colors = SliderColors(Color.Yellow), + disabledRange = 0.8f..1f, ) } \ No newline at end of file