From 982f669a1f75ddfe482da755ce50629605d9d61a Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Mon, 10 Aug 2026 22:50:32 +0300 Subject: [PATCH 1/2] feat(wear): add One-Handed Gestures demo snippet (#1001) --- settings.gradle.kts | 2 + wear/build.gradle.kts | 1 + wear/snapshot/build.gradle.kts | 47 +++ .../snippets/m3/gestures/OneHandedGestures.kt | 268 ++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 wear/snapshot/build.gradle.kts create mode 100644 wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c6fd64b0..6e0fe7132 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,7 @@ dependencyResolutionManagement { includeGroup("com.github.xgouchet") } } + maven { url = uri("https://androidx.dev/snapshots/latest/artifacts/repository/") } google() mavenCentral() } @@ -43,6 +44,7 @@ include( ":camerax", ":watchface", ":wear", + ":wear:snapshot", ":wearcompanion", ":views", ":misc", diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 10635fb1f..e3a10c45e 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -98,6 +98,7 @@ dependencies { implementation(libs.wear.compose.material) implementation(libs.wear.compose.material3) implementation(libs.compose.foundation) + implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) implementation(libs.horologist.compose.layout) diff --git a/wear/snapshot/build.gradle.kts b/wear/snapshot/build.gradle.kts new file mode 100644 index 000000000..9839c3215 --- /dev/null +++ b/wear/snapshot/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.compose.compiler) +} + +kotlin { + jvmToolchain(21) + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + } +} + +android { + namespace = "com.example.wear.snippet.snapshot" + compileSdk = 37 + + defaultConfig { + minSdk = 33 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.wear.tooling.preview) + + // Snapshot dependencies required for OneHandedGestures; expected to go away in the first 1.7.0 beta release. + implementation("androidx.wear.compose:compose-material3:1.7.0-SNAPSHOT") + implementation("androidx.compose.foundation:foundation:1.7.0-SNAPSHOT") + implementation(libs.wear.compose.material) + implementation(libs.compose.ui.tooling) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.activity.compose) + implementation(libs.horologist.compose.layout) + implementation(libs.horologist.compose.material) +} diff --git a/wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt b/wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt new file mode 100644 index 000000000..07a30007c --- /dev/null +++ b/wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt @@ -0,0 +1,268 @@ +/* + * Copyright 2026 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.wear.snippets.m3.gestures + +/* + * NOTE: One-Handed Gesture APIs require Wear Compose Material 3 1.7.0-SNAPSHOT (expected to go away in the first 1.7.0 beta release). + * Published alpha releases up to 1.7.0-alpha07 used older API names (GestureAction/GesturePriority) and will not compile with these snippets. + */ + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.EdgeButton +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureAction +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureClickIndicatorState +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureDefaults +import androidx.wear.compose.material3.onehandedgesture.OneHandedGesturePriority +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicator +import androidx.wear.compose.material3.onehandedgesture.OneHandedGestureScrollIndicatorState +import androidx.wear.compose.material3.onehandedgesture.oneHandedGesture +import androidx.wear.compose.material3.onehandedgesture.rememberOneHandedGestureConfiguration +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import kotlinx.coroutines.launch + +@Composable +fun OneHandedGestureScreen(modifier: Modifier = Modifier) { + var isPlaying by remember { mutableStateOf(false) } + val onClick = remember { { isPlaying = !isPlaying } } + + val scrollState = rememberTransformingLazyColumnState() + + val scrollGestureConfig = rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable + ) + val scrollIndicatorState = remember(scrollGestureConfig) { OneHandedGestureScrollIndicatorState() } + + val buttonGestureConfig = rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Clickable + ) + val buttonIndicatorState = remember { OneHandedGestureClickIndicatorState() } + val buttonInteractionSource = remember { MutableInteractionSource() } + val coroutineScope = rememberCoroutineScope() + + ScreenScaffold( + scrollState = scrollState, + edgeButton = { + EdgeButton( + onClick = onClick, + interactionSource = buttonInteractionSource, + modifier = + if (scrollState.canScrollForward) { + Modifier + } else { + Modifier.oneHandedGesture( + gestureConfiguration = buttonGestureConfig, + interactionSource = buttonInteractionSource, + onGestureLabel = if (isPlaying) "pause" else "play", + onGestureAvailable = { + coroutineScope.launch { buttonIndicatorState.showIndicator() } + }, + onGesture = onClick + ) + } then + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical, + reverseDirection = true, + overscrollEffect = rememberOverscrollEffect() + ) + ) { + OneHandedGestureClickIndicator( + gestureConfiguration = buttonGestureConfig, + state = buttonIndicatorState + ) { + Text(if (isPlaying) "Pause" else "Play") + } + } + }, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = scrollGestureConfig, + indicatorState = scrollIndicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd) + ) + } + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = modifier + .fillMaxSize() + .oneHandedGesture( + gestureConfiguration = scrollGestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { scrollIndicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDownToNextItem(scrollState) } + ) + ) { + item { + ListHeader { + Text("Gestures Demo") + } + } + + // Scrollable Items to demonstrate gesture scrolling + items(10) { index -> + Text("Item $index", modifier = Modifier.padding(8.dp)) + } + } + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun OneHandedGestureScreenPreview() { + OneHandedGestureScreen() +} + +@Composable +private fun ButtonGestureSnippet() { + // [START android_wear_one_handed_gesture_button] + var isPlaying by remember { mutableStateOf(false) } + val onClick = { isPlaying = !isPlaying } + + val gestureConfig = rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary + ) + // Share MutableInteractionSource so gesture events emit visual press state on the button + val interactionSource = remember { MutableInteractionSource() } + + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + interactionSource = interactionSource, + onGestureLabel = if (isPlaying) "pause" else "play", + onGesture = onClick + ) + ) { + Text(if (isPlaying) "Pause" else "Play") + } + // [END android_wear_one_handed_gesture_button] +} + +@Composable +fun ButtonGestureHintSnippet() { + // [START android_wear_one_handed_gesture_button_hint] + var isPlaying by remember { mutableStateOf(false) } + val onClick = { isPlaying = !isPlaying } + + val gestureConfig = rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary + ) + val indicatorState = remember { OneHandedGestureClickIndicatorState() } + val coroutineScope = rememberCoroutineScope() + val interactionSource = remember { MutableInteractionSource() } + + Button( + onClick = onClick, + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + interactionSource = interactionSource, + onGestureLabel = if (isPlaying) "pause" else "play", + onGestureAvailable = { coroutineScope.launch { indicatorState.showIndicator() } }, + onGesture = onClick + ) + ) { + OneHandedGestureClickIndicator( + gestureConfiguration = gestureConfig, + state = indicatorState + ) { + Text(if (isPlaying) "Pause" else "Play", modifier = Modifier.fillMaxWidth()) + } + } + // [END android_wear_one_handed_gesture_button_hint] +} + +@Composable +fun ScrollGestureHintSnippet() { + // [START android_wear_one_handed_gesture_scroll_hint] + val scrollState = rememberTransformingLazyColumnState() + val gestureConfig = rememberOneHandedGestureConfiguration( + action = OneHandedGestureAction.Primary, + priority = OneHandedGesturePriority.Scrollable + ) + val indicatorState = remember(gestureConfig) { OneHandedGestureScrollIndicatorState() } + val coroutineScope = rememberCoroutineScope() + + ScreenScaffold( + scrollState = scrollState, + scrollIndicator = { + OneHandedGestureScrollIndicator( + gestureConfiguration = gestureConfig, + indicatorState = indicatorState, + scrollState = scrollState, + modifier = Modifier.align(Alignment.CenterEnd) + ) + } + ) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding, + modifier = Modifier + .fillMaxSize() + .oneHandedGesture( + gestureConfiguration = gestureConfig, + onGestureLabel = "scroll", + onGestureAvailable = { + coroutineScope.launch { indicatorState.showIndicator() } + }, + onGesture = { OneHandedGestureDefaults.scrollDown(scrollState) } + ) + ) { + items(10) { index -> + Text("Item $index", modifier = Modifier.padding(8.dp)) + } + } + } + // [END android_wear_one_handed_gesture_scroll_hint] +} + From 6f74bb02cce2d447a18b91067386d043e38fc7bf Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Thu, 13 Aug 2026 15:47:20 +0100 Subject: [PATCH 2/2] feat(wear): migrate gestures to 1.7.0-beta01 Migrate Wear OS Material 3 One-Handed Gesture snippets from SNAPSHOT dependencies to the published 1.7.0-beta01 Jetpack release. - Move OneHandedGestures.kt into the :wear module - Remove the temporary :wear:snapshot subproject and snapshot Maven repository from settings.gradle.kts - Update wearCompose version catalog entries to 1.7.0-beta01 --- gradle/libs.versions.toml | 10 ++-- settings.gradle.kts | 2 - wear/snapshot/build.gradle.kts | 47 ------------------- .../snippets/m3/gestures/OneHandedGestures.kt | 12 ++--- 4 files changed, 8 insertions(+), 63 deletions(-) delete mode 100644 wear/snapshot/build.gradle.kts rename wear/{snapshot => }/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt (97%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e6ed8751b..64b615f11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ coil = "2.7.0" compileSdk = "37" compose-latest = "1.12.0" compose-remote = "1.0.0-alpha17" -composeUiTooling = "1.6.2" +composeUiTooling = "1.7.0-beta01" coreSplashscreen = "1.2.0" coroutines = "1.11.0" crossdeviceprompt = "0.0.1" @@ -117,10 +117,10 @@ validatorPush = "1.1.0-alpha01" version-catalog-update = "1.1.1" watchfaceComplicationsDataSourceKtx = "1.3.0" wear = "1.4.0" -wearComposeFoundation = "1.6.2" -wearComposeMaterial = "1.6.2" -wearComposeMaterial3 = "1.6.2" -wearComposeNavigation3 = "1.6.2" +wearComposeFoundation = "1.7.0-beta01" +wearComposeMaterial = "1.7.0-beta01" +wearComposeMaterial3 = "1.7.0-beta01" +wearComposeNavigation3 = "1.7.0-beta01" wearInput = "1.2.0" wearOngoing = "1.1.0" wearPhoneInteractions = "1.1.1" diff --git a/settings.gradle.kts b/settings.gradle.kts index 6e0fe7132..2c6fd64b0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,7 +23,6 @@ dependencyResolutionManagement { includeGroup("com.github.xgouchet") } } - maven { url = uri("https://androidx.dev/snapshots/latest/artifacts/repository/") } google() mavenCentral() } @@ -44,7 +43,6 @@ include( ":camerax", ":watchface", ":wear", - ":wear:snapshot", ":wearcompanion", ":views", ":misc", diff --git a/wear/snapshot/build.gradle.kts b/wear/snapshot/build.gradle.kts deleted file mode 100644 index 9839c3215..000000000 --- a/wear/snapshot/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.compose.compiler) -} - -kotlin { - jvmToolchain(21) - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) - } -} - -android { - namespace = "com.example.wear.snippet.snapshot" - compileSdk = 37 - - defaultConfig { - minSdk = 33 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - - buildFeatures { - compose = true - } -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.wear.tooling.preview) - - // Snapshot dependencies required for OneHandedGestures; expected to go away in the first 1.7.0 beta release. - implementation("androidx.wear.compose:compose-material3:1.7.0-SNAPSHOT") - implementation("androidx.compose.foundation:foundation:1.7.0-SNAPSHOT") - implementation(libs.wear.compose.material) - implementation(libs.compose.ui.tooling) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.activity.compose) - implementation(libs.horologist.compose.layout) - implementation(libs.horologist.compose.material) -} diff --git a/wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt b/wear/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt similarity index 97% rename from wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt rename to wear/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt index 07a30007c..211c25de3 100644 --- a/wear/snapshot/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt +++ b/wear/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt @@ -16,15 +16,13 @@ package com.example.wear.snippets.m3.gestures -/* - * NOTE: One-Handed Gesture APIs require Wear Compose Material 3 1.7.0-SNAPSHOT (expected to go away in the first 1.7.0 beta release). - * Published alpha releases up to 1.7.0-alpha07 used older API names (GestureAction/GesturePriority) and will not compile with these snippets. - */ - +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,9 +31,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState @@ -265,4 +260,3 @@ fun ScrollGestureHintSnippet() { } // [END android_wear_one_handed_gesture_scroll_hint] } -