From 22976bd4e3586c6084d6419807ede7d0ed26da0e Mon Sep 17 00:00:00 2001 From: compose-devrel-github-bot <118755852+compose-devrel-github-bot@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:35:39 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=A4=96=20Update=20Dependencies=20(#42?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b75440f..15dcc4e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ accompanist = "0.36.0" androidGradlePlugin = "8.7.3" androidx-activity-compose = "1.9.3" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2024.11.00" +androidx-compose-bom = "2024.12.01" androidx-compose-ui-test = "1.7.0-alpha08" androidx-constraintlayout = "2.2.0" androidx-constraintlayout-compose = "1.1.0" @@ -14,8 +14,8 @@ androidx-fragment-ktx = "1.8.5" androidx-glance-appwidget = "1.1.1" androidx-lifecycle-compose = "2.8.7" androidx-lifecycle-runtime-compose = "2.8.7" -androidx-navigation = "2.8.4" -androidx-paging = "3.3.4" +androidx-navigation = "2.8.5" +androidx-paging = "3.3.5" androidx-test = "1.6.1" androidx-test-espresso = "3.6.1" androidx-window = "1.3.0" @@ -23,39 +23,39 @@ androidxHiltNavigationCompose = "1.2.0" coil = "2.7.0" # @keep compileSdk = "35" -compose-latest = "1.7.5" +compose-latest = "1.7.6" composeUiTooling = "1.4.0" coreSplashscreen = "1.0.1" coroutines = "1.9.0" glide = "1.0.0-beta01" google-maps = "19.0.0" gradle-versions = "0.51.0" -guava = "33.2.1-android" -hilt = "2.52" -horologist = "0.6.20" +guava = "33.4.0-jre" +hilt = "2.53.1" +horologist = "0.6.22" junit = "4.13.2" -kotlin = "2.0.21" +kotlin = "2.1.0" kotlinxSerializationJson = "1.7.3" -ksp = "2.0.21-1.0.26" -maps-compose = "6.2.0" -material = "1.13.0-alpha07" +ksp = "2.1.0-1.0.29" +maps-compose = "6.4.0" +material = "1.13.0-alpha08" material3-adaptive = "1.0.0" material3-adaptive-navigation-suite = "1.3.1" -media3 = "1.4.1" +media3 = "1.5.0" # @keep minSdk = "21" -playServicesWearable = "18.2.0" -protolayout = "1.3.0-alpha04" -protolayoutExpression = "1.3.0-alpha04" -protolayoutMaterial = "1.3.0-alpha04" +playServicesWearable = "19.0.0" +protolayout = "1.3.0-alpha05" +protolayoutExpression = "1.3.0-alpha05" +protolayoutMaterial = "1.3.0-alpha05" recyclerview = "1.3.2" # @keep targetSdk = "34" -tiles = "1.5.0-alpha04" -tilesRenderer = "1.5.0-alpha04" -tilesTesting = "1.5.0-alpha04" -tilesTooling = "1.5.0-alpha04" -tilesToolingPreview = "1.5.0-alpha04" +tiles = "1.5.0-alpha05" +tilesRenderer = "1.5.0-alpha05" +tilesTesting = "1.5.0-alpha05" +tilesTooling = "1.5.0-alpha05" +tilesToolingPreview = "1.5.0-alpha05" version-catalog-update = "0.8.5" wear = "1.3.0" wearComposeFoundation = "1.4.0" @@ -64,7 +64,7 @@ wearToolingPreview = "1.0.0" [libraries] accompanist-adaptive = { module = "com.google.accompanist:accompanist-adaptive", version.ref = "accompanist" } -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } +accompanist-permissions = "com.google.accompanist:accompanist-permissions:0.37.0" accompanist-theme-adapter-appcompat = { module = "com.google.accompanist:accompanist-themeadapter-appcompat", version.ref = "accompanist" } accompanist-theme-adapter-material = { module = "com.google.accompanist:accompanist-themeadapter-material", version.ref = "accompanist" } accompanist-theme-adapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist" } From e6997d9eacbf86fc5ff8c910dc5baa0913768961 Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Fri, 20 Dec 2024 10:55:44 +0000 Subject: [PATCH 2/5] Adding SearchBar examples (#408) * Adding SearchBar examples * Apply Spotless * Add search bar examples to top compose examples * Add content descriptions * Delete shared/build/generated/res/pngs/debug/drawable-anydpi-v24/ic_launcher_foreground.xml * remove errant build files from branch * Apply Spotless --------- Co-authored-by: jakeroseman Co-authored-by: Rebecca Franks --- .../compose/snippets/SnippetsActivity.kt | 2 + .../compose/snippets/components/SearchBar.kt | 244 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 3 +- 3 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.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 f07c4613..49f42603 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 @@ -46,6 +46,7 @@ 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 +import com.example.compose.snippets.components.SearchBarExamples import com.example.compose.snippets.components.SegmentedButtonExamples import com.example.compose.snippets.components.SliderExamples import com.example.compose.snippets.components.SwitchExamples @@ -121,6 +122,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.TooltipExamples -> TooltipExamples() TopComponentsDestination.NavigationDrawerExamples -> NavigationDrawerExamples() TopComponentsDestination.SegmentedButtonExamples -> SegmentedButtonExamples() + TopComponentsDestination.SearchBarExamples -> SearchBarExamples() } } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt new file mode 100644 index 00000000..78c37b8e --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SearchBar.kt @@ -0,0 +1,244 @@ +/* + * 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.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SearchBarExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + var currentExample by remember { mutableStateOf(null) } + + when (currentExample) { + "basic" -> SearchBarBasicFilterList() + "advanced" -> AppSearchBar() + else -> { + Button(onClick = { currentExample = "basic" }) { + Text("Basic search bar with filter") + } + Button(onClick = { currentExample = "advanced" }) { + Text("Advanced search bar with filter") + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +// [START android_compose_components_searchbarbasicfilterlist] +@Composable +fun SearchBarBasicFilterList(modifier: Modifier = Modifier) { + var text by rememberSaveable { mutableStateOf("") } + var expanded by rememberSaveable { mutableStateOf(false) } + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = text, + onQueryChange = { text = it }, + onSearch = { expanded = false }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Hinted search text") } + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + Column(Modifier.verticalScroll(rememberScrollState())) { + repeat(4) { index -> + val resultText = "Suggestion $index" + ListItem( + headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + modifier = Modifier + .clickable { + text = resultText + expanded = false + } + .fillMaxWidth() + ) + } + } + } + } +} +// [END android_compose_components_searchbarbasicfilterlist] + +@Preview(showBackground = true) +@Composable +private fun SearchBarBasicFilterListPreview() { + SearchBarBasicFilterList() +} + +// [START android_compose_components_searchbarfilterlist] +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchBarFilterList( + list: List, + modifier: Modifier = Modifier +) { + var text by rememberSaveable { mutableStateOf("") } + val filteredList by remember { + derivedStateOf { + list.filter { it.lowercase().contains(text.lowercase()) } + } + } + var expanded by rememberSaveable { mutableStateOf(false) } + + Box( + modifier + .fillMaxSize() + .semantics { isTraversalGroup = true } + ) { + SearchBar( + modifier = Modifier + .align(Alignment.TopCenter) + .semantics { traversalIndex = 0f }, + inputField = { + SearchBarDefaults.InputField( + query = text, + onQueryChange = { text = it }, + onSearch = { expanded = false }, + expanded = expanded, + onExpandedChange = { expanded = it }, + placeholder = { Text("Hinted search text") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = "More options") }, + ) + }, + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + LazyColumn { + items(count = filteredList.size) { index -> + val resultText = filteredList[index] + ListItem( + headlineContent = { Text(resultText) }, + supportingContent = { Text("Additional info") }, + leadingContent = { + Icon( + Icons.Filled.Star, + contentDescription = "Starred item" + ) + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier + .clickable { + text = resultText + expanded = false + } + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + } + } + LazyColumn( + contentPadding = PaddingValues( + start = 16.dp, + top = 72.dp, + end = 16.dp, + bottom = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.semantics { + traversalIndex = 1f + }, + ) { + items(count = filteredList.size) { + Text(text = filteredList[it]) + } + } + } +} +// [END android_compose_components_searchbarfilterlist] + +@Preview(showBackground = true) +@Composable +fun AppSearchBar(modifier: Modifier = Modifier) { + SearchBarFilterList( + list = listOf( + "Cupcake", + "Donut", + "Eclair", + "Froyo", + "Gingerbread", + "Honeycomb", + "Ice Cream Sandwich", + "Jelly Bean", + "KitKat", + "Lollipop", + "Marshmallow", + "Nougat", + "Oreo", + "Pie" + ), + modifier + ) +} 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 78396390..dbfdece1 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 @@ -49,5 +49,6 @@ enum class TopComponentsDestination(val route: String, val title: String) { MenusExample("menusExamples", "Menus"), TooltipExamples("tooltipExamples", "Tooltips"), NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer"), - SegmentedButtonExamples("segmentedButtonExamples", "Segmented button") + SegmentedButtonExamples("segmentedButtonExamples", "Segmented button"), + SearchBarExamples("searchBarExamples", "Search bar") } From 5bf449b7382ace487ba77e15ef354d2729a24a2b Mon Sep 17 00:00:00 2001 From: Jake Roseman <122034773+jakeroseman@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:14:46 +0000 Subject: [PATCH 3/5] Add Swipe to reveal examples (#412) * Add Swipe to reveal examples * Apply Spotless * Adding swipe to dismiss examples to the top compose examples. * Tidy up merge conflict errors --------- Co-authored-by: jakeroseman --- .../compose/snippets/SnippetsActivity.kt | 2 + .../snippets/components/SwipeToDismissBox.kt | 339 ++++++++++++++++++ .../snippets/navigation/Destination.kt | 1 + 3 files changed, 342 insertions(+) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.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 49f42603..48f2a110 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 @@ -49,6 +49,7 @@ import com.example.compose.snippets.components.ScaffoldExample import com.example.compose.snippets.components.SearchBarExamples import com.example.compose.snippets.components.SegmentedButtonExamples import com.example.compose.snippets.components.SliderExamples +import com.example.compose.snippets.components.SwipeToDismissBoxExamples import com.example.compose.snippets.components.SwitchExamples import com.example.compose.snippets.components.TimePickerExamples import com.example.compose.snippets.components.TooltipExamples @@ -122,6 +123,7 @@ class SnippetsActivity : ComponentActivity() { TopComponentsDestination.TooltipExamples -> TooltipExamples() TopComponentsDestination.NavigationDrawerExamples -> NavigationDrawerExamples() TopComponentsDestination.SegmentedButtonExamples -> SegmentedButtonExamples() + TopComponentsDestination.SwipeToDismissBoxExamples -> SwipeToDismissBoxExamples() TopComponentsDestination.SearchBarExamples -> SearchBarExamples() } } diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt new file mode 100644 index 00000000..34a6aaeb --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/components/SwipeToDismissBox.kt @@ -0,0 +1,339 @@ +/* + * 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.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material.icons.Icons +import androidx.compose.material.icons.filled.CheckBox +import androidx.compose.material.icons.filled.CheckBoxOutlineBlank +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SwipeToDismissBoxExamples() { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("Swipe to dismiss with change of background", fontWeight = FontWeight.Bold) + SwipeItemExample() + Text("Swipe to dismiss with a cross-fade animation", fontWeight = FontWeight.Bold) + SwipeCardItemExample() + } +} + +// [START android_compose_components_todoitem] +data class TodoItem( + var isItemDone: Boolean, + var itemDescription: String +) +// [END android_compose_components_todoitem] + +// [START android_compose_components_swipeitem] +@Composable +fun SwipeItem( + value: TodoItem, + startToEndAction: (TodoItem) -> Unit, + endToStartAction: (TodoItem) -> Unit, + modifier: Modifier = Modifier, + content: @Composable (TodoItem) -> Unit +) { + val swipeToDismissBoxState = rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + startToEndAction(value) + // Do not dismiss this item. + false + } + SwipeToDismissBoxValue.EndToStart -> { + endToStartAction(value) + true + } + SwipeToDismissBoxValue.Settled -> { + false + } + } + } + ) + + SwipeToDismissBox( + state = swipeToDismissBoxState, + modifier = modifier + .fillMaxSize(), + backgroundContent = { + Row( + modifier = Modifier + .background( + when (swipeToDismissBoxState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + Color.Blue + } + SwipeToDismissBoxValue.EndToStart -> { + Color.Red + } + SwipeToDismissBoxValue.Settled -> { + Color.LightGray + } + } + ) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + when (swipeToDismissBoxState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + if (value.isItemDone) { + Icon( + imageVector = Icons.Default.CheckBox, + contentDescription = "Item done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } else { + Icon( + imageVector = Icons.Default.CheckBoxOutlineBlank, + contentDescription = "Item not done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + } + + SwipeToDismissBoxValue.EndToStart -> { + Spacer(modifier = Modifier) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + + SwipeToDismissBoxValue.Settled -> {} + } + } + } + ) { + content(value) + } +} +// [END android_compose_components_swipeitem] + +@Preview(showBackground = true) +// [START android_compose_components_swipeitemexample] +@Composable +private fun SwipeItemExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem(isItemDone = false, itemDescription = "Pay bills"), + TodoItem(isItemDone = false, itemDescription = "Buy groceries"), + TodoItem(isItemDone = false, itemDescription = "Go to gym"), + TodoItem(isItemDone = false, itemDescription = "Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + SwipeItem( + value = todoItem, + startToEndAction = { + todoItem.isItemDone = !todoItem.isItemDone + }, + endToStartAction = { + todoItems -= todoItem + } + ) { + ListItem( + headlineContent = { Text(text = todoItem.itemDescription) }, + supportingContent = { Text(text = "swipe me to update or remove.") } + ) + } + } + } +} +// [END android_compose_components_swipeitemexample] + +// [START android_compose_components_swipecarditem] +@Composable +fun SwipeCardItem( + value: TodoItem, + startToEndAction: (TodoItem) -> Unit, + endToStartAction: (TodoItem) -> Unit, + modifier: Modifier = Modifier, + content: @Composable (TodoItem) -> Unit +) { + val swipeToDismissState = rememberSwipeToDismissBoxState( + positionalThreshold = { totalDistance -> totalDistance * 0.25f }, + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> { + startToEndAction(value) + // Do not dismiss this item. + false + } + SwipeToDismissBoxValue.EndToStart -> { + endToStartAction(value) + true + } + SwipeToDismissBoxValue.Settled -> { + false + } + } + } + ) + + SwipeToDismissBox( + modifier = Modifier, + state = swipeToDismissState, + backgroundContent = { + // Cross-fade the background color as the drag gesture progresses. + val color by animateColorAsState( + when (swipeToDismissState.targetValue) { + SwipeToDismissBoxValue.Settled -> Color.LightGray + SwipeToDismissBoxValue.StartToEnd -> + lerp(Color.LightGray, Color.Blue, swipeToDismissState.progress) + + SwipeToDismissBoxValue.EndToStart -> + lerp(Color.LightGray, Color.Red, swipeToDismissState.progress) + }, + label = "swipeable card item background color" + ) + Row( + modifier = Modifier + .background(color) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + when (swipeToDismissState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + if (value.isItemDone) { + Icon( + imageVector = Icons.Default.CheckBox, + contentDescription = "Item done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } else { + Icon( + imageVector = Icons.Default.CheckBoxOutlineBlank, + contentDescription = "Item not done", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + } + + SwipeToDismissBoxValue.EndToStart -> { + Spacer(modifier = Modifier) + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Remove item", + tint = Color.White, + modifier = Modifier + .padding(12.dp) + ) + } + + SwipeToDismissBoxValue.Settled -> {} + } + } + } + ) { + content(value) + } +} +// [END android_compose_components_swipecarditem] + +// [START android_compose_components_swipecarditemexample] +@Preview +@Composable +private fun SwipeCardItemExample() { + val todoItems = remember { + mutableStateListOf( + TodoItem(isItemDone = false, itemDescription = "Pay bills"), + TodoItem(isItemDone = false, itemDescription = "Buy groceries"), + TodoItem(isItemDone = false, itemDescription = "Go to gym"), + TodoItem(isItemDone = false, itemDescription = "Get dinner") + ) + } + + LazyColumn { + items( + items = todoItems, + key = { it.itemDescription } + ) { todoItem -> + SwipeCardItem( + value = todoItem, + startToEndAction = { + todoItem.isItemDone = !todoItem.isItemDone + }, + endToStartAction = { + todoItems -= todoItem + } + ) { + OutlinedCard(shape = RectangleShape) { + ListItem( + headlineContent = { Text(todoItem.itemDescription) }, + supportingContent = { Text("swipe me to update or remove.") } + ) + } + } + } + } +} +// [END android_compose_components_swipecarditemexample] 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 dbfdece1..8d86b28b 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 @@ -50,5 +50,6 @@ enum class TopComponentsDestination(val route: String, val title: String) { TooltipExamples("tooltipExamples", "Tooltips"), NavigationDrawerExamples("navigationDrawerExamples", "Navigation drawer"), SegmentedButtonExamples("segmentedButtonExamples", "Segmented button"), + SwipeToDismissBoxExamples("swipeToDismissBoxExamples", "Swipe to dismiss box examples"), SearchBarExamples("searchBarExamples", "Search bar") } From 14b63840f52d4e6ed086455bbbe31dfa79e196a9 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 6 Jan 2025 11:05:41 +0000 Subject: [PATCH 4/5] Add example usage of keyframesWithSplines (#430) * Add example of keyframesWithSplines * Apply Spotless * PR Review comments. * PR Review comments. * Apply Spotless --------- Co-authored-by: riggaroo --- .../snippets/animations/AnimationSnippets.kt | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt index 38a1eb44..2fe06cf0 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/AnimationSnippets.kt @@ -26,9 +26,11 @@ import androidx.compose.animation.EnterExitState import androidx.compose.animation.SizeTransform import androidx.compose.animation.animateColor import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector2D import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.ExperimentalAnimationSpecApi import androidx.compose.animation.core.ExperimentalTransitionApi import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing @@ -43,11 +45,13 @@ import androidx.compose.animation.core.TwoWayConverter import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.animateRect import androidx.compose.animation.core.animateValueAsState import androidx.compose.animation.core.createChildTransition import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.keyframesWithSpline import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.repeatable @@ -71,11 +75,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints 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.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn @@ -93,25 +99,34 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.PointMode import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.round import com.example.compose.snippets.R import java.text.BreakIterator import java.text.StringCharacterIterator import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive /* * Copyright 2023 The Android Open Source Project @@ -709,6 +724,101 @@ private fun AnimationSpecKeyframe() { // [END android_compose_animations_spec_keyframe] } +@OptIn(ExperimentalAnimationSpecApi::class) +@Composable +private fun AnimationSpecKeyframeWithSpline() { + // [START android_compose_animation_spec_keyframes_with_spline] + val offset by animateOffsetAsState( + targetValue = Offset(300f, 300f), + animationSpec = keyframesWithSpline { + durationMillis = 6000 + Offset(0f, 0f) at 0 + Offset(150f, 200f) atFraction 0.5f + Offset(0f, 100f) atFraction 0.7f + } + ) + // [END android_compose_animation_spec_keyframes_with_spline] +} + +@OptIn(ExperimentalAnimationSpecApi::class) +@Preview +@Composable +private fun OffsetKeyframeWithSplineDemo() { + val points = remember { mutableStateListOf() } + val offsetAnim = remember { + Animatable( + Offset.Zero, + Offset.VectorConverter + ) + } + val density = LocalDensity.current + + BoxWithConstraints( + Modifier.fillMaxSize().drawBehind { + drawPoints( + points = points, + pointMode = PointMode.Lines, + color = Color.LightGray, + strokeWidth = 4f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(30f, 20f)) + ) + } + ) { + val minDimension = minOf(maxWidth, maxHeight) + val size = minDimension / 4 + + val sizePx = with(density) { size.toPx() } + val widthPx = with(density) { maxWidth.toPx() } + val heightPx = with(density) { maxHeight.toPx() } + + val maxXOff = (widthPx - sizePx) / 2f + val maxYOff = heightPx - (sizePx / 2f) + + Box( + Modifier.align(Alignment.TopCenter) + .offset { offsetAnim.value.round() } + .size(size) + .background(Color.Red, RoundedCornerShape(50)) + .onPlaced { points.add(it.boundsInParent().center) } + ) + + LaunchedEffect(Unit) { + delay(1000) + while (isActive) { + offsetAnim.animateTo( + targetValue = Offset.Zero, + animationSpec = + keyframesWithSpline { + durationMillis = 4400 + + // Increasingly approach the halfway point moving from side to side + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (i.toFloat() / 5f) * sign, + y = (maxYOff) * (i.toFloat() / 5f) + ) atFraction (0.1f * i) + } + + // Halfway point (at bottom of the screen) + Offset(0f, maxYOff) atFraction 0.5f + + // Return with mirrored movement + for (i in 0..4) { + val sign = if (i % 2 == 0) 1 else -1 + Offset( + x = maxXOff * (1f - i.toFloat() / 5f) * sign, + y = (maxYOff) * (1f - i.toFloat() / 5f) + ) atFraction ((0.1f * i) + 0.5f) + } + } + ) + points.clear() + } + } + } +} + @Composable private fun AnimationSpecRepeatable() { // [START android_compose_animations_spec_repeatable] From 716dfe18f62b17fd01cb479233416a12bc0c3012 Mon Sep 17 00:00:00 2001 From: Roberto Orgiu Date: Tue, 7 Jan 2025 16:18:52 +0100 Subject: [PATCH 5/5] Update Advanced Focus Snippets to remove Deprecations (#432) Co-authored-by: Rob Orgiu --- .../touchinput/focus/FocusSnippets.kt | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt index 82b5b2d1..2fce7f0a 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/touchinput/focus/FocusSnippets.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Android Open Source Project + * Copyright 2025 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. @@ -14,20 +14,17 @@ * limitations under the License. */ -@file:Suppress("DEPRECATION_ERROR") - package com.example.compose.snippets.touchinput.focus -import androidx.compose.foundation.Indication -import androidx.compose.foundation.IndicationInstance +import androidx.compose.foundation.IndicationNodeFactory import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -42,7 +39,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -70,9 +66,13 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.node.DelegatableNode +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch @Preview @Composable @@ -436,45 +436,64 @@ private fun ReactToFocus() { } // [START android_compose_touchinput_focus_advanced_cues] -private class MyHighlightIndicationInstance(isEnabledState: State) : - IndicationInstance { - private val isEnabled by isEnabledState - override fun ContentDrawScope.drawIndication() { +private class MyHighlightIndicationNode(private val interactionSource: InteractionSource) : + Modifier.Node(), DrawModifierNode { + private var isFocused = false + + override fun onAttach() { + coroutineScope.launch { + var focusCount = 0 + interactionSource.interactions.collect { interaction -> + when (interaction) { + is FocusInteraction.Focus -> focusCount++ + is FocusInteraction.Unfocus -> focusCount-- + } + val focused = focusCount > 0 + if (isFocused != focused) { + isFocused = focused + invalidateDraw() + } + } + } + } + + override fun ContentDrawScope.draw() { drawContent() - if (isEnabled) { + if (isFocused) { drawRect(size = size, color = Color.White, alpha = 0.2f) } } } + // [END android_compose_touchinput_focus_advanced_cues] // [START android_compose_touchinput_focus_indication] -class MyHighlightIndication : Indication { - @Composable - override fun rememberUpdatedInstance(interactionSource: InteractionSource): - IndicationInstance { - val isFocusedState = interactionSource.collectIsFocusedAsState() - return remember(interactionSource) { - MyHighlightIndicationInstance(isEnabledState = isFocusedState) - } +object MyHighlightIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode { + return MyHighlightIndicationNode(interactionSource) } + + override fun hashCode(): Int = -1 + + override fun equals(other: Any?) = other === this } // [END android_compose_touchinput_focus_indication] @Composable private fun ApplyIndication() { // [START android_compose_touchinput_focus_apply_indication] - val highlightIndication = remember { MyHighlightIndication() } var interactionSource = remember { MutableInteractionSource() } Card( modifier = Modifier .clickable( interactionSource = interactionSource, - indication = highlightIndication, + indication = MyHighlightIndication, enabled = true, onClick = { } ) - ) {} + ) { + Text("hello") + } // [END android_compose_touchinput_focus_apply_indication] }