From 31b4de25dcfe883c3591790ebb1006c40a9c9add Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Wed, 2 Oct 2024 09:12:37 +0100 Subject: [PATCH 1/3] Added two snippets for showcasing how to do Masking and Clipping in Compose (#362) * Code snippet for Compose doc at https://developer.android.com/quick-guides/content/animate-text?hl=en (Animate text character-by-character). This commit slightly modifies (makes buildable in our repo) the existing code on the current DAC page. That code, in turn, was BNR's simplified version of Xoogler astamato's Medium article at https://medium.com/androiddevelopers/effective-state-management-for-textfield-in-compose-d6e5b070fbe5 * Code snippet for Compose doc at https://developer.android.com/quick-guides/content/animate-text?hl=en (Animate text character-by-character). This commit slightly modifies (makes buildable in our repo) the existing code on the current DAC page. That code, in turn, was BNR's simplified version of Xoogler astamato's Medium article at https://medium.com/androiddevelopers/effective-state-management-for-textfield-in-compose-d6e5b070fbe5 * Apply Spotless * Fix email input snippet * Migrate to use BasicSecureTextField. * Updated to use BasicSecureTextField. * Added clipping and faded edge examples * Apply Spotless * Clean up snippet * Clean up snippet --------- Co-authored-by: dmail Co-authored-by: thedmail Co-authored-by: riggaroo --- .../graphics/GraphicsModifiersSnippets.kt | 475 ++++++++++++------ 1 file changed, 331 insertions(+), 144 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt index ef00faecc..0493012ed 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/graphics/GraphicsModifiersSnippets.kt @@ -16,15 +16,26 @@ package com.example.compose.snippets.graphics +import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -42,21 +53,33 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.compose.snippets.R +import kotlin.random.Random /* * Copyright 2022 The Android Open Source Project @@ -296,171 +319,170 @@ fun ModifierGraphicsLayerAlpha() { @Preview @Composable fun ModifierGraphicsLayerCompositingStrategy() { - /* Commented out until compositing Strategy is rolled out to production - // [START android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] - - Image(painter = painterResource(id = R.drawable.dog), - contentDescription = "Dog", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(120.dp) - .aspectRatio(1f) - .background( - Brush.linearGradient( - listOf( - Color(0xFFC5E1A5), - Color(0xFF80DEEA) - ) - ) - ) - .padding(8.dp) - .graphicsLayer { - compositingStrategy = CompositingStrategy.Offscreen - } - .drawWithCache { - val path = Path() - path.addOval( - Rect( - topLeft = Offset.Zero, - bottomRight = Offset(size.width, size.height) - ) - ) - onDrawWithContent { - clipPath(path) { - // this draws the actual image - if you don't call drawContent, it wont - // render anything - this@onDrawWithContent.drawContent() - } - val dotSize = size.width / 8f - // Clip a white border for the content - drawCircle( - Color.Black, - radius = dotSize, - center = Offset( - x = size.width - dotSize, - y = size.height - dotSize - ), - blendMode = BlendMode.Clear - ) - // draw the red circle indication - drawCircle( - Color(0xFFEF5350), radius = dotSize * 0.8f, - center = Offset( - x = size.width - dotSize, - y = size.height - dotSize - ) - ) - } - - } - ) - // [END android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] - */ + // [START android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] + + Image( + painter = painterResource(id = R.drawable.dog), + contentDescription = "Dog", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(120.dp) + .aspectRatio(1f) + .background( + Brush.linearGradient( + listOf( + Color(0xFFC5E1A5), + Color(0xFF80DEEA) + ) + ) + ) + .padding(8.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithCache { + val path = Path() + path.addOval( + Rect( + topLeft = Offset.Zero, + bottomRight = Offset(size.width, size.height) + ) + ) + onDrawWithContent { + clipPath(path) { + // this draws the actual image - if you don't call drawContent, it wont + // render anything + this@onDrawWithContent.drawContent() + } + val dotSize = size.width / 8f + // Clip a white border for the content + drawCircle( + Color.Black, + radius = dotSize, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ), + blendMode = BlendMode.Clear + ) + // draw the red circle indication + drawCircle( + Color(0xFFEF5350), radius = dotSize * 0.8f, + center = Offset( + x = size.width - dotSize, + y = size.height - dotSize + ) + ) + } + } + ) + // [END android_compose_graphics_modifiers_graphicsLayer_compositing_strategy] } -/* Commented out until compositing Strategy is rolled out to production + @Preview // [START android_compose_graphics_modifier_compositing_strategy_differences] @Composable fun CompositingStrategyExamples() { - Column( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) - ) { - /** Does not clip content even with a graphics layer usage here. By default, graphicsLayer - does not allocate + rasterize content into a separate layer but instead is used - for isolation. That is draw invalidations made outside of this graphicsLayer will not - re-record the drawing instructions in this composable as they have not changed **/ - Canvas( - modifier = Modifier - .graphicsLayer() - .size(100.dp) // Note size of 100 dp here - .border(2.dp, color = Color.Blue) - ) { - // ... and drawing a size of 200 dp here outside the bounds - drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) - } - - Spacer(modifier = Modifier.size(300.dp)) - - /** Clips content as alpha usage here creates an offscreen buffer to rasterize content - into first then draws to the original destination **/ - Canvas( - modifier = Modifier - // force to an offscreen buffer - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .size(100.dp) // Note size of 100 dp here - .border(2.dp, color = Color.Blue) - ) { - /** ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the - content gets clipped **/ - drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) - } - } + Column( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.Center) + ) { + // Does not clip content even with a graphics layer usage here. By default, graphicsLayer + // does not allocate + rasterize content into a separate layer but instead is used + // for isolation. That is draw invalidations made outside of this graphicsLayer will not + // re-record the drawing instructions in this composable as they have not changed + Canvas( + modifier = Modifier + .graphicsLayer() + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + // ... and drawing a size of 200 dp here outside the bounds + drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + + Spacer(modifier = Modifier.size(300.dp)) + + /* Clips content as alpha usage here creates an offscreen buffer to rasterize content + into first then draws to the original destination */ + Canvas( + modifier = Modifier + // force to an offscreen buffer + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .size(100.dp) // Note size of 100 dp here + .border(2.dp, color = Color.Blue) + ) { + /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the + content gets clipped */ + drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) + } + } } // [END android_compose_graphics_modifier_compositing_strategy_differences] - */ -/* Commented out until compositing Strategy is rolled out to production // [START android_compose_graphics_modifier_compositing_strategy_modulate_alpha] @Preview @Composable -fun CompositingStratgey_ModulateAlpha() { - Column( - modifier = Modifier - .fillMaxSize() - .padding(32.dp) - ) { - // Base drawing, no alpha applied - Canvas( - modifier = Modifier.size(200.dp) - ) { - drawSquares() - } - - Spacer(modifier = Modifier.size(36.dp)) - - // Alpha 0.5f applied to whole composable - Canvas(modifier = Modifier - .size(200.dp) - .graphicsLayer { - alpha = 0.5f - }) { - drawSquares() - } - Spacer(modifier = Modifier.size(36.dp)) - - // 0.75f alpha applied to each draw call when using ModulateAlpha - Canvas(modifier = Modifier - .size(200.dp) - .graphicsLayer { - compositingStrategy = CompositingStrategy.ModulateAlpha - alpha = 0.75f - }) { - drawSquares() - } - } +fun CompositingStrategy_ModulateAlpha() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp) + ) { + // Base drawing, no alpha applied + Canvas( + modifier = Modifier.size(200.dp) + ) { + drawSquares() + } + + Spacer(modifier = Modifier.size(36.dp)) + + // Alpha 0.5f applied to whole composable + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + alpha = 0.5f + } + ) { + drawSquares() + } + Spacer(modifier = Modifier.size(36.dp)) + + // 0.75f alpha applied to each draw call when using ModulateAlpha + Canvas( + modifier = Modifier + .size(200.dp) + .graphicsLayer { + compositingStrategy = CompositingStrategy.ModulateAlpha + alpha = 0.75f + } + ) { + drawSquares() + } + } } private fun DrawScope.drawSquares() { - val size = Size(100.dp.toPx(), 100.dp.toPx()) - drawRect(color = Red, size = size) - drawRect( - color = Purple, size = size, - topLeft = Offset(size.width / 4f, size.height / 4f) - ) - drawRect( - color = Yellow, size = size, - topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) - ) + val size = Size(100.dp.toPx(), 100.dp.toPx()) + drawRect(color = Red, size = size) + drawRect( + color = Purple, size = size, + topLeft = Offset(size.width / 4f, size.height / 4f) + ) + drawRect( + color = Yellow, size = size, + topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) + ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350) // [END android_compose_graphics_modifier_compositing_strategy_modulate_alpha] -*/ // [START android_compose_graphics_modifier_flipped] class FlippedModifier : DrawModifier { @@ -485,3 +507,168 @@ fun ModifierGraphicsFlippedUsage() { ) // [END android_compose_graphics_modifier_flipped_usage] } + +// [START android_compose_graphics_faded_edge_example] +@Composable +fun FadedEdgeBox(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Box( + modifier = modifier + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + .drawWithContent { + drawContent() + drawRect( + brush = Brush.verticalGradient( + listOf(Color.Black, Color.Transparent) + ), + blendMode = BlendMode.DstIn + ) + } + ) { + content() + } +} +// [END android_compose_graphics_faded_edge_example] +@Preview +@Composable +private fun FadingLazyComments() { + FadedEdgeBox( + modifier = Modifier + .padding(32.dp) + .height(300.dp) + .fillMaxWidth() + ) { + LazyColumn { + items(listComments, key = { it.key }) { + ListCommentItem(it) + } + item { + Spacer(Modifier.height(100.dp)) + } + } + } +} + +@Composable +private fun ListCommentItem(it: Comment) { + Row(modifier = Modifier.padding(bottom = 8.dp)) { + val strokeWidthPx = with(LocalDensity.current) { + 2.dp.toPx() + } + Avatar(strokeWidth = strokeWidthPx, modifier = Modifier.size(48.dp)) { + Image( + painter = painterResource(id = it.avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + Spacer(Modifier.width(6.dp)) + Text( + it.text, + fontSize = 20.sp, + modifier = Modifier.align(Alignment.CenterVertically) + ) + } +} + +data class Comment( + val avatar: Int, + val text: String, + val key: Int = Random.nextInt() +) + +val listComments = listOf( + Comment(R.drawable.dog, "Woof 🐶"), + Comment(R.drawable.froyo, "I love ice cream..."), + Comment(R.drawable.donut, "Mmmm delicious"), + Comment(R.drawable.cupcake, "I love cupcakes"), + Comment(R.drawable.gingerbread, "🍪🍪❤️"), + Comment(R.drawable.eclair, "Where do I get the recipe?"), + Comment(R.drawable.froyo, "🍦The ice cream is BEST"), +) + +// [START android_compose_graphics_stacked_clipped_avatars] +@Composable +fun Avatar( + strokeWidth: Float, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val stroke = remember(strokeWidth) { + Stroke(width = strokeWidth) + } + Box( + modifier = modifier + .drawWithContent { + drawContent() + drawCircle( + Color.Black, + size.minDimension / 2, + size.center, + style = stroke, + blendMode = BlendMode.Clear + ) + } + .clip(CircleShape) + ) { + content() + } +} + +@Preview +@Composable +private fun StackedAvatars() { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.linearGradient( + colors = listOf( + Color.Magenta.copy(alpha = 0.5f), + Color.Blue.copy(alpha = 0.5f) + ) + ) + ) + ) { + val size = 80.dp + val strokeWidth = 2.dp + val strokeWidthPx = with(LocalDensity.current) { + strokeWidth.toPx() + } + val sizeModifier = Modifier.size(size) + val avatars = listOf( + R.drawable.cupcake, + R.drawable.donut, + R.drawable.eclair, + R.drawable.froyo, + R.drawable.gingerbread, + R.drawable.dog + ) + val width = ((size / 2) + strokeWidth * 2) * (avatars.size + 1) + Box( + modifier = Modifier + .size(width, size) + .graphicsLayer { + // Use an offscreen buffer as underdraw protection when + // using blendmodes that clear destination pixels + compositingStrategy = CompositingStrategy.Offscreen + } + .align(Alignment.Center), + ) { + var offset = 0.dp + for (avatar in avatars) { + Avatar( + strokeWidth = strokeWidthPx, + modifier = sizeModifier.offset(offset) + ) { + Image( + painter = painterResource(id = avatar), + contentScale = ContentScale.Crop, + contentDescription = null + ) + } + offset += size / 2 + } + } + } +} +// [END android_compose_graphics_stacked_clipped_avatars] From d1e297bd4e81028e062a48527318d08c3a3e751b Mon Sep 17 00:00:00 2001 From: compose-devrel-github-bot <118755852+compose-devrel-github-bot@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:11:50 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20Update=20Dependencies=20(#36?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3b72fb4e..6aed96f52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,20 +1,20 @@ [versions] accompanist = "0.36.0" -androidGradlePlugin = "8.6.1" +androidGradlePlugin = "8.7.0" androidx-activity-compose = "1.9.2" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2024.09.02" +androidx-compose-bom = "2024.09.03" androidx-compose-ui-test = "1.7.0-alpha08" androidx-constraintlayout = "2.1.4" androidx-constraintlayout-compose = "1.0.1" androidx-coordinator-layout = "1.2.0" androidx-corektx = "1.13.1" androidx-emoji2-views = "1.5.0" -androidx-fragment-ktx = "1.8.3" +androidx-fragment-ktx = "1.8.4" androidx-glance-appwidget = "1.1.0" androidx-lifecycle-compose = "2.8.6" androidx-lifecycle-runtime-compose = "2.8.6" -androidx-navigation = "2.8.1" +androidx-navigation = "2.8.2" androidx-paging = "3.3.2" androidx-test = "1.6.1" androidx-test-espresso = "3.6.1" @@ -23,15 +23,15 @@ androidxHiltNavigationCompose = "1.2.0" coil = "2.7.0" # @keep compileSdk = "34" -compose-latest = "1.7.2" +compose-latest = "1.7.3" composeUiTooling = "1.4.0" coreSplashscreen = "1.0.1" -coroutines = "1.7.3" +coroutines = "1.9.0" glide = "1.0.0-beta01" google-maps = "19.0.0" gradle-versions = "0.51.0" hilt = "2.52" -horologist = "0.6.19" +horologist = "0.6.20" junit = "4.13.2" kotlin = "2.0.20" kotlinxSerializationJson = "1.7.3" @@ -94,7 +94,7 @@ androidx-glance-appwidget = { module = "androidx.glance:glance-appwidget", versi androidx-glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "androidx-glance-appwidget" } androidx-graphics-shapes = "androidx.graphics:graphics-shapes:1.0.1" androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHiltNavigationCompose" } -androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5" +androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } @@ -124,7 +124,7 @@ horologist-compose-material = { module = "com.google.android.horologist:horologi junit = { module = "junit:junit", version.ref = "junit" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0" +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } From 0c9024be7705d86edca458dc84f3e47e59996a4a Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Thu, 3 Oct 2024 13:13:13 +0100 Subject: [PATCH 3/3] Add Material Carousel (#363) * Add Carousel * Apply Spotless * add to components * Apply Spotless * Clean up landing screens, using Scaffold and list items. * Apply Spotless * Review comments --------- Co-authored-by: riggaroo --- .../compose/snippets/SnippetsActivity.kt | 2 + .../compose/snippets/components/Carousel.kt | 136 ++++++++++++++++++ .../snippets/components/ComponentsScreen.kt | 52 ++++--- .../compose/snippets/landing/LandingScreen.kt | 57 +++----- .../snippets/navigation/Destination.kt | 1 + 5 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt index 3979a110b..357cf556f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/SnippetsActivity.kt @@ -33,6 +33,7 @@ import com.example.compose.snippets.components.AppBarExamples import com.example.compose.snippets.components.BadgeExamples import com.example.compose.snippets.components.ButtonExamples import com.example.compose.snippets.components.CardExamples +import com.example.compose.snippets.components.CarouselExamples import com.example.compose.snippets.components.CheckboxExamples import com.example.compose.snippets.components.ChipExamples import com.example.compose.snippets.components.ComponentsScreen @@ -109,6 +110,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.PartialBottomSheet -> PartialBottomSheet() TopComponentsDestination.TimePickerExamples -> TimePickerExamples() TopComponentsDestination.DatePickerExamples -> DatePickerExamples() + TopComponentsDestination.CarouselExamples -> CarouselExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt new file mode 100644 index 000000000..fb8a31775 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Carousel.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.snippets.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel +import androidx.compose.material3.carousel.HorizontalUncontainedCarousel +import androidx.compose.material3.carousel.rememberCarouselState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.R + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_multi_browse_basic] +@Composable +fun CarouselExample_MultiBrowse() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val items = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalMultiBrowseCarousel( + state = rememberCarouselState { items.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + preferredItemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = items[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_multi_browse_basic] + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +// [START android_compose_carousel_uncontained_basic] +@Composable +fun CarouselExample() { + data class CarouselItem( + val id: Int, + @DrawableRes val imageResId: Int, + val contentDescription: String + ) + + val carouselItems = remember { + listOf( + CarouselItem(0, R.drawable.cupcake, "cupcake"), + CarouselItem(1, R.drawable.donut, "donut"), + CarouselItem(2, R.drawable.eclair, "eclair"), + CarouselItem(3, R.drawable.froyo, "froyo"), + CarouselItem(4, R.drawable.gingerbread, "gingerbread"), + ) + } + + HorizontalUncontainedCarousel( + state = rememberCarouselState { carouselItems.count() }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 16.dp), + itemWidth = 186.dp, + itemSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 16.dp) + ) { i -> + val item = carouselItems[i] + Image( + modifier = Modifier + .height(205.dp) + .maskClip(MaterialTheme.shapes.extraLarge), + painter = painterResource(id = item.imageResId), + contentDescription = item.contentDescription, + contentScale = ContentScale.Crop + ) + } +} +// [END android_compose_carousel_uncontained_basic] + +@Preview +@Composable +fun CarouselExamples() { + Column { + CarouselExample() + CarouselExample_MultiBrowse() + } +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt index ce1bbae40..7f87a5c95 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/ComponentsScreen.kt @@ -16,45 +16,59 @@ package com.example.compose.snippets.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +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.unit.dp import com.example.compose.snippets.navigation.TopComponentsDestination +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ComponentsScreen( navigate: (TopComponentsDestination) -> Unit ) { - LazyColumn( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - items(TopComponentsDestination.entries) { destination -> - NavigationItem(destination) { - navigate( - destination - ) + Scaffold(topBar = { + TopAppBar(title = { + Text("Common Components") + }) + }, content = { padding -> + LazyColumn( + modifier = Modifier + .padding(padding) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + items(TopComponentsDestination.entries) { destination -> + NavigationItem(destination) { + navigate( + destination + ) + } } } - } + }) } @Composable fun NavigationItem(destination: TopComponentsDestination, onClick: () -> Unit) { - Button( - onClick = { onClick() } - ) { - Text(destination.title) - } + ListItem( + headlineContent = { + Text(destination.title) + }, + modifier = Modifier.clickable { + onClick() + } + ) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt index 79083bd3b..6e6c84019 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/landing/LandingScreen.kt @@ -18,56 +18,45 @@ package com.example.compose.snippets.landing import androidx.compose.foundation.clickable 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +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.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.compose.snippets.navigation.Destination +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LandingScreen( navigate: (Destination) -> Unit ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Text( - modifier = Modifier.fillMaxWidth(), - style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ), - text = "Android snippets", - ) - Text( - text = "Use the following buttons to view a selection of the snippets used in the Android documentation." - ) - NavigationItems { navigate(it) } + Scaffold( + topBar = { + TopAppBar(title = { + Text(text = "Android snippets",) + }) + } + ) { padding -> + NavigationItems(modifier = Modifier.padding(padding)) { navigate(it) } } } @Composable -fun NavigationItems(navigate: (Destination) -> Unit) { +fun NavigationItems( + modifier: Modifier = Modifier, + navigate: (Destination) -> Unit +) { LazyColumn( - modifier = Modifier + modifier = modifier .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -84,14 +73,14 @@ fun NavigationItems(navigate: (Destination) -> Unit) { @Composable fun NavigationItem(destination: Destination, onClick: () -> Unit) { - Box( + ListItem( + headlineContent = { + Text(destination.title) + }, modifier = Modifier .heightIn(min = 48.dp) .clickable { onClick() } - ) { - Text(destination.title) - HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) - } + ) } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt index 9e7110503..20753d365 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/navigation/Destination.kt @@ -44,4 +44,5 @@ enum class TopComponentsDestination(val route: String, val title: String) { PartialBottomSheet("partialBottomSheets", "Partial Bottom Sheet"), TimePickerExamples("timePickerExamples", "Time Pickers"), DatePickerExamples("datePickerExamples", "Date Pickers"), + CarouselExamples("carouselExamples", "Carousel") }