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/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/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt b/wear/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt new file mode 100644 index 000000000..211c25de3 --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/m3/gestures/OneHandedGestures.kt @@ -0,0 +1,262 @@ +/* + * 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 + +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 +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.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] +}