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 1/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 2/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 3/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 4/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 5/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") } From 4baf47718fa0e5e7e0f153da3722be1216ad9b04 Mon Sep 17 00:00:00 2001 From: Jolanda Verhoef Date: Fri, 11 Oct 2024 19:57:31 +0100 Subject: [PATCH 6/8] Add pull to refresh snippets (#378) * Add pull to refresh snippets * Apply Spotless --- .../snippets/components/PullToRefreshBox.kt | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt new file mode 100644 index 00000000..81280151 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/PullToRefreshBox.kt @@ -0,0 +1,241 @@ +/* + * 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.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.PositionalThreshold +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.pullToRefreshIndicator +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.CROSSFADE_DURATION_MILLIS +import com.example.compose.snippets.components.PullToRefreshIndicatorConstants.SPINNER_SIZE +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private object PullToRefreshIndicatorConstants { + const val CROSSFADE_DURATION_MILLIS = 100 + val SPINNER_SIZE = 16.dp +} + +@Preview +@Composable +fun PullToRefreshBasicPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshBasicSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomStylePreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomStyleSample(items, isRefreshing, onRefresh) + } +} + +@Preview +@Composable +fun PullToRefreshCustomIndicatorPreview() { + PullToRefreshStatefulWrapper { itemCount, isRefreshing, onRefresh -> + val items = List(itemCount) { "Item ${itemCount - it}" } + PullToRefreshCustomIndicatorSample(items, isRefreshing, onRefresh) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_basic] +@Composable +fun PullToRefreshBasicSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_basic] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_style] +@Composable +fun PullToRefreshCustomStyleSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + Indicator( + modifier = Modifier.align(Alignment.TopCenter), + isRefreshing = isRefreshing, + containerColor = MaterialTheme.colorScheme.primaryContainer, + color = MaterialTheme.colorScheme.onPrimaryContainer, + state = state + ) + }, + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_style] + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_pull_to_refresh_custom_indicator] +@Composable +fun PullToRefreshCustomIndicatorSample( + items: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier +) { + val state = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = onRefresh, + modifier = modifier, + state = state, + indicator = { + MyCustomIndicator( + state = state, + isRefreshing = isRefreshing, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + ) { + LazyColumn(Modifier.fillMaxSize()) { + items(items) { + ListItem({ Text(text = it) }) + } + } + } +} + +// [START_EXCLUDE] +@OptIn(ExperimentalMaterial3Api::class) +// [END_EXCLUDE] +@Composable +fun MyCustomIndicator( + state: PullToRefreshState, + isRefreshing: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.pullToRefreshIndicator( + state = state, + isRefreshing = isRefreshing, + containerColor = PullToRefreshDefaults.containerColor, + threshold = PositionalThreshold + ), + contentAlignment = Alignment.Center + ) { + Crossfade( + targetState = isRefreshing, + animationSpec = tween(durationMillis = CROSSFADE_DURATION_MILLIS), + modifier = Modifier.align(Alignment.Center) + ) { refreshing -> + if (refreshing) { + CircularProgressIndicator(Modifier.size(SPINNER_SIZE)) + } else { + val distanceFraction = { state.distanceFraction.coerceIn(0f, 1f) } + Icon( + imageVector = Icons.Filled.CloudDownload, + contentDescription = "Refresh", + modifier = Modifier + .size(18.dp) + .graphicsLayer { + val progress = distanceFraction() + this.alpha = progress + this.scaleX = progress + this.scaleY = progress + } + ) + } + } + } +} +// [END android_compose_components_pull_to_refresh_custom_indicator] + +@Composable +fun PullToRefreshStatefulWrapper( + content: @Composable (itemCount: Int, isRefreshing: Boolean, onRefresh: () -> Unit) -> Unit +) { + var itemCount by remember { mutableIntStateOf(15) } + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + val onRefresh: () -> Unit = { + isRefreshing = true + coroutineScope.launch { + delay(1500) + itemCount += 5 + isRefreshing = false + } + } + content(itemCount, isRefreshing, onRefresh) +} From 5545f3751aaf989770dd6b67b518e81c368444fd Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:21:49 +0100 Subject: [PATCH 7/8] Remove LaunchedEffect workaround (#379) * Remove LaunchedEffect workaround for library bug * Apply Spotless * Changed single var to val --------- Co-authored-by: jakeroseman --- .../compose/snippets/components/Tooltips.kt | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) 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 index e43ac2d9..1b5d9ea0 100644 --- 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 @@ -34,12 +34,9 @@ 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.material3.rememberTooltipState 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 @@ -79,14 +76,13 @@ 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 + state = rememberTooltipState() ) { IconButton(onClick = { /* Do something... */ }) { Icon( @@ -95,13 +91,6 @@ fun PlainTooltipExample( ) } } - - // Reset tooltipState after closing the tooltip. - LaunchedEffect(tooltipState.isVisible) { - if (!tooltipState.isVisible) { - tooltipState = TooltipState() - } - } } // [END android_compose_components_plaintooltipexample] @@ -120,8 +109,6 @@ fun RichTooltipExample( 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(), @@ -132,7 +119,7 @@ fun RichTooltipExample( Text(richTooltipText) } }, - state = tooltipState + state = rememberTooltipState() ) { IconButton(onClick = { /* Icon button's click event */ }) { Icon( @@ -141,13 +128,6 @@ fun RichTooltipExample( ) } } - - // Reset tooltipState after closing the tooltip. - LaunchedEffect(tooltipState.isVisible) { - if (!tooltipState.isVisible) { - tooltipState = TooltipState(isPersistent = true) - } - } } // [END android_compose_components_richtooltipexample] @@ -166,7 +146,7 @@ fun AdvancedRichTooltipExample( richTooltipText: String = "Rich tooltips support multiple lines of informational text.", richTooltipActionText: String = "Dismiss" ) { - var tooltipState by remember { mutableStateOf(TooltipState(isPersistent = true)) } + val tooltipState = rememberTooltipState() TooltipBox( modifier = modifier, @@ -195,13 +175,6 @@ fun AdvancedRichTooltipExample( ) } } - - // Reset tooltipState after closing the tooltip. - LaunchedEffect(tooltipState.isVisible) { - if (!tooltipState.isVisible) { - tooltipState = TooltipState(isPersistent = true) - } - } } // [END android_compose_components_advancedrichtooltipexample] From 90b8500ff3655f1559fcb6ea0476af6d5c277b19 Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:05:17 +0100 Subject: [PATCH 8/8] Navigation drawer examples (#380) * Basic navigation drawer examples * Add previews * Fix merge issue * Apply Spotless * rearrange functions * Narrowing the examples to just the example with nested items * Apply Spotless * refactoring as dismissable drawer * Fixing imports * refactor, new region tags * Renaming functions * Apply Spotless * Add horizontal padding to the drawer content * Apply Spotless * Make drawer content scrollable to make it work on small screens / landscape --------- Co-authored-by: jakeroseman Co-authored-by: Jolanda Verhoef --- .../compose/snippets/SnippetsActivity.kt | 2 + .../snippets/components/NavigationDrawer.kt | 147 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 3 +- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.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 c8aab798..33e8c3dd 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 @@ -16,6 +16,7 @@ package com.example.compose.snippets +import NavigationDrawerExamples import android.os.Bundle import android.os.StrictMode import androidx.activity.ComponentActivity @@ -117,6 +118,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.CarouselExamples -> CarouselExamples() TopComponentsDestination.MenusExample -> MenusExamples() TopComponentsDestination.TooltipExamples -> TooltipExamples() + TopComponentsDestination.NavigationDrawerExamples -> NavigationDrawerExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt new file mode 100644 index 00000000..993f1c99 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/NavigationDrawer.kt @@ -0,0 +1,147 @@ +/* + * 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. + */ + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Help +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun NavigationDrawerExamples() { + // Add more examples here in future if necessary. + + DetailedDrawerExample { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + Text( + "Swipe from left edge or use menu icon to open the dismissible drawer", + modifier = Modifier.padding(16.dp) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_detaileddrawerexample] +@Composable +fun DetailedDrawerExample( + content: @Composable (PaddingValues) -> Unit +) { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() + + ModalNavigationDrawer( + drawerContent = { + ModalDrawerSheet { + Column( + modifier = Modifier.padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + Spacer(Modifier.height(12.dp)) + Text("Drawer Title", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleLarge) + HorizontalDivider() + + Text("Section 1", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Item 1") }, + selected = false, + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Item 2") }, + selected = false, + onClick = { /* Handle click */ } + ) + + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + Text("Section 2", modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.titleMedium) + NavigationDrawerItem( + label = { Text("Settings") }, + selected = false, + icon = { Icon(Icons.Outlined.Settings, contentDescription = null) }, + badge = { Text("20") }, // Placeholder + onClick = { /* Handle click */ } + ) + NavigationDrawerItem( + label = { Text("Help and feedback") }, + selected = false, + icon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) }, + onClick = { /* Handle click */ }, + ) + Spacer(Modifier.height(12.dp)) + } + } + }, + drawerState = drawerState + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Navigation Drawer Example") }, + navigationIcon = { + IconButton(onClick = { + scope.launch { + if (drawerState.isClosed) { + drawerState.open() + } else { + drawerState.close() + } + } + }) { + Icon(Icons.Default.Menu, contentDescription = "Menu") + } + } + ) + } + ) { innerPadding -> + content(innerPadding) + } + } +} +// [END android_compose_components_detaileddrawerexample] + +@Preview +@Composable +fun DetailedDrawerExamplePreview() { + NavigationDrawerExamples() +} 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 189f473d..f913923e 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 @@ -47,5 +47,6 @@ enum class TopComponentsDestination(val route: String, val title: String) { DatePickerExamples("datePickerExamples", "Date Pickers"), CarouselExamples("carouselExamples", "Carousel"), MenusExample("menusExamples", "Menus"), - TooltipExamples("tooltipExamples", "Tooltips") + TooltipExamples("tooltipExamples", "Tooltips"), + NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer") }