diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 543a9545f..babb910f0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ google-maps = "19.2.0" gradle-versions = "0.52.0" guava = "33.4.8-jre" hilt = "2.56.2" -horologist = "0.6.23" +horologist = "0.7.10-alpha" junit = "4.13.2" kotlin = "2.1.20" kotlinCoroutinesOkhttp = "1.0" @@ -60,18 +60,19 @@ media3 = "1.6.1" minSdk = "21" okHttp = "4.12.0" playServicesWearable = "19.0.0" -protolayout = "1.2.1" +protolayout = "1.3.0-beta02" recyclerview = "1.4.0" # @keep androidx-xr-arcore = "1.0.0-alpha04" androidx-xr-scenecore = "1.0.0-alpha04" androidx-xr-compose = "1.0.0-alpha04" targetSdk = "34" -tiles = "1.4.1" +tiles = "1.5.0-beta01" version-catalog-update = "1.0.0" wear = "1.3.0" -wearComposeFoundation = "1.4.1" -wearComposeMaterial = "1.4.1" +wearComposeFoundation = "1.5.0-beta01" +wearComposeMaterial = "1.5.0-beta01" +wearComposeMaterial3 = "1.5.0-beta01" wearToolingPreview = "1.0.0" webkit = "1.13.0" @@ -135,6 +136,7 @@ androidx-paging-compose = { module = "androidx.paging:paging-compose", version.r androidx-protolayout = { module = "androidx.wear.protolayout:protolayout", version.ref = "protolayout" } androidx-protolayout-expression = { module = "androidx.wear.protolayout:protolayout-expression", version.ref = "protolayout" } androidx-protolayout-material = { module = "androidx.wear.protolayout:protolayout-material", version.ref = "protolayout" } +androidx-protolayout-material3 = { module = "androidx.wear.protolayout:protolayout-material3", version.ref = "protolayout" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup-runtime" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test" } @@ -159,7 +161,7 @@ androidx-xr-scenecore = { module = "androidx.xr.scenecore:scenecore", version.re appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } compose-foundation = { module = "androidx.wear.compose:compose-foundation", version.ref = "wearComposeFoundation" } -compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } +wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "composeUiTooling" } glide-compose = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } google-android-material = { module = "com.google.android.material:material", version.ref = "material" } @@ -179,6 +181,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp" } play-services-wearable = { module = "com.google.android.gms:play-services-wearable", version.ref = "playServicesWearable" } +wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 154e7d37e..888f3d434 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(libs.androidx.wear) implementation(libs.androidx.protolayout) implementation(libs.androidx.protolayout.material) + implementation(libs.androidx.protolayout.material3) implementation(libs.androidx.protolayout.expression) debugImplementation(libs.androidx.tiles.renderer) testImplementation(libs.androidx.tiles.testing) @@ -69,7 +70,8 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.compose.material) + implementation(libs.wear.compose.material) + implementation(libs.wear.compose.material3) implementation(libs.compose.foundation) implementation(libs.androidx.activity.compose) implementation(libs.androidx.core.splashscreen) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 84c4785a2..c2740b2dc 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ android:value="true" /> @@ -52,6 +52,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt b/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt new file mode 100644 index 000000000..52e9c2eb7 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/MainActivity.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2021 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 + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import com.example.wear.snippets.m3.list.ComposeList + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + WearApp() + } + } +} + +@Composable +fun WearApp() { + // insert here the snippet you want to test + ComposeList() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt b/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt new file mode 100644 index 000000000..29b0f8fd5 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/list/List.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2022 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.list + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.style.TextOverflow +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.Icon +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.layout.ColumnItemType +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults +import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState +import com.google.android.horologist.compose.material.Button +import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding +import com.google.android.horologist.compose.material.ResponsiveListHeader + +@Composable +fun ComposeList() { + // [START android_wear_list] + val columnState = rememberTransformingLazyColumnState() + val contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader, + last = ColumnItemType.Button, + ) + val transformationSpec = rememberTransformationSpec() + ScreenScaffold( + scrollState = columnState, + contentPadding = contentPadding + ) { contentPadding -> + TransformingLazyColumn( + state = columnState, + contentPadding = contentPadding + ) { + item { + ListHeader( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec) + ) { + Text(text = "Header") + } + } + // ... other items + item { + Button( + modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + onClick = { /* ... */ }, + icon = { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "build", + ) + }, + ) { + Text( + text = "Build", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + // [END android_wear_list] +} + +@OptIn(ExperimentalHorologistApi::class) +@Composable +fun SnapAndFlingComposeList() { + // [START android_wear_snap] + val columnState = rememberResponsiveColumnState( + // ... + // [START_EXCLUDE] + contentPadding = ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.SingleButton + ), + // [END_EXCLUDE] + rotaryMode = ScalingLazyColumnState.RotaryMode.Snap + ) + ScreenScaffold(scrollState = columnState) { + ScalingLazyColumn( + columnState = columnState + ) { + // ... + // [START_EXCLUDE] + item { + ResponsiveListHeader(contentPadding = firstItemPadding()) { + androidx.wear.compose.material.Text(text = "Header") + } + } + // ... other items + item { + Button( + imageVector = Icons.Default.Build, + contentDescription = "Example Button", + onClick = { } + ) + } + // [END_EXCLUDE] + } + } + // [END android_wear_snap] +} + +// [START android_wear_list_breakpoint] +const val LARGE_DISPLAY_BREAKPOINT = 225 + +@Composable +fun isLargeDisplay() = + LocalConfiguration.current.screenWidthDp >= LARGE_DISPLAY_BREAKPOINT + +// [START_EXCLUDE] +@Composable +fun breakpointDemo() { + // [END_EXCLUDE] +// ... use in your Composables: + if (isLargeDisplay()) { + // Show additional content. + } else { + // Show content only for smaller displays. + } + // [START_EXCLUDE] +} +// [END_EXCLUDE] +// [END android_wear_list_breakpoint] + +// [START android_wear_list_preview] +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun ComposeListPreview() { + ComposeList() +} +// [END android_wear_list_preview] + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun SnapAndFlingComposeListPreview() { + SnapAndFlingComposeList() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt b/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt new file mode 100644 index 000000000..283fa08ca --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/navigation/Navigation.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2022 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.navigation + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.Text +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable +import androidx.wear.compose.navigation.rememberSwipeDismissableNavController +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.example.wear.R +import com.google.android.horologist.compose.layout.ColumnItemType +import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding + +@Composable +fun navigation() { + // [START android_wear_navigation] + AppScaffold { + val navController = rememberSwipeDismissableNavController() + SwipeDismissableNavHost( + navController = navController, + startDestination = "message_list" + ) { + composable("message_list") { + MessageList(onMessageClick = { id -> + navController.navigate("message_detail/$id") + }) + } + composable("message_detail/{id}") { + MessageDetail(id = it.arguments?.getString("id")!!) + } + } + } + // [START_EXCLUDE] +} + +@Composable +fun MessageDetail(id: String) { + // [END_EXCLUDE] + // .. Screen level content goes here + val scrollState = rememberTransformingLazyColumnState() + + val padding = rememberResponsiveColumnPadding( + first = ColumnItemType.BodyText + ) + + ScreenScaffold( + scrollState = scrollState, + contentPadding = padding + ) { + // Screen content goes here + // [END android_wear_navigation] + TransformingLazyColumn(state = scrollState) { + item { + Text( + text = id, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +fun MessageList(onMessageClick: (String) -> Unit) { + val scrollState = rememberTransformingLazyColumnState() + + val padding = rememberResponsiveColumnPadding( + first = ColumnItemType.ListHeader, + last = ColumnItemType.Button + ) + + ScreenScaffold(scrollState = scrollState, contentPadding = padding) { contentPadding -> + TransformingLazyColumn( + state = scrollState, + contentPadding = contentPadding + ) { + item { + ListHeader() { + Text(text = stringResource(R.string.message_list)) + } + } + item { + Button( + onClick = { onMessageClick("message1") }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Message 1") + } + } + item { + Button( + onClick = { onMessageClick("message2") }, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "Message 2") + } + } + } + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun MessageDetailPreview() { + MessageDetail("test") +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun MessageListPreview() { + MessageList(onMessageClick = {}) +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt b/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt new file mode 100644 index 000000000..ac352c73b --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/rotary/Rotary.kt @@ -0,0 +1,233 @@ +/* + * 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.wear.snippets.m3.rotary + +import android.view.MotionEvent +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.input.rotary.onRotaryScrollEvent +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults +import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.MaterialTheme +import androidx.wear.compose.material3.Picker +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.ScrollIndicator +import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.rememberPickerState +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import kotlinx.coroutines.launch + +@Composable +fun TimePicker() { + val textStyle = MaterialTheme.typography.displayMedium + + // [START android_wear_rotary_input_picker] + var selectedColumn by remember { mutableIntStateOf(0) } + + val hoursFocusRequester = remember { FocusRequester() } + val minutesRequester = remember { FocusRequester() } + // [START_EXCLUDE] + val coroutineScope = rememberCoroutineScope() + + @Composable + fun Option(column: Int, text: String) = Box(modifier = Modifier.fillMaxSize()) { + Text( + text = text, style = textStyle, + color = if (selectedColumn == column) MaterialTheme.colorScheme.secondary + else MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .pointerInteropFilter { + if (it.action == MotionEvent.ACTION_DOWN) selectedColumn = column + true + } + ) + } + // [END_EXCLUDE] + ScreenScaffold(modifier = Modifier.fillMaxSize()) { + Row( + // [START_EXCLUDE] + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + // [END_EXCLUDE] + // ... + ) { + // [START_EXCLUDE] + val hourState = rememberPickerState( + initialNumberOfOptions = 12, + initiallySelectedIndex = 5 + ) + val hourContentDescription by remember { + derivedStateOf { "${hourState.selectedOptionIndex + 1 } hours" } + } + // [END_EXCLUDE] + Picker( + readOnly = selectedColumn != 0, + modifier = Modifier.size(64.dp, 100.dp) + .onRotaryScrollEvent { + coroutineScope.launch { + hourState.scrollBy(it.verticalScrollPixels) + } + true + } + .focusRequester(hoursFocusRequester) + .focusable(), + onSelected = { selectedColumn = 0 }, + // ... + // [START_EXCLUDE] + state = hourState, + contentDescription = { hourContentDescription }, + option = { hour: Int -> Option(0, "%2d".format(hour + 1)) } + // [END_EXCLUDE] + ) + // [START_EXCLUDE] + Spacer(Modifier.width(8.dp)) + Text(text = ":", style = textStyle, color = MaterialTheme.colorScheme.onBackground) + Spacer(Modifier.width(8.dp)) + val minuteState = + rememberPickerState(initialNumberOfOptions = 60, initiallySelectedIndex = 0) + val minuteContentDescription by remember { + derivedStateOf { "${minuteState.selectedOptionIndex} minutes" } + } + // [END_EXCLUDE] + Picker( + readOnly = selectedColumn != 1, + modifier = Modifier.size(64.dp, 100.dp) + .onRotaryScrollEvent { + coroutineScope.launch { + minuteState.scrollBy(it.verticalScrollPixels) + } + true + } + .focusRequester(minutesRequester) + .focusable(), + onSelected = { selectedColumn = 1 }, + // ... + // [START_EXCLUDE] + state = minuteState, + contentDescription = { minuteContentDescription }, + option = { minute: Int -> Option(1, "%02d".format(minute)) } + // [END_EXCLUDE] + ) + LaunchedEffect(selectedColumn) { + listOf( + hoursFocusRequester, + minutesRequester + )[selectedColumn] + .requestFocus() + } + } + } + // [END android_wear_rotary_input_picker] +} + +@Composable +fun SnapScrollableScreen() { + // This sample doesn't add a Time Text at the top of the screen. + // If using Time Text, add padding to ensure content does not overlap with Time Text. + // [START android_wear_rotary_input_snap_fling] + val listState = rememberScalingLazyListState() + ScreenScaffold( + scrollIndicator = { + ScrollIndicator(state = listState) + } + ) { + + val state = rememberScalingLazyListState() + ScalingLazyColumn( + modifier = Modifier.fillMaxWidth(), + state = state, + flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state) + ) { + // Content goes here + // [START_EXCLUDE] + item { ListHeader { Text(text = "List Header") } } + items(20) { + Button( + onClick = {}, + label = { Text("List item $it") }, + colors = ButtonDefaults.filledTonalButtonColors() + ) + } + // [END_EXCLUDE] + } + } + // [END android_wear_rotary_input_snap_fling] +} + +@Composable +fun PositionScrollIndicator() { + // [START android_wear_rotary_position_indicator] + val listState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollIndicator = { + ScrollIndicator(state = listState) + } + ) { + // ... + } + // [END android_wear_rotary_position_indicator] +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun TimePickerPreview() { + TimePicker() +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun SnapScrollableScreenPreview() { + SnapScrollableScreen() +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun PositionScrollIndicatorPreview() { + PositionScrollIndicator() +} diff --git a/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt b/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt new file mode 100644 index 000000000..2ad9812e2 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/tile/Tile.kt @@ -0,0 +1,60 @@ +/* + * 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. + * 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.tile + +import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.material3.Typography.BODY_LARGE +import androidx.wear.protolayout.material3.materialScope +import androidx.wear.protolayout.material3.primaryLayout +import androidx.wear.protolayout.material3.text +import androidx.wear.protolayout.types.layoutString +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.RequestBuilders.ResourcesRequest +import androidx.wear.tiles.TileBuilders.Tile +import androidx.wear.tiles.TileService +import com.google.common.util.concurrent.Futures + +private const val RESOURCES_VERSION = "1" + +// [START android_wear_m3_tile_mytileservice] +class MyTileService : TileService() { + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + materialScope(this, requestParams.deviceConfiguration) { + primaryLayout( + mainSlot = { + text("Hello, World!".layoutString, typography = BODY_LARGE) + } + ) + } + ) + ) + .build() + ) + + override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture( + Resources.Builder().setVersion(RESOURCES_VERSION).build() + ) +} +// [END android_wear_m3_tile_mytileservice] diff --git a/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt b/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt new file mode 100644 index 000000000..d926487c5 --- /dev/null +++ b/wear/src/main/java/com.example.wear.snippets.m3/voiceinput/VoiceInputScreen.kt @@ -0,0 +1,109 @@ +/* + * 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.wear.snippets.m3.voiceinput + +import android.content.Intent +import android.speech.RecognizerIntent +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.fillMaxWidth +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.wear.compose.foundation.lazy.TransformingLazyColumn +import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.Text +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales +import com.example.wear.R +import com.google.android.horologist.compose.layout.ColumnItemType +import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding + +/** + * Shows voice input option + */ +@Composable +fun VoiceInputScreen() { + AppScaffold { + // [START android_wear_voice_input] + var textForVoiceInput by remember { mutableStateOf("") } + + val voiceLauncher = + rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { activityResult -> + // This is where you process the intent and extract the speech text from the intent. + activityResult.data?.let { data -> + val results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + textForVoiceInput = results?.get(0) ?: "None" + } + } + + val scrollState = rememberTransformingLazyColumnState() + ScreenScaffold( + scrollState = scrollState, + contentPadding = rememberResponsiveColumnPadding( + first = ColumnItemType.Button + ) + ) { contentPadding -> + TransformingLazyColumn( + contentPadding = contentPadding, + state = scrollState, + ) { + item { + // Create an intent that can start the Speech Recognizer activity + val voiceIntent: Intent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + + putExtra( + RecognizerIntent.EXTRA_PROMPT, + stringResource(R.string.voice_text_entry_label) + ) + } + // Invoke the process from a Button + Button( + onClick = { + voiceLauncher.launch(voiceIntent) + }, + label = { Text(stringResource(R.string.voice_input_label)) }, + secondaryLabel = { Text(textForVoiceInput) }, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + // [END android_wear_voice_input] + } +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun VoiceInputScreenPreview() { + VoiceInputScreen() +} diff --git a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt index 42507078c..ed75220ab 100644 --- a/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt +++ b/wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt @@ -26,6 +26,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.wear.compose.foundation.rememberActiveFocusRequester +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults.behavior +import androidx.wear.compose.foundation.rotary.rotaryScrollable import androidx.wear.compose.material.Text import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.composable @@ -43,7 +46,6 @@ import com.google.android.horologist.compose.layout.rememberResponsiveColumnStat import com.google.android.horologist.compose.material.Chip import com.google.android.horologist.compose.material.ListHeaderDefaults.firstItemPadding import com.google.android.horologist.compose.material.ResponsiveListHeader -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll @Composable fun navigation() { @@ -81,12 +83,16 @@ fun MessageDetail(id: String) { first = ItemType.Text, last = ItemType.Text )() + val focusRequester = rememberActiveFocusRequester() Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) - .rotaryWithScroll(scrollState) - .padding(padding), + .padding(padding) + .rotaryScrollable( + behavior = behavior(scrollableState = scrollState), + focusRequester = focusRequester, + ), verticalArrangement = Arrangement.Center ) { Text( diff --git a/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt b/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt index fa80ab800..31bbde0e9 100644 --- a/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt +++ b/wear/src/main/java/com/example/wear/snippets/voiceinput/VoiceInputScreen.kt @@ -49,6 +49,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.wear.compose.foundation.rememberActiveFocusRequester +import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults.behavior +import androidx.wear.compose.foundation.rotary.rotaryScrollable import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales import com.example.wear.R @@ -58,7 +61,6 @@ import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.ItemType import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.material.Chip -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll /** * Shows voice input option @@ -90,6 +92,7 @@ fun VoiceInputScreen() { first = ItemType.Text, last = ItemType.Chip )() + val focusRequester = rememberActiveFocusRequester() // [END_EXCLUDE] Column( // rest of implementation here @@ -97,8 +100,11 @@ fun VoiceInputScreen() { modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) - .rotaryWithScroll(scrollState) - .padding(padding), + .padding(padding) + .rotaryScrollable( + behavior = behavior(scrollableState = scrollState), + focusRequester = focusRequester, + ), verticalArrangement = Arrangement.Center ) { // [END_EXCLUDE] diff --git a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt index f60d9ecfe..460d35db2 100644 --- a/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt +++ b/xr/src/main/java/com/example/xr/scenecore/SpatialVideo.kt @@ -104,4 +104,4 @@ private fun ComponentActivity.surfaceEntityCreateMVHEVC(xrSession: Session) { exoPlayer.prepare() exoPlayer.play() // [END androidxr_scenecore_surfaceEntityCreateMVHEVC] -} \ No newline at end of file +}