diff --git a/.gitignore b/.gitignore
index af15f32..d532680 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,13 +8,10 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
-<<<<<<< HEAD
/.idea/misc.xml
/.idea/deploymentTargetDropDown.xml
/.idea/deploymentTargetSelector.xml
-=======
/.idea/*
->>>>>>> origin/main
.DS_Store
/build
/captures
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 8f00030..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,5 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# GitHub Copilot persisted chat sessions
-/copilot/chatSessions
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b589d56..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 8001ed1..0000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
deleted file mode 100644
index 82d64ee..0000000
--- a/.idea/deploymentTargetSelector.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 5cff6b4..0000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 44ca2d9..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index e805548..0000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
deleted file mode 100644
index f8051a6..0000000
--- a/.idea/migrations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 8978d23..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/carousel/.gitignore b/carousel/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/carousel/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/carousel/build.gradle.kts b/carousel/build.gradle.kts
new file mode 100644
index 0000000..cf4b73e
--- /dev/null
+++ b/carousel/build.gradle.kts
@@ -0,0 +1,55 @@
+import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.mosaic.library)
+ alias(libs.plugins.gradle.maven.publish)
+}
+
+android {
+ namespace = "io.monstarlab.mosaic.carousel"
+ defaultConfig {
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
+}
+
+mavenPublishing {
+ configure(AndroidSingleVariantLibrary("release"))
+ publishToMavenCentral(SonatypeHost.S01)
+ signAllPublications()
+}
+
+kotlin {
+ jvmToolchain(17)
+ explicitApi()
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(platform(libs.compose.bom))
+ implementation(libs.bundles.compose.core)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/carousel/consumer-rules.pro b/carousel/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/carousel/proguard-rules.pro b/carousel/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/carousel/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/carousel/src/androidTest/java/io/monstarlab/mosaic/carousel/ExampleInstrumentedTest.kt b/carousel/src/androidTest/java/io/monstarlab/mosaic/carousel/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..3831849
--- /dev/null
+++ b/carousel/src/androidTest/java/io/monstarlab/mosaic/carousel/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package io.monstarlab.mosaic.carousel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("io.monstarlab.mosaic.carousel.test", appContext.packageName)
+ }
+}
diff --git a/carousel/src/main/AndroidManifest.xml b/carousel/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/carousel/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/carousel/src/main/java/io/monstarlab/mosaic/carousel/CarouselDefaults.kt b/carousel/src/main/java/io/monstarlab/mosaic/carousel/CarouselDefaults.kt
new file mode 100644
index 0000000..b65700a
--- /dev/null
+++ b/carousel/src/main/java/io/monstarlab/mosaic/carousel/CarouselDefaults.kt
@@ -0,0 +1,10 @@
+package io.monstarlab.mosaic.carousel
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+
+internal object CarouselDefaults {
+ internal val itemDuration: Duration = 2.seconds
+ internal val refreshRate: Duration = 60.milliseconds
+}
diff --git a/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarousel.kt b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarousel.kt
new file mode 100644
index 0000000..09915eb
--- /dev/null
+++ b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarousel.kt
@@ -0,0 +1,120 @@
+package io.monstarlab.mosaic.carousel
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedContentScope
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+
+/**
+ * A Carousel that animates between different content at a given interval. The user
+ * can also manually navigate between the different items by clicking the left and right sides
+ * of the component
+ *
+ * By Holding the component down, the carousel will stop and will resume when the user releases.
+ *
+ * @param state the state of the carousel.
+ * @param modifier the modifier to apply to this layout.
+ * @param label accessibility label to use for the carousel.
+ * @param transitionSpec the transition to use when changing the content of the carousel.
+ * @param content the content to display in the carousel.
+ */
+@Composable
+public fun MosaicCarousel(
+ state: MosaicCarouselState,
+ modifier: Modifier = Modifier,
+ label: String = "",
+ transitionSpec: MosaicCarouselTransition = defaultCarouselTransition(),
+ content: @Composable AnimatedContentScope.(Int) -> Unit,
+) {
+ LaunchedEffect(state) {
+ state.start()
+ }
+
+ Box(modifier = modifier) {
+ AnimatedContent(
+ targetState = state.currentItem,
+ label = label,
+ transitionSpec = transitionSpec,
+ content = content,
+ )
+
+ Row(
+ horizontalArrangement = Arrangement.Start,
+ modifier = Modifier,
+ ) {
+ ClickableBox(
+ onClick = state::moveToPrevious,
+ onHold = state::stop,
+ onRelease = state::start,
+ )
+
+ ClickableBox(
+ onClick = state::moveToNext,
+ onHold = state::stop,
+ onRelease = state::start,
+ )
+ }
+ }
+}
+
+@Composable
+private fun RowScope.ClickableBox(
+ onClick: () -> Unit = {},
+ onHold: () -> Unit = {},
+ onRelease: () -> Unit = {},
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(0.5f)
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onTap = { onClick() },
+ onLongPress = { },
+ onPress = {
+ onHold()
+ awaitRelease()
+ onRelease()
+ },
+ )
+ },
+ )
+}
+
+@Preview
+@Composable
+private fun PreviewCarousel() {
+ val state = rememberCarouselState(itemsCount = 5)
+ Box(modifier = Modifier.fillMaxSize()) {
+ MosaicCarousel(state, Modifier.align(Alignment.Center)) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .align(Alignment.Center)
+ .background(if (it % 2 == 0) Color.Yellow else Color.Green),
+ ) {
+ BasicText(
+ text = "$it",
+ modifier = Modifier.align(Alignment.Center),
+ style = TextStyle.Default.copy(fontSize = 48.sp),
+ )
+ }
+ }
+ }
+}
diff --git a/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselProgressBar.kt b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselProgressBar.kt
new file mode 100644
index 0000000..3f8f538
--- /dev/null
+++ b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselProgressBar.kt
@@ -0,0 +1,113 @@
+package io.monstarlab.mosaic.carousel
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+/**
+ * A progress indicator that shows the progress of a carousel and the progress of the current item.
+ * @param state The state of the carousel.
+ * @param modifier The modifier to apply to this layout.
+ * @param segmentActiveColor The color of the active segment.
+ * @param segmentInactiveColor The color of the inactive segments.
+ * @param segmentCornerRadius The corner radius of the segments.
+ */
+@Composable
+public fun MosaicCarouselProgressBar(
+ state: MosaicCarouselState,
+ modifier: Modifier = Modifier,
+ segmentActiveColor: Color = Color.White,
+ segmentInactiveColor: Color = Color.White.copy(alpha = 0.5f),
+ segmentCornerRadius: CornerRadius = CornerRadius(16f),
+) {
+ MosaicCarouselProgressBar(
+ itemsTotal = state.itemsCount,
+ currentItem = state.currentItem,
+ currentItemProgress = state.currentItemProgress,
+ modifier = modifier,
+ segmentActiveColor = segmentActiveColor,
+ segmentInactiveColor = segmentInactiveColor,
+ segmentCornerRadius = segmentCornerRadius,
+ )
+}
+
+@Composable
+internal fun MosaicCarouselProgressBar(
+ itemsTotal: Int,
+ currentItem: Int,
+ currentItemProgress: Float,
+ modifier: Modifier = Modifier,
+ segmentActiveColor: Color = Color.White,
+ segmentInactiveColor: Color = Color.White.copy(alpha = 0.5f),
+ segmentCornerRadius: CornerRadius = CornerRadius(0f),
+) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ modifier = modifier.height(1.dp),
+ ) {
+ repeat(itemsTotal) { index ->
+ val progress = when {
+ index < currentItem -> 1f
+ index == currentItem -> currentItemProgress
+ else -> 0f
+ }
+ ProgressSegment(
+ progress = progress,
+ modifier = Modifier.weight(1f / itemsTotal),
+ color = segmentActiveColor,
+ inactiveColor = segmentInactiveColor,
+ cornerRadius = segmentCornerRadius,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProgressSegment(
+ progress: Float,
+ modifier: Modifier = Modifier,
+ color: Color,
+ inactiveColor: Color,
+ cornerRadius: CornerRadius,
+) {
+ Canvas(
+ modifier = modifier.fillMaxSize(),
+
+ ) {
+ val activeWidth = size.width * progress
+ drawRoundRect(
+ color = color,
+ topLeft = Offset.Zero,
+ size = Size(activeWidth, size.height),
+ cornerRadius = cornerRadius,
+ )
+ drawRoundRect(
+ color = inactiveColor,
+ topLeft = Offset(x = activeWidth, y = 0f),
+ size = Size(size.width - activeWidth, size.height),
+ cornerRadius = cornerRadius,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewCarouselProgressIndicator() {
+ MosaicCarouselProgressBar(
+ itemsTotal = 3,
+ currentItem = 2,
+ currentItemProgress = 0.5f,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
diff --git a/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselState.kt b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselState.kt
new file mode 100644
index 0000000..14bd2b4
--- /dev/null
+++ b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselState.kt
@@ -0,0 +1,128 @@
+package io.monstarlab.mosaic.carousel
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import kotlin.time.Duration
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * Represents the state of a carousel.
+ * @param itemsCount The total number of items in the carousel.
+ * @param stayDuration The duration each item should stay on screen.
+ * @param scope The coroutine scope to use for the carousel.
+ */
+public class MosaicCarouselState(
+ public val itemsCount: Int,
+ private val stayDuration: Duration,
+ private val scope: CoroutineScope,
+) {
+
+ private var carouselProgress: Float by mutableFloatStateOf(0f)
+
+ private val itemProgressIncrement: Float by lazy {
+ val refreshRate = CarouselDefaults.refreshRate.inWholeMilliseconds.toFloat()
+ val stayDuration = stayDuration.inWholeMilliseconds.toFloat()
+ refreshRate / stayDuration
+ }
+
+ private var job: Job? = null
+
+ /**
+ * The current item index.
+ */
+ public val currentItem: Int
+ get() {
+ return carouselProgress.toInt()
+ }
+
+ /**
+ * The progress of the current item.
+ * This value is between 0 and 1.
+ */
+ public val currentItemProgress: Float
+ get() {
+ return (carouselProgress - currentItem).coerceIn(0f, 1f)
+ }
+
+ /**
+ * Stops the carousel and any ongoing animations.
+ */
+ public fun stop() {
+ job?.cancel()
+ }
+
+ /**
+ * Moves to the previous item and setting its progress to 0.
+ */
+ public fun moveToPrevious() {
+ updateProgress(currentItem - 1f)
+ }
+
+ /**
+ * Moves to the next item and setting its progress to 0.
+ */
+ public fun moveToNext() {
+ updateProgress(currentItem + 1f)
+ }
+
+ /**
+ * Moves to the specified item index.
+ * @param index The index of the item to move to.
+ */
+ public fun moveTo(index: Int) {
+ updateProgress(index.toFloat())
+ }
+
+ /**
+ * Starts the carousel.
+ * The carousel will automatically move to the next item at the specified interval.
+ * The interval is determined by the [stayDuration] property.
+ */
+ public fun start() {
+ job?.cancel()
+ job = scope.launch {
+ while (true) {
+ delay(CarouselDefaults.refreshRate)
+ updateProgress(carouselProgress + itemProgressIncrement)
+ println(currentItemProgress)
+ }
+ }
+ }
+
+ private fun updateProgress(progress: Float) {
+ carouselProgress = when {
+ progress < 0 -> 0f
+ progress > itemsCount -> 0f
+ else -> progress
+ }
+ }
+}
+
+/**
+ * Creates a [MosaicCarouselState] that will remember its state across compositions.
+ * @param itemsCount The total number of items in the carousel.
+ * @param stayDuration The duration each item should stay on screen. By default, it is 2 seconds.
+ * @return A [MosaicCarouselState] that will remember its state across compositions.
+ */
+@Composable
+public fun rememberCarouselState(
+ itemsCount: Int,
+ stayDuration: Duration = CarouselDefaults.itemDuration,
+): MosaicCarouselState {
+ val scope = rememberCoroutineScope()
+
+ return remember(itemsCount) {
+ MosaicCarouselState(
+ itemsCount = itemsCount,
+ stayDuration = stayDuration,
+ scope = scope,
+ )
+ }
+}
diff --git a/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselTransition.kt b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselTransition.kt
new file mode 100644
index 0000000..ac09344
--- /dev/null
+++ b/carousel/src/main/java/io/monstarlab/mosaic/carousel/MosaicCarouselTransition.kt
@@ -0,0 +1,19 @@
+package io.monstarlab.mosaic.carousel
+
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+
+/**
+ * A type alias for a transition that can be applied to the carousel content.
+ */
+public typealias MosaicCarouselTransition =
+ AnimatedContentTransitionScope.() -> ContentTransform
+
+internal fun defaultCarouselTransition(): MosaicCarouselTransition {
+ return {
+ fadeIn() togetherWith fadeOut()
+ }
+}
diff --git a/carousel/src/test/java/io/monstarlab/mosaic/carousel/ExampleUnitTest.kt b/carousel/src/test/java/io/monstarlab/mosaic/carousel/ExampleUnitTest.kt
new file mode 100644
index 0000000..8f78a98
--- /dev/null
+++ b/carousel/src/test/java/io/monstarlab/mosaic/carousel/ExampleUnitTest.kt
@@ -0,0 +1,16 @@
+package io.monstarlab.mosaic.carousel
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts
index 9dcde37..ee3fd9c 100644
--- a/demo/build.gradle.kts
+++ b/demo/build.gradle.kts
@@ -44,6 +44,7 @@ kotlin {
dependencies {
implementation(project(":slider"))
+ implementation(project(":carousel"))
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 e1532f9..45771b9 100644
--- a/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt
+++ b/demo/src/main/java/io/monstarlab/mosaic/MainActivity.kt
@@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
+import io.monstarlab.mosaic.demos.CarouselDemo
import io.monstarlab.mosaic.demos.SliderDemo
import io.monstarlab.mosaic.ui.theme.MosaicTheme
@@ -31,6 +32,7 @@ class MainActivity : ComponentActivity() {
navController.navigate(Routes.SliderDemo.value)
},
onCarouselClick = {
+ navController.navigate(Routes.CarouselDemo.value)
},
)
}
@@ -38,6 +40,10 @@ class MainActivity : ComponentActivity() {
composable(Routes.SliderDemo.value) {
SliderDemo()
}
+
+ composable(Routes.CarouselDemo.value) {
+ CarouselDemo()
+ }
}
}
}
diff --git a/demo/src/main/java/io/monstarlab/mosaic/Routes.kt b/demo/src/main/java/io/monstarlab/mosaic/Routes.kt
index ba88619..137a46b 100644
--- a/demo/src/main/java/io/monstarlab/mosaic/Routes.kt
+++ b/demo/src/main/java/io/monstarlab/mosaic/Routes.kt
@@ -3,4 +3,5 @@ package io.monstarlab.mosaic
enum class Routes(val value: String) {
Home("home"),
SliderDemo("slider-demo"),
+ CarouselDemo("carousel-demo"),
}
diff --git a/demo/src/main/java/io/monstarlab/mosaic/demos/CarouselDemo.kt b/demo/src/main/java/io/monstarlab/mosaic/demos/CarouselDemo.kt
new file mode 100644
index 0000000..d1481f0
--- /dev/null
+++ b/demo/src/main/java/io/monstarlab/mosaic/demos/CarouselDemo.kt
@@ -0,0 +1,124 @@
+package io.monstarlab.mosaic.demos
+
+import android.graphics.drawable.Icon
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Email
+import androidx.compose.material.icons.rounded.Favorite
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.monstarlab.mosaic.carousel.MosaicCarousel
+import io.monstarlab.mosaic.carousel.MosaicCarouselProgressBar
+import io.monstarlab.mosaic.carousel.rememberCarouselState
+import io.monstarlab.mosaic.ui.theme.MosaicTheme
+import kotlin.time.Duration.Companion.seconds
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CarouselDemo() = Scaffold(
+ topBar = {
+ TopAppBar(title = { Text(text = "Mosaic Carousel") })
+ },
+) {
+ val state = rememberCarouselState(
+ itemsCount = 3,
+ stayDuration = 5.seconds,
+ )
+
+ Box(modifier = Modifier.padding(it)) {
+ MosaicCarousel(
+ state = state,
+ transitionSpec = {
+ slideInHorizontally(
+ initialOffsetX = { it },
+ ) togetherWith slideOutHorizontally(
+ targetOffsetX = { -it },
+ )
+ },
+
+ ) {
+ val icon = when (it) {
+ 0 -> Icons.Rounded.Settings
+ 1 -> Icons.Rounded.Favorite
+ else -> Icons.Rounded.Email
+ }
+
+ val text = when (it) {
+ 0 ->
+ "Est deserunt cillum ipsum aute reprehenderit labore Lorem enim tempor enim " +
+ "incididunt quis dolore anim fugiat."
+ 1 ->
+ "Nulla commodo voluptate aliquip. Exercitation " +
+ "laboris laborum laborum laborum."
+ else ->
+ "Officia ea voluptate nostrud quis. " +
+ "Adipisicing Officia ea voluptate nostrud quis. Adipisicing."
+ }
+
+ CarouselItem(text = text, icon = icon)
+ }
+
+ MosaicCarouselProgressBar(
+ state = state,
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .height(2.dp),
+ )
+ }
+}
+
+@Composable
+fun CarouselItem(text: String, icon: ImageVector, modifier: Modifier = Modifier) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .size(200.dp)
+ .align(Alignment.CenterHorizontally)
+ .padding(16.dp),
+ )
+ Text(
+ text = text,
+ style = MaterialTheme.typography.headlineSmall,
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewCarouselDemo() {
+ MosaicTheme {
+ CarouselDemo()
+ }
+}
diff --git a/demo/src/main/java/io/monstarlab/mosaic/ui/theme/Theme.kt b/demo/src/main/java/io/monstarlab/mosaic/ui/theme/Theme.kt
index f305019..2df59a4 100644
--- a/demo/src/main/java/io/monstarlab/mosaic/ui/theme/Theme.kt
+++ b/demo/src/main/java/io/monstarlab/mosaic/ui/theme/Theme.kt
@@ -1,6 +1,5 @@
package io.monstarlab.mosaic.ui.theme
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
@@ -15,12 +14,7 @@ private val DarkColorScheme = darkColorScheme(
)
@Composable
-fun MosaicTheme(
- darkTheme: Boolean = isSystemInDarkTheme(),
- // Dynamic color is available on Android 12+
- dynamicColor: Boolean = true,
- content: @Composable () -> Unit,
-) {
+fun MosaicTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = DarkColorScheme,
typography = Typography,
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8b4c8fc..83bdbc4 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -24,3 +24,4 @@ rootProject.name = "Mosaic"
include(":demo")
include(":lib")
include(":slider")
+include(":carousel")
diff --git a/slider/src/main/java/io/monstarlab/mosaic/slider/MosaicSliderState.kt b/slider/src/main/java/io/monstarlab/mosaic/slider/MosaicSliderState.kt
index 421a00e..fe6c0a3 100644
--- a/slider/src/main/java/io/monstarlab/mosaic/slider/MosaicSliderState.kt
+++ b/slider/src/main/java/io/monstarlab/mosaic/slider/MosaicSliderState.kt
@@ -55,7 +55,8 @@ public class MosaicSliderState(
/**
* Current value of the slider
* If value of the slider is out of the [range] it will be coerced into it
- * If the value of the slider is inside the [disabledRange] It will be coerced int closes available range that is not disabled
+ * If the value of the slider is inside the [disabledRange]
+ * It will be coerced int closes available range that is not disabled
*
*/
public var value: Float