Skip to content

Commit 184d434

Browse files
committed
Adds sample for rotary and timepicker
1 parent fcc7775 commit 184d434

File tree

4 files changed

+294
-62
lines changed

4 files changed

+294
-62
lines changed

wear/src/main/java/com/example/wear/snippets/MainActivity.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ import android.os.Bundle
2020
import androidx.activity.ComponentActivity
2121
import androidx.activity.compose.setContent
2222
import androidx.compose.runtime.Composable
23-
import com.example.wear.snippets.navigation.navigation
24-
import com.example.wear.snippets.voiceinput.VoiceInputScreen
23+
import com.example.wear.snippets.rotary.TimePicker
2524
import com.google.android.horologist.annotations.ExperimentalHorologistApi
26-
import com.google.android.horologist.compose.layout.AppScaffold
2725

2826
class MainActivity : ComponentActivity() {
2927
override fun onCreate(savedInstanceState: Bundle?) {
@@ -41,7 +39,6 @@ class MainActivity : ComponentActivity() {
4139
@OptIn(ExperimentalHorologistApi::class)
4240
@Composable
4341
fun WearApp() {
44-
AppScaffold {
45-
navigation()
46-
}
42+
// insert here the snippet you want to test
43+
TimePicker()
4744
}

wear/src/main/java/com/example/wear/snippets/navigation/Navigation.kt

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
3434
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
3535
import com.example.wear.R
3636
import com.google.android.horologist.annotations.ExperimentalHorologistApi
37+
import com.google.android.horologist.compose.layout.AppScaffold
3738
import com.google.android.horologist.compose.layout.ScalingLazyColumn
3839
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
3940
import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.ItemType
@@ -44,8 +45,9 @@ import com.google.android.horologist.compose.material.ListHeaderDefaults.firstIt
4445
import com.google.android.horologist.compose.material.ResponsiveListHeader
4546
import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
4647

47-
@Composable
48-
fun navigation() {
48+
@Composable
49+
fun navigation() {
50+
AppScaffold {
4951
// [START android_wear_navigation]
5052
val navController = rememberSwipeDismissableNavController()
5153
SwipeDismissableNavHost(
@@ -63,10 +65,11 @@ import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
6365
}
6466
// [END android_wear_navigation]
6567
}
68+
}
6669

6770
@OptIn(ExperimentalHorologistApi::class)
6871
@Composable
69-
fun MessageDetail(id: String){
72+
fun MessageDetail(id: String) {
7073
val scrollState = rememberScrollState()
7174

7275
ScreenScaffold(scrollState = scrollState) {
@@ -82,16 +85,18 @@ fun MessageDetail(id: String){
8285
.padding(padding),
8386
verticalArrangement = Arrangement.Center
8487
) {
85-
Text(text= id,
88+
Text(
89+
text = id,
8690
textAlign = TextAlign.Center,
87-
modifier = Modifier.fillMaxSize())
91+
modifier = Modifier.fillMaxSize()
92+
)
8893
}
8994
}
9095
}
9196

9297
@OptIn(ExperimentalHorologistApi::class)
9398
@Composable
94-
fun MessageList(onMessageClick: (String) -> Unit){
99+
fun MessageList(onMessageClick: (String) -> Unit) {
95100
val columnState = rememberResponsiveColumnState(
96101
contentPadding = ScalingLazyColumnDefaults.padding(
97102
first = ItemType.Text,
@@ -111,10 +116,10 @@ fun MessageList(onMessageClick: (String) -> Unit){
111116
}
112117
}
113118
item {
114-
Chip(label = "Message 1", onClick = { onMessageClick("message1")})
119+
Chip(label = "Message 1", onClick = { onMessageClick("message1") })
115120
}
116121
item {
117-
Chip(label = "Message 2", onClick = { onMessageClick("message2")})
122+
Chip(label = "Message 2", onClick = { onMessageClick("message2") })
118123
}
119124
}
120125
}
@@ -133,4 +138,3 @@ fun MessageDetailPreview() {
133138
fun MessageListPreview() {
134139
MessageList(onMessageClick = {})
135140
}
136-
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.wear.snippets.rotary
18+
19+
import android.view.MotionEvent
20+
import androidx.compose.foundation.focusable
21+
import androidx.compose.foundation.gestures.animateScrollBy
22+
import androidx.compose.foundation.gestures.scrollBy
23+
import androidx.compose.foundation.layout.Arrangement
24+
import androidx.compose.foundation.layout.Box
25+
import androidx.compose.foundation.layout.Row
26+
import androidx.compose.foundation.layout.Spacer
27+
import androidx.compose.foundation.layout.fillMaxSize
28+
import androidx.compose.foundation.layout.size
29+
import androidx.compose.foundation.layout.width
30+
import androidx.compose.runtime.Composable
31+
import androidx.compose.runtime.LaunchedEffect
32+
import androidx.compose.runtime.derivedStateOf
33+
import androidx.compose.runtime.getValue
34+
import androidx.compose.runtime.mutableIntStateOf
35+
import androidx.compose.runtime.remember
36+
import androidx.compose.runtime.rememberCoroutineScope
37+
import androidx.compose.runtime.setValue
38+
import androidx.compose.ui.Alignment
39+
import androidx.compose.ui.ExperimentalComposeUiApi
40+
import androidx.compose.ui.Modifier
41+
import androidx.compose.ui.focus.FocusRequester
42+
import androidx.compose.ui.focus.focusRequester
43+
import androidx.compose.ui.input.pointer.pointerInteropFilter
44+
import androidx.compose.ui.input.rotary.onRotaryScrollEvent
45+
import androidx.compose.ui.unit.dp
46+
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
47+
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
48+
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
49+
import androidx.wear.compose.foundation.rememberActiveFocusRequester
50+
import androidx.wear.compose.material.Chip
51+
import androidx.wear.compose.material.MaterialTheme
52+
import androidx.wear.compose.material.Picker
53+
import androidx.wear.compose.material.PositionIndicator
54+
import androidx.wear.compose.material.Scaffold
55+
import androidx.wear.compose.material.Text
56+
import androidx.wear.compose.material.rememberPickerState
57+
import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices
58+
import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales
59+
import kotlinx.coroutines.launch
60+
61+
@OptIn(ExperimentalWearFoundationApi::class)
62+
@Composable
63+
fun ScrollableScreen() {
64+
// This sample doesn't add a Time Text at the top of the screen.
65+
// If using Time Text, add padding to ensure content does not overlap with Time Text.
66+
// [START android_wear_rotary_input]
67+
val listState = rememberScalingLazyListState()
68+
Scaffold(
69+
positionIndicator = {
70+
PositionIndicator(scalingLazyListState = listState)
71+
}
72+
) {
73+
74+
val focusRequester = rememberActiveFocusRequester()
75+
val coroutineScope = rememberCoroutineScope()
76+
77+
ScalingLazyColumn(
78+
modifier = Modifier
79+
.onRotaryScrollEvent {
80+
coroutineScope.launch {
81+
listState.scrollBy(it.verticalScrollPixels)
82+
listState.animateScrollBy(0f)
83+
}
84+
true
85+
}
86+
.focusRequester(focusRequester)
87+
.focusable()
88+
.fillMaxSize(),
89+
state = listState
90+
) {
91+
// Content goes here
92+
// [START_EXCLUDE]
93+
items(count = 5) {
94+
Chip(onClick = { }, label = { Text("Item #$it") })
95+
}
96+
// [END_EXCLUDE]
97+
}
98+
}
99+
// [END android_wear_rotary_input]
100+
}
101+
102+
@OptIn(ExperimentalComposeUiApi::class)
103+
@Composable
104+
fun TimePicker() {
105+
val textStyle = MaterialTheme.typography.display1
106+
107+
// [START android_wear_rotary_input_picker]
108+
var selectedColumn by remember { mutableIntStateOf(0) }
109+
110+
val hoursFocusRequester = remember { FocusRequester() }
111+
val minutesRequester = remember { FocusRequester() }
112+
// [START_EXCLUDE]
113+
val coroutineScope = rememberCoroutineScope()
114+
115+
@Composable
116+
fun Option(column: Int, text: String) = Box(modifier = Modifier.fillMaxSize()) {
117+
Text(
118+
text = text, style = textStyle,
119+
color = if (selectedColumn == column) MaterialTheme.colors.secondary
120+
else MaterialTheme.colors.onBackground,
121+
modifier = Modifier
122+
.pointerInteropFilter {
123+
if (it.action == MotionEvent.ACTION_DOWN) selectedColumn = column
124+
true
125+
}
126+
)
127+
}
128+
// [END_EXCLUDE]
129+
Scaffold(modifier = Modifier.fillMaxSize()) {
130+
Row(
131+
// [START_EXCLUDE]
132+
modifier = Modifier.fillMaxSize(),
133+
verticalAlignment = Alignment.CenterVertically,
134+
horizontalArrangement = Arrangement.Center,
135+
// [END_EXCLUDE]
136+
// ...
137+
) {
138+
// [START_EXCLUDE]
139+
val hourState = rememberPickerState(
140+
initialNumberOfOptions = 12,
141+
initiallySelectedOption = 5
142+
)
143+
val hourContentDescription by remember {
144+
derivedStateOf { "${hourState.selectedOption + 1 } hours" }
145+
}
146+
// [END_EXCLUDE]
147+
Picker(
148+
readOnly = selectedColumn != 0,
149+
modifier = Modifier.size(64.dp, 100.dp)
150+
.onRotaryScrollEvent {
151+
coroutineScope.launch {
152+
hourState.scrollBy(it.verticalScrollPixels)
153+
}
154+
true
155+
}
156+
.focusRequester(hoursFocusRequester)
157+
.focusable(),
158+
onSelected = { selectedColumn = 0 },
159+
// ...
160+
// [START_EXCLUDE]
161+
state = hourState,
162+
contentDescription = hourContentDescription,
163+
option = { hour: Int -> Option(0, "%2d".format(hour + 1)) }
164+
// [END_EXCLUDE]
165+
)
166+
// [START_EXCLUDE]
167+
Spacer(Modifier.width(8.dp))
168+
Text(text = ":", style = textStyle, color = MaterialTheme.colors.onBackground)
169+
Spacer(Modifier.width(8.dp))
170+
val minuteState =
171+
rememberPickerState(initialNumberOfOptions = 60, initiallySelectedOption = 0)
172+
val minuteContentDescription by remember {
173+
derivedStateOf { "${minuteState.selectedOption} minutes" }
174+
}
175+
// [END_EXCLUDE]
176+
Picker(
177+
readOnly = selectedColumn != 1,
178+
modifier = Modifier.size(64.dp, 100.dp)
179+
.onRotaryScrollEvent {
180+
coroutineScope.launch {
181+
minuteState.scrollBy(it.verticalScrollPixels)
182+
}
183+
true
184+
}
185+
.focusRequester(minutesRequester)
186+
.focusable(),
187+
onSelected = { selectedColumn = 1 },
188+
// ...
189+
// [START_EXCLUDE]
190+
state = minuteState,
191+
contentDescription = minuteContentDescription,
192+
option = { minute: Int -> Option(1, "%02d".format(minute)) }
193+
// [END_EXCLUDE]
194+
)
195+
LaunchedEffect(selectedColumn) {
196+
listOf(
197+
hoursFocusRequester,
198+
minutesRequester
199+
)[selectedColumn]
200+
.requestFocus()
201+
}
202+
}
203+
}
204+
// [END android_wear_rotary_input_picker]
205+
}
206+
207+
@WearPreviewDevices
208+
@WearPreviewFontScales
209+
@Composable
210+
fun ScrollableScreenPreview() {
211+
ScrollableScreen()
212+
}
213+
214+
@WearPreviewDevices
215+
@WearPreviewFontScales
216+
@Composable
217+
fun TimePickerPreview() {
218+
TimePicker()
219+
}

0 commit comments

Comments
 (0)