From 31b4de25dcfe883c3591790ebb1006c40a9c9add Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Wed, 2 Oct 2024 09:12:37 +0100 Subject: [PATCH 1/8] 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 ef00faec..0493012e 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/8] =?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 b3b72fb4..6aed96f5 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/8] 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 3979a110..357cf556 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 00000000..fb8a3177 --- /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 ce1bbae4..7f87a5c9 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 79083bd3..6e6c8401 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 9e711050..20753d36 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") } From 68fef6e953c5566cad2fb9ce37301a87cadc7c2f Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:34:20 +0100 Subject: [PATCH 4/8] Basic menu examples (#371) * basic menu examples * Make DropdownMenuWithDetails toggle expanded on click * Apply Spotless * Remove unneeded dependencies * Remove unneeded imports --------- Co-authored-by: jakeroseman --- .../compose/snippets/SnippetsActivity.kt | 2 + .../compose/snippets/components/Menus.kt | 214 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 3 +- 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.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 357cf556..645cd9e5 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 @@ -41,6 +41,7 @@ import com.example.compose.snippets.components.DatePickerExamples import com.example.compose.snippets.components.DialogExamples import com.example.compose.snippets.components.DividerExamples import com.example.compose.snippets.components.FloatingActionButtonExamples +import com.example.compose.snippets.components.MenusExamples import com.example.compose.snippets.components.PartialBottomSheet import com.example.compose.snippets.components.ProgressIndicatorExamples import com.example.compose.snippets.components.ScaffoldExample @@ -111,6 +112,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.TimePickerExamples -> TimePickerExamples() TopComponentsDestination.DatePickerExamples -> DatePickerExamples() TopComponentsDestination.CarouselExamples -> CarouselExamples() + TopComponentsDestination.MenusExample -> MenusExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt new file mode 100644 index 00000000..c355f948 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt @@ -0,0 +1,214 @@ +/* + * 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.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.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.automirrored.outlined.OpenInNew +import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun MenusExamples() { + var currentExample by remember { mutableStateOf<(@Composable () -> Unit)?>(null) } + + Box(modifier = Modifier.fillMaxSize()) { + currentExample?.let { + it() + FloatingActionButton( + onClick = { currentExample = null }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Text(text = "Close example", modifier = Modifier.padding(16.dp)) + } + return + } + + Column(modifier = Modifier.padding(16.dp)) { + Button(onClick = { currentExample = { MinimalDropdownMenu() } }) { + Text("Minimal dropdown menu") + } + Button(onClick = { currentExample = { LongBasicDropdownMenu() } }) { + Text("Dropdown menu with many items") + } + Button(onClick = { currentExample = { DropdownMenuWithDetails() } }) { + Text("Dropdown menu with sections and icons") + } + } + } +} + +// [START android_compose_components_minimaldropdownmenu] +@Composable +fun MinimalDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text("Option 1") }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Option 2") }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_minimaldropdownmenu] + +@Preview +@Composable +fun MinimalDropdownMenuPreview() { + MinimalDropdownMenu() +} + +// [START android_compose_components_longbasicdropdownmenu] +@Composable +fun LongBasicDropdownMenu() { + var expanded by remember { mutableStateOf(false) } + // Placeholder list of 100 strings for demonstration + val menuItemData = List(100) { "Option ${it + 1}" } + + Box( + modifier = Modifier + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + menuItemData.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { /* Do something... */ } + ) + } + } + } +} +// [END android_compose_components_longbasicdropdownmenu] + +@Preview +@Composable +fun LongBasicDropdownMenuPreview() { + LongBasicDropdownMenu() +} + +// [START android_compose_components_dropdownmenuwithdetails] +@Composable +fun DropdownMenuWithDetails() { + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + // First section + DropdownMenuItem( + text = { Text("Profile") }, + leadingIcon = { Icon(Icons.Outlined.Person, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Settings") }, + leadingIcon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Second section + DropdownMenuItem( + text = { Text("Send Feedback") }, + leadingIcon = { Icon(Icons.Outlined.Feedback, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.Send, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + + HorizontalDivider() + + // Third section + DropdownMenuItem( + text = { Text("About") }, + leadingIcon = { Icon(Icons.Outlined.Info, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + DropdownMenuItem( + text = { Text("Help") }, + leadingIcon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + trailingIcon = { Icon(Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = null) }, + onClick = { /* Do something... */ } + ) + } + } +} +// [END android_compose_components_dropdownmenuwithdetails] + +@Preview +@Composable +fun DropdownMenuWithDetailsPreview() { + DropdownMenuWithDetails() +} 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 20753d36..0bce08f0 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,5 +44,6 @@ 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") + CarouselExamples("carouselExamples", "Carousel"), + MenusExample("menusExamples", "Menus") } From fbfd07d9c17db10e628c84ec865d6628a8ae92c1 Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Fri, 11 Oct 2024 13:14:46 +0100 Subject: [PATCH 5/8] Filter chip dropdown menu (#375) * Filter chip dropdown menu * Apply Spotless --- .../compose/snippets/components/Menus.kt | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt index c355f948..a3468a7c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Menus.kt @@ -16,16 +16,26 @@ package com.example.compose.snippets.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.DirectionsBike +import androidx.compose.material.icons.automirrored.filled.DirectionsRun +import androidx.compose.material.icons.automirrored.filled.DirectionsWalk import androidx.compose.material.icons.automirrored.outlined.Help import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.automirrored.outlined.Send +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Hiking import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.outlined.Feedback import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Person @@ -33,6 +43,7 @@ import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -76,6 +87,9 @@ fun MenusExamples() { Button(onClick = { currentExample = { DropdownMenuWithDetails() } }) { Text("Dropdown menu with sections and icons") } + Button(onClick = { currentExample = { DropdownFilter() } }) { + Text("Menu for applying a filter, attached to a filter chip") + } } } } @@ -212,3 +226,79 @@ fun DropdownMenuWithDetails() { fun DropdownMenuWithDetailsPreview() { DropdownMenuWithDetails() } + +@Composable +fun DropdownFilter(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding(16.dp) + .wrapContentSize(unbounded = true), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.Tune, "Filters") + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Time") }) + DropdownFilterChip() + FilterChip(selected = false, onClick = { /*TODO*/ }, label = { Text("Wheelchair accessible") }) + } +} + +// [START android_compose_components_dropdownfilterchip] +@Composable +fun DropdownFilterChip(modifier: Modifier = Modifier) { + var isDropdownExpanded by remember { mutableStateOf(false) } + var selectedChipText by remember { mutableStateOf(null) } + Box(modifier) { + FilterChip( + selected = selectedChipText != null, + onClick = { isDropdownExpanded = !isDropdownExpanded }, + label = { Text(if (selectedChipText == null) "Type" else "$selectedChipText") }, + leadingIcon = { if (selectedChipText != null) Icon(Icons.Default.Check, null) }, + trailingIcon = { Icon(Icons.Default.ArrowDropDown, null) }, + ) + DropdownMenu( + expanded = isDropdownExpanded, + onDismissRequest = { isDropdownExpanded = !isDropdownExpanded } + ) { + DropdownMenuItem( + text = { Text("Running") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsRun, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Running") null else "Running" + } + ) + DropdownMenuItem( + text = { Text("Walking") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsWalk, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Walking") null else "Walking" + } + ) + DropdownMenuItem( + text = { Text("Hiking") }, + leadingIcon = { Icon(Icons.Default.Hiking, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Hiking") null else "Hiking" + } + ) + DropdownMenuItem( + text = { Text("Cycling") }, + leadingIcon = { Icon(Icons.AutoMirrored.Default.DirectionsBike, null) }, + onClick = { + selectedChipText = + if (selectedChipText == "Cycling") null else "Cycling" + } + ) + } + } +} +// [END android_compose_components_dropdownfilterchip] + +@Preview +@Composable +private fun DropdownFilterPreview() { + DropdownFilter() +} From f67b8492ff5f850ba962102c7b5c65d83895bdd3 Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Fri, 11 Oct 2024 14:56:55 +0100 Subject: [PATCH 6/8] Add example of date picker textfield opening picker dialog on click (#376) * Add example of date picker textfield opening picker dialog on click * Apply Spotless --- .../snippets/components/DatePickers.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt index 650b969a..69219912 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/DatePickers.kt @@ -17,6 +17,9 @@ package com.example.compose.snippets.components import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -49,12 +52,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup +import com.example.compose.snippets.touchinput.userinteractions.MyAppTheme import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +@Preview +@Composable +private fun DatePickerPreview() { + MyAppTheme { + DatePickerExamples() + } +} + // [START android_compose_components_datepicker_examples] // [START_EXCLUDE] @Composable @@ -77,6 +92,9 @@ fun DatePickerExamples() { Text("Docked date picker:") DatePickerDocked() + Text("Open modal picker on click") + DatePickerFieldToModal() + Text("Modal date pickers:") Button(onClick = { showModal = true }) { Text("Show Modal Date Picker") @@ -259,6 +277,43 @@ fun DatePickerDocked() { } } +@Composable +fun DatePickerFieldToModal(modifier: Modifier = Modifier) { + var selectedDate by remember { mutableStateOf(null) } + var showModal by remember { mutableStateOf(false) } + + OutlinedTextField( + value = selectedDate?.let { convertMillisToDate(it) } ?: "", + onValueChange = { }, + label = { Text("DOB") }, + placeholder = { Text("MM/DD/YYYY") }, + trailingIcon = { + Icon(Icons.Default.DateRange, contentDescription = "Select date") + }, + modifier = modifier + .fillMaxWidth() + .pointerInput(selectedDate) { + awaitEachGesture { + // Modifier.clickable doesn't work for text fields, so we use Modifier.pointerInput + // in the Initial pass to observe events before the text field consumes them + // in the Main pass. + awaitFirstDown(pass = PointerEventPass.Initial) + val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) + if (upEvent != null) { + showModal = true + } + } + } + ) + + if (showModal) { + DatePickerModal( + onDateSelected = { selectedDate = it }, + onDismiss = { showModal = false } + ) + } +} + fun convertMillisToDate(millis: Long): String { val formatter = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault()) return formatter.format(Date(millis)) From a0b94c02fb4043e2b4889e78a542d7562fbfd0e8 Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:14:14 +0100 Subject: [PATCH 7/8] Add auto advance pager snippets (#377) * Add auto advance pager snippets * Apply Spotless --------- Co-authored-by: jakeroseman --- .../compose/snippets/SnippetsActivity.kt | 2 + .../compose/snippets/layouts/PagerSnippets.kt | 109 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 3 +- 3 files changed, 113 insertions(+), 1 deletion(-) 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 645cd9e5..40d0a254 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 @@ -53,6 +53,7 @@ import com.example.compose.snippets.graphics.BitmapFromComposableFullSnippet import com.example.compose.snippets.graphics.BrushExamplesScreen import com.example.compose.snippets.images.ImageExamplesScreen import com.example.compose.snippets.landing.LandingScreen +import com.example.compose.snippets.layouts.PagerExamples import com.example.compose.snippets.navigation.Destination import com.example.compose.snippets.navigation.TopComponentsDestination import com.example.compose.snippets.ui.theme.SnippetsTheme @@ -87,6 +88,7 @@ class SnippetsActivity : ComponentActivity() { } Destination.ShapesExamples -> ApplyPolygonAsClipImage() Destination.SharedElementExamples -> PlaceholderSizeAnimated_Demo() + Destination.PagerExamples -> PagerExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt index ec8e84cb..45361ef8 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/layouts/PagerSnippets.kt @@ -20,7 +20,12 @@ package com.example.compose.snippets.layouts import android.util.Log import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -50,6 +55,8 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -58,6 +65,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @@ -65,6 +73,7 @@ import androidx.compose.ui.util.lerp import coil.compose.rememberAsyncImagePainter import com.example.compose.snippets.util.rememberRandomSampleImageUrl import kotlin.math.absoluteValue +import kotlinx.coroutines.delay import kotlinx.coroutines.launch /* @@ -83,6 +92,18 @@ import kotlinx.coroutines.launch * limitations under the License. */ +@Composable +fun PagerExamples() { + AutoAdvancePager( + listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + ) +} + @Preview @Composable fun HorizontalPagerSample() { @@ -392,6 +413,94 @@ fun PagerIndicator() { } } +@Composable +fun AutoAdvancePager(pageItems: List, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + val pagerState = rememberPagerState(pageCount = { pageItems.size }) + val pagerIsDragged by pagerState.interactionSource.collectIsDraggedAsState() + + val pageInteractionSource = remember { MutableInteractionSource() } + val pageIsPressed by pageInteractionSource.collectIsPressedAsState() + + // Stop auto-advancing when pager is dragged or one of the pages is pressed + val autoAdvance = !pagerIsDragged && !pageIsPressed + + if (autoAdvance) { + LaunchedEffect(pagerState, pageInteractionSource) { + while (true) { + delay(2000) + val nextPage = (pagerState.currentPage + 1) % pageItems.size + pagerState.animateScrollToPage(nextPage) + } + } + } + + HorizontalPager( + state = pagerState + ) { page -> + Text( + text = "Page: $page", + textAlign = TextAlign.Center, + modifier = modifier + .fillMaxSize() + .background(pageItems[page]) + .clickable( + interactionSource = pageInteractionSource, + indication = LocalIndication.current + ) { + // Handle page click + } + .wrapContentSize(align = Alignment.Center) + ) + } + + PagerIndicator(pageItems.size, pagerState.currentPage) + } +} + +@Preview +@Composable +private fun AutoAdvancePagerPreview() { + val pageItems: List = listOf( + Color.Red, + Color.Gray, + Color.Green, + Color.White + ) + AutoAdvancePager(pageItems = pageItems) +} + +@Composable +fun PagerIndicator(pageCount: Int, currentPageIndex: Int, modifier: Modifier = Modifier) { + Box(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.Center + ) { + repeat(pageCount) { iteration -> + val color = if (currentPageIndex == iteration) Color.DarkGray else Color.LightGray + Box( + modifier = modifier + .padding(2.dp) + .clip(CircleShape) + .background(color) + .size(16.dp) + ) + } + } + } +} + +@Preview +@Composable +private fun PagerIndicatorPreview() { + PagerIndicator(pageCount = 4, currentPageIndex = 1) +} + // [START android_compose_pager_custom_page_size] private val threePagesPerViewport = object : PageSize { override fun Density.calculateMainAxisPageSize( 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 0bce08f0..70368d31 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 @@ -23,7 +23,8 @@ enum class Destination(val route: String, val title: String) { ComponentsExamples("topComponents", "Top Compose Components"), ScreenshotExample("screenshotExample", "Screenshot Examples"), ShapesExamples("shapesExamples", "Shapes Examples"), - SharedElementExamples("sharedElement", "Shared elements") + SharedElementExamples("sharedElement", "Shared elements"), + PagerExamples("pagerExamples", "Pager examples") } // Enum class for compose components navigation screen. From 591dfa5cc87e9b1ee7adc59006443e88d1c2098e Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:39:40 +0100 Subject: [PATCH 8/8] Tooltip component examples (#373) * Tooltip component examples * Apply Spotless * Addressing PR comments * use LaunchedEffect to fix tooltip bug * Apply Spotless * Updated content descriptions --------- Co-authored-by: jakeroseman --- .../compose/snippets/SnippetsActivity.kt | 2 + .../compose/snippets/components/Tooltips.kt | 212 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 3 +- 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.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 40d0a254..c8aab798 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 @@ -48,6 +48,7 @@ import com.example.compose.snippets.components.ScaffoldExample import com.example.compose.snippets.components.SliderExamples import com.example.compose.snippets.components.SwitchExamples import com.example.compose.snippets.components.TimePickerExamples +import com.example.compose.snippets.components.TooltipExamples import com.example.compose.snippets.graphics.ApplyPolygonAsClipImage import com.example.compose.snippets.graphics.BitmapFromComposableFullSnippet import com.example.compose.snippets.graphics.BrushExamplesScreen @@ -115,6 +116,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.DatePickerExamples -> DatePickerExamples() TopComponentsDestination.CarouselExamples -> CarouselExamples() TopComponentsDestination.MenusExample -> MenusExamples() + TopComponentsDestination.TooltipExamples -> TooltipExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt new file mode 100644 index 00000000..e43ac2d9 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/Tooltips.kt @@ -0,0 +1,212 @@ +/* + * 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.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.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Camera +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.RichTooltip +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.TooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +@Composable +fun TooltipExamples() { + Text( + "Long press an icon to see the tooltip.", + modifier = Modifier.fillMaxWidth().padding(16.dp), + textAlign = TextAlign.Center + ) + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + PlainTooltipExample() + RichTooltipExample() + AdvancedRichTooltipExample() + } +} + +@Preview +@Composable +private fun TooltipExamplesPreview() { + TooltipExamples() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_plaintooltipexample] +@Composable +fun PlainTooltipExample( + modifier: Modifier = Modifier, + plainTooltipText: String = "Add to favorites" +) { + var tooltipState by remember { mutableStateOf(TooltipState()) } + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip { Text(plainTooltipText) } + }, + state = tooltipState + ) { + IconButton(onClick = { /* Do something... */ }) { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = "Add to favorites" + ) + } + } + + // Reset tooltipState after closing the tooltip. + LaunchedEffect(tooltipState.isVisible) { + if (!tooltipState.isVisible) { + tooltipState = TooltipState() + } + } +} + +// [END android_compose_components_plaintooltipexample] + +@Preview +@Composable +private fun PlainTooltipSamplePreview() { + PlainTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_richtooltipexample] +@Composable +fun RichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text." +) { + var tooltipState by remember { mutableStateOf(TooltipState(isPersistent = true)) } + + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) } + ) { + Text(richTooltipText) + } + }, + state = tooltipState + ) { + IconButton(onClick = { /* Icon button's click event */ }) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = "Show more information" + ) + } + } + + // Reset tooltipState after closing the tooltip. + LaunchedEffect(tooltipState.isVisible) { + if (!tooltipState.isVisible) { + tooltipState = TooltipState(isPersistent = true) + } + } +} +// [END android_compose_components_richtooltipexample] + +@Preview +@Composable +private fun RichTooltipSamplePreview() { + RichTooltipExample() +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_advancedrichtooltipexample] +@Composable +fun AdvancedRichTooltipExample( + modifier: Modifier = Modifier, + richTooltipSubheadText: String = "Custom Rich Tooltip", + richTooltipText: String = "Rich tooltips support multiple lines of informational text.", + richTooltipActionText: String = "Dismiss" +) { + var tooltipState by remember { mutableStateOf(TooltipState(isPersistent = true)) } + + TooltipBox( + modifier = modifier, + positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(), + tooltip = { + RichTooltip( + title = { Text(richTooltipSubheadText) }, + action = { + Row { + TextButton(onClick = { tooltipState.dismiss() }) { + Text(richTooltipActionText) + } + } + }, + caretSize = DpSize(32.dp, 16.dp) + ) { + Text(richTooltipText) + } + }, + state = tooltipState + ) { + IconButton(onClick = { tooltipState.dismiss() }) { + Icon( + imageVector = Icons.Filled.Camera, + contentDescription = "Open camera" + ) + } + } + + // Reset tooltipState after closing the tooltip. + LaunchedEffect(tooltipState.isVisible) { + if (!tooltipState.isVisible) { + tooltipState = TooltipState(isPersistent = true) + } + } +} +// [END android_compose_components_advancedrichtooltipexample] + +@Preview +@Composable +private fun RichTooltipWithCustomCaretSamplePreview() { + AdvancedRichTooltipExample() +} 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 70368d31..189f473d 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 @@ -46,5 +46,6 @@ enum class TopComponentsDestination(val route: String, val title: String) { TimePickerExamples("timePickerExamples", "Time Pickers"), DatePickerExamples("datePickerExamples", "Date Pickers"), CarouselExamples("carouselExamples", "Carousel"), - MenusExample("menusExamples", "Menus") + MenusExample("menusExamples", "Menus"), + TooltipExamples("tooltipExamples", "Tooltips") }