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