From a8f0fa7c6520a615b1b187d35cb95a73c0795f49 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Tue, 19 Dec 2023 13:09:43 +0000 Subject: [PATCH] App/Screen/Page Scaffold (#1886) --------- Co-authored-by: yschimke --- .../android/horologist/auth/sample/WearApp.kt | 4 +- .../horologist/composables/DatePicker.kt | 303 +++++----- .../horologist/composables/TimePicker.kt | 8 +- compose-layout/api/current.api | 24 +- .../horologist/compose/layout/AppScaffold.kt | 61 ++ .../horologist/compose/layout/PageScaffold.kt | 44 ++ .../compose/layout/ScaffoldState.kt | 81 +++ .../compose/layout/ScalingLazyColumnState.kt | 20 +- .../compose/layout/ScreenScaffold.kt | 100 ++++ .../horologist/compose/layout/ScrollAway.kt | 277 +++++---- .../navscaffold/NavScaffoldViewModel.kt | 2 +- .../compose/navscaffold/WearNavScaffold.kt | 29 +- .../compose/navscaffold/NavScaffoldTest.kt | 1 + docs/compose-layout.md | 73 ++- .../mediasample/ui/app/UampWearApp.kt | 227 ++++---- media/ui/api/current.api | 4 +- .../ui/navigation/MediaPlayerScaffold.kt | 212 ++++--- .../PlayerLibraryPagerScreen.kt | 26 +- .../horologist/navsample/FillerScreen.kt | 15 +- .../horologist/navsample/NavMenuScreen.kt | 21 +- .../horologist/navsample/NavWearApp.kt | 158 ++--- .../horologist/sample/SampleWearApp.kt | 544 ++++++++++-------- .../screensizes/MediaPlayerLibraryTest.kt | 2 +- 23 files changed, 1377 insertions(+), 859 deletions(-) create mode 100644 compose-layout/src/main/java/com/google/android/horologist/compose/layout/AppScaffold.kt create mode 100644 compose-layout/src/main/java/com/google/android/horologist/compose/layout/PageScaffold.kt create mode 100644 compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScaffoldState.kt create mode 100644 compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScreenScaffold.kt diff --git a/auth/sample/wear/src/main/java/com/google/android/horologist/auth/sample/WearApp.kt b/auth/sample/wear/src/main/java/com/google/android/horologist/auth/sample/WearApp.kt index 277f2fa95a..9ce34265f3 100644 --- a/auth/sample/wear/src/main/java/com/google/android/horologist/auth/sample/WearApp.kt +++ b/auth/sample/wear/src/main/java/com/google/android/horologist/auth/sample/WearApp.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController +import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.ui.tooling.preview.WearPreviewSmallRound import com.google.android.horologist.auth.data.watch.oauth.common.impl.google.api.DeviceCodeResponse @@ -44,7 +45,6 @@ import com.google.android.horologist.auth.sample.screens.tokenshare.defaultkey.T import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInScreen import com.google.android.horologist.auth.ui.oauth.devicegrant.signin.DeviceGrantSignInScreen import com.google.android.horologist.auth.ui.oauth.pkce.signin.PKCESignInScreen -import com.google.android.horologist.compose.navscaffold.WearNavScaffold import com.google.android.horologist.compose.navscaffold.composable import com.google.android.horologist.compose.navscaffold.scrollable @@ -53,7 +53,7 @@ fun WearApp( modifier: Modifier = Modifier, navController: NavHostController = rememberSwipeDismissableNavController(), ) { - WearNavScaffold(startDestination = Screen.MainScreen.route, navController = navController) { + SwipeDismissableNavHost(startDestination = Screen.MainScreen.route, navController = navController) { scrollable( route = Screen.MainScreen.route, ) { diff --git a/composables/src/main/java/com/google/android/horologist/composables/DatePicker.kt b/composables/src/main/java/com/google/android/horologist/composables/DatePicker.kt index 2e14873cdd..89f34b79b0 100644 --- a/composables/src/main/java/com/google/android/horologist/composables/DatePicker.kt +++ b/composables/src/main/java/com/google/android/horologist/composables/DatePicker.kt @@ -57,6 +57,7 @@ import com.google.android.horologist.composables.picker.PickerGroup import com.google.android.horologist.composables.picker.PickerGroupState import com.google.android.horologist.composables.picker.PickerState import com.google.android.horologist.composables.picker.rememberPickerGroupState +import com.google.android.horologist.compose.layout.ScreenScaffold import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAdjusters @@ -175,174 +176,180 @@ public fun DatePicker( } } - BoxWithConstraints( + ScreenScaffold( modifier = modifier .fillMaxSize() .alpha(fullyDrawn.value), + timeText = {}, ) { - val boxConstraints = this - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + BoxWithConstraints( + modifier = modifier + .fillMaxSize(), ) { - Spacer(Modifier.height(16.dp)) - Text( - text = when (FocusableElementDatePicker[pickerGroupState.selectedIndex]) { - FocusableElementDatePicker.DAY -> dayString - FocusableElementDatePicker.MONTH -> monthString - FocusableElementDatePicker.YEAR -> yearString - else -> "" - }, - color = optionColor, - style = MaterialTheme.typography.button, - maxLines = 1, - ) - val weightsToCenterVertically = 0.5f - Spacer( - Modifier - .fillMaxWidth() - .weight(weightsToCenterVertically), - ) - val spacerWidth = 8.dp - // Add spaces on to allow room to grow - val dayWidth = 54.dp + spacerWidth - val monthWidth = 80.dp + spacerWidth - val yearWidth = 100.dp + spacerWidth - val onPickerSelected = - { current: FocusableElementDatePicker, next: FocusableElementDatePicker -> - if (pickerGroupState.selectedIndex != current.index) { - pickerGroupState.selectedIndex = current.index - } else { - pickerGroupState.selectedIndex = next.index - if (next == FocusableElementDatePicker.CONFIRM_BUTTON) { - focusRequesterConfirmButton.requestFocus() + val boxConstraints = this + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(16.dp)) + Text( + text = when (FocusableElementDatePicker[pickerGroupState.selectedIndex]) { + FocusableElementDatePicker.DAY -> dayString + FocusableElementDatePicker.MONTH -> monthString + FocusableElementDatePicker.YEAR -> yearString + else -> "" + }, + color = optionColor, + style = MaterialTheme.typography.button, + maxLines = 1, + ) + val weightsToCenterVertically = 0.5f + Spacer( + Modifier + .fillMaxWidth() + .weight(weightsToCenterVertically), + ) + val spacerWidth = 8.dp + // Add spaces on to allow room to grow + val dayWidth = 54.dp + spacerWidth + val monthWidth = 80.dp + spacerWidth + val yearWidth = 100.dp + spacerWidth + val onPickerSelected = + { current: FocusableElementDatePicker, next: FocusableElementDatePicker -> + if (pickerGroupState.selectedIndex != current.index) { + pickerGroupState.selectedIndex = current.index + } else { + pickerGroupState.selectedIndex = next.index + if (next == FocusableElementDatePicker.CONFIRM_BUTTON) { + focusRequesterConfirmButton.requestFocus() + } } } - } - Row( - modifier = Modifier - .fillMaxWidth() - .offset( - getPickerGroupRowOffset( - boxConstraints.maxWidth, - dayWidth, - monthWidth, - yearWidth, - spacerWidth, - touchExplorationServicesEnabled, - pickerGroupState, + Row( + modifier = Modifier + .fillMaxWidth() + .offset( + getPickerGroupRowOffset( + boxConstraints.maxWidth, + dayWidth, + monthWidth, + yearWidth, + spacerWidth, + touchExplorationServicesEnabled, + pickerGroupState, + ), ), - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - ) { - PickerGroup( - pickerGroupItemWithRSB( - pickerState = datePickerState.dayState, - modifier = Modifier.size(dayWidth, 100.dp), - onSelected = { + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + PickerGroup( + pickerGroupItemWithRSB( + pickerState = datePickerState.dayState, + modifier = Modifier.size(dayWidth, 100.dp), + onSelected = { + onPickerSelected( + FocusableElementDatePicker.DAY, + FocusableElementDatePicker.MONTH, + ) + }, + contentDescription = dayContentDescription, + option = pickerTextOption(textStyle) { + "%d".format(datePickerState.currentDay(it)) + }, + ), + pickerGroupItemWithRSB( + pickerState = datePickerState.monthState, + modifier = Modifier.size(monthWidth, 100.dp), + onSelected = { + onPickerSelected( + FocusableElementDatePicker.MONTH, + FocusableElementDatePicker.YEAR, + ) + }, + contentDescription = monthContentDescription, + option = pickerTextOption(textStyle) { + shortMonthNames[(datePickerState.currentMonth(it) - 1) % 12] + }, + ), + pickerGroupItemWithRSB( + pickerState = datePickerState.yearState, + modifier = Modifier.size(yearWidth, 100.dp), + onSelected = { + onPickerSelected( + FocusableElementDatePicker.YEAR, + FocusableElementDatePicker.CONFIRM_BUTTON, + ) + }, + contentDescription = yearContentDescription, + option = pickerTextOption(textStyle) { + "%4d".format(datePickerState.currentYear(it)) + }, + ), + pickerGroupState = pickerGroupState, + autoCenter = true, + separator = { }, + touchExplorationStateProvider = touchExplorationStateProvider, + ) + } + Spacer( + Modifier + .fillMaxWidth() + .weight(weightsToCenterVertically), + ) + Button( + onClick = { + if (pickerGroupState.selectedIndex >= 2) { + val confirmedYear: Int = datePickerState.currentYear() + val confirmedMonth: Int = datePickerState.currentMonth() + val confirmedDay: Int = datePickerState.currentDay() + val confirmedDate = + LocalDate.of(confirmedYear, confirmedMonth, confirmedDay) + onDateConfirm(confirmedDate) + } else if (pickerGroupState.selectedIndex == FocusableElementDatePicker.DAY.index) { onPickerSelected( FocusableElementDatePicker.DAY, FocusableElementDatePicker.MONTH, ) - }, - contentDescription = dayContentDescription, - option = pickerTextOption(textStyle) { - "%d".format(datePickerState.currentDay(it)) - }, - ), - pickerGroupItemWithRSB( - pickerState = datePickerState.monthState, - modifier = Modifier.size(monthWidth, 100.dp), - onSelected = { + } else if (pickerGroupState.selectedIndex == FocusableElementDatePicker.MONTH.index) { onPickerSelected( FocusableElementDatePicker.MONTH, FocusableElementDatePicker.YEAR, ) - }, - contentDescription = monthContentDescription, - option = pickerTextOption(textStyle) { - shortMonthNames[(datePickerState.currentMonth(it) - 1) % 12] - }, - ), - pickerGroupItemWithRSB( - pickerState = datePickerState.yearState, - modifier = Modifier.size(yearWidth, 100.dp), - onSelected = { + } else { onPickerSelected( - FocusableElementDatePicker.YEAR, - FocusableElementDatePicker.CONFIRM_BUTTON, + FocusableElementDatePicker.NONE, + FocusableElementDatePicker.DAY, ) - }, - contentDescription = yearContentDescription, - option = pickerTextOption(textStyle) { - "%4d".format(datePickerState.currentYear(it)) - }, - ), - pickerGroupState = pickerGroupState, - autoCenter = true, - separator = { }, - touchExplorationStateProvider = touchExplorationStateProvider, - ) - } - Spacer( - Modifier - .fillMaxWidth() - .weight(weightsToCenterVertically), - ) - Button( - onClick = { - if (pickerGroupState.selectedIndex >= 2) { - val confirmedYear: Int = datePickerState.currentYear() - val confirmedMonth: Int = datePickerState.currentMonth() - val confirmedDay: Int = datePickerState.currentDay() - val confirmedDate = - LocalDate.of(confirmedYear, confirmedMonth, confirmedDay) - onDateConfirm(confirmedDate) - } else if (pickerGroupState.selectedIndex == FocusableElementDatePicker.DAY.index) { - onPickerSelected( - FocusableElementDatePicker.DAY, - FocusableElementDatePicker.MONTH, - ) - } else if (pickerGroupState.selectedIndex == FocusableElementDatePicker.MONTH.index) { - onPickerSelected( - FocusableElementDatePicker.MONTH, - FocusableElementDatePicker.YEAR, - ) - } else { - onPickerSelected( - FocusableElementDatePicker.NONE, - FocusableElementDatePicker.DAY, - ) - } - }, - modifier = Modifier - .semantics { - focused = pickerGroupState.selectedIndex == - FocusableElementDatePicker.CONFIRM_BUTTON.index - } - .focusRequester(focusRequesterConfirmButton) - .focusable(), - ) { - Icon( - imageVector = - if (pickerGroupState.selectedIndex < 2) { - Icons.Filled.ChevronRight - } else { - Icons.Filled.Check - }, - contentDescription = - if (pickerGroupState.selectedIndex >= 2) { - stringResource(R.string.horologist_picker_confirm_button_content_description) - } else { - stringResource(R.string.horologist_picker_next_button_content_description) - }, + } + }, modifier = Modifier - .size(24.dp) - .wrapContentSize(align = Alignment.Center), - ) + .semantics { + focused = pickerGroupState.selectedIndex == + FocusableElementDatePicker.CONFIRM_BUTTON.index + } + .focusRequester(focusRequesterConfirmButton) + .focusable(), + ) { + Icon( + imageVector = + if (pickerGroupState.selectedIndex < 2) { + Icons.Filled.ChevronRight + } else { + Icons.Filled.Check + }, + contentDescription = + if (pickerGroupState.selectedIndex >= 2) { + stringResource(R.string.horologist_picker_confirm_button_content_description) + } else { + stringResource(R.string.horologist_picker_next_button_content_description) + }, + modifier = Modifier + .size(24.dp) + .wrapContentSize(align = Alignment.Center), + ) + } + Spacer(Modifier.height(12.dp)) } - Spacer(Modifier.height(12.dp)) } } diff --git a/composables/src/main/java/com/google/android/horologist/composables/TimePicker.kt b/composables/src/main/java/com/google/android/horologist/composables/TimePicker.kt index fcc84ed2e5..357e2195f5 100644 --- a/composables/src/main/java/com/google/android/horologist/composables/TimePicker.kt +++ b/composables/src/main/java/com/google/android/horologist/composables/TimePicker.kt @@ -78,6 +78,7 @@ import com.google.android.horologist.composables.picker.PickerState import com.google.android.horologist.composables.picker.rememberPickerGroupState import com.google.android.horologist.composables.picker.rememberPickerState import com.google.android.horologist.composables.picker.toRotaryScrollAdapter +import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.rotaryinput.rotaryWithSnap import java.time.LocalTime import java.time.temporal.ChronoField @@ -182,10 +183,11 @@ public fun TimePicker( } } - Box( + ScreenScaffold( modifier = modifier .fillMaxSize() .alpha(fullyDrawn.value), + timeText = {}, ) { Column( verticalArrangement = Arrangement.Center, @@ -398,10 +400,12 @@ public fun TimePickerWith12HourClock( } } } - Box( + + ScreenScaffold( modifier = modifier .fillMaxSize() .alpha(fullyDrawn.value), + timeText = {}, ) { Column( modifier = modifier.fillMaxSize(), diff --git a/compose-layout/api/current.api b/compose-layout/api/current.api index ec03ad29bb..5301d12fd0 100644 --- a/compose-layout/api/current.api +++ b/compose-layout/api/current.api @@ -50,6 +50,10 @@ package com.google.android.horologist.compose.focus { package com.google.android.horologist.compose.layout { + public final class AppScaffoldKt { + method @androidx.compose.runtime.Composable public static void AppScaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0 timeText, kotlin.jvm.functions.Function1 content); + } + public final class BelowTimeTextPreviewKt { method @androidx.compose.runtime.Composable public static com.google.android.horologist.compose.layout.ScalingLazyColumnState belowTimeTextPreview(); } @@ -64,6 +68,10 @@ package com.google.android.horologist.compose.layout { method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier fillMaxRectangle(androidx.compose.ui.Modifier); } + public final class PageScaffoldKt { + method @androidx.compose.runtime.Composable public static void PageScaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0? timeText, optional androidx.compose.foundation.gestures.ScrollableState? scrollState, optional kotlin.jvm.functions.Function0? positionIndicator, kotlin.jvm.functions.Function1 content); + } + public final class ScalingLazyColumnDefaults { method @com.google.android.horologist.annotations.ExperimentalHorologistApi public com.google.android.horologist.compose.layout.ScalingLazyColumnState.Factory belowTimeText(optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode rotaryMode, optional boolean firstItemIsFullWidth, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional float topPaddingDp); method @com.google.android.horologist.annotations.ExperimentalHorologistApi public com.google.android.horologist.compose.layout.ScalingLazyColumnState.Factory responsive(optional boolean firstItemIsFullWidth, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional float horizontalPaddingPercent, optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode? rotaryMode, optional boolean hapticsEnabled, optional boolean reverseLayout, optional boolean userScrollEnabled); @@ -71,8 +79,9 @@ package com.google.android.horologist.compose.layout { field public static final com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults INSTANCE; } - @com.google.android.horologist.annotations.ExperimentalHorologistApi public final class ScalingLazyColumnState { + @com.google.android.horologist.annotations.ExperimentalHorologistApi public final class ScalingLazyColumnState implements androidx.compose.foundation.gestures.ScrollableState { ctor public ScalingLazyColumnState(optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.ScrollPosition initialScrollPosition, optional androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering, optional int anchorType, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode? rotaryMode, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.foundation.lazy.ScalingParams scalingParams, optional boolean hapticsEnabled); + method public float dispatchRawDelta(float delta); method public int getAnchorType(); method public androidx.wear.compose.foundation.lazy.AutoCenteringParams? getAutoCentering(); method public androidx.compose.foundation.layout.PaddingValues getContentPadding(); @@ -86,14 +95,19 @@ package com.google.android.horologist.compose.layout { method public androidx.wear.compose.foundation.lazy.ScalingLazyListState getState(); method public boolean getUserScrollEnabled(); method public androidx.compose.foundation.layout.Arrangement.Vertical getVerticalArrangement(); + method public boolean isScrollInProgress(); + method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2,?> block, kotlin.coroutines.Continuation); method public void setState(androidx.wear.compose.foundation.lazy.ScalingLazyListState); property public final int anchorType; property public final androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering; + property public boolean canScrollBackward; + property public boolean canScrollForward; property public final androidx.compose.foundation.layout.PaddingValues contentPadding; property public final androidx.compose.foundation.gestures.FlingBehavior? flingBehavior; property public final boolean hapticsEnabled; property public final androidx.compose.ui.Alignment.Horizontal horizontalAlignment; property public final com.google.android.horologist.compose.layout.ScalingLazyColumnState.ScrollPosition initialScrollPosition; + property public boolean isScrollInProgress; property public final boolean reverseLayout; property public final com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode? rotaryMode; property public final androidx.wear.compose.foundation.lazy.ScalingParams scalingParams; @@ -137,11 +151,13 @@ package com.google.android.horologist.compose.layout { method @androidx.compose.runtime.Composable public static com.google.android.horologist.compose.layout.ScalingLazyColumnState rememberColumnState(optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.Factory factory); } + public final class ScreenScaffoldKt { + method @androidx.compose.runtime.Composable public static void ScreenScaffold(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0? timeText, optional androidx.compose.foundation.gestures.ScrollableState? scrollState, optional androidx.wear.compose.material.PageIndicatorState? pageIndicatorState, optional kotlin.jvm.functions.Function0? positionIndicator, kotlin.jvm.functions.Function1 content); + } + public final class ScrollAwayKt { - method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional float offset); - method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional float offset); - method @Deprecated public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollState, optional int itemIndex, optional float offset); method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, com.google.android.horologist.compose.layout.ScalingLazyColumnState scalingLazyColumnState); + method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function0 scrollableState); } public final class StateUtils { diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/AppScaffold.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/AppScaffold.kt new file mode 100644 index 0000000000..65f9884f82 --- /dev/null +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/AppScaffold.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2023 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.google.android.horologist.compose.layout + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.TimeText +import androidx.wear.compose.navigation.SwipeDismissableNavHost + +/** + * An app scaffold, to be used to wrap a [SwipeDismissableNavHost]. + * The [TimeText] will be shown here, but can be customised in either [ScreenScaffold] or + * [PageScaffold]. + * + * Without this, the vanilla [Scaffold] is likely placed on each individual screen and [TimeText] + * moves with the screen, or shown twice when swiping to dimiss. + * + * @param modifier the Scaffold modifier. + * @param timeText the app default time text, defaults to TimeText(). + * @param content the content block. + */ +@Composable +fun AppScaffold( + modifier: Modifier = Modifier, + timeText: @Composable () -> Unit = { TimeText() }, + content: @Composable BoxScope.() -> Unit, +) { + val scaffoldState = LocalScaffoldState.current.apply { + appTimeText.value = timeText + } + + Scaffold( + modifier = modifier, + timeText = scaffoldState.timeText, + ) { + Box(modifier = Modifier.fillMaxSize()) { + content() + } + } +} + +internal val LocalScaffoldState = compositionLocalOf { ScaffoldState() } diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/PageScaffold.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/PageScaffold.kt new file mode 100644 index 0000000000..706bf78afd --- /dev/null +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/PageScaffold.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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.google.android.horologist.compose.layout + +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.TimeText + +/** + * Pager Scaffold to place *inside* a single page of HorizontalPager. + * The [TimeText] if set will override the AppScaffold timeText. + * + * @param modifier the Scaffold modifier. + * @param timeText the page specific time text. + * @param scrollState the ScrollableState to show in a default PositionIndicator. + * @param positionIndicator set a non default PositionIndicator or disable with an no-op lambda. + * @param content the content block. + */ +@Composable +fun PageScaffold( + modifier: Modifier = Modifier, + timeText: (@Composable () -> Unit)? = null, + scrollState: ScrollableState? = null, + positionIndicator: (@Composable () -> Unit)? = null, + content: @Composable BoxScope.() -> Unit, +) { + ScreenScaffold(modifier = modifier, timeText, scrollState, null, positionIndicator, content) +} diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScaffoldState.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScaffoldState.kt new file mode 100644 index 0000000000..ba9cb79ef2 --- /dev/null +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScaffoldState.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2023 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.google.android.horologist.compose.layout + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.wear.compose.material.TimeText + +internal class ScaffoldState { + fun removeScreenTimeText(key: Any) { + screenContent.removeIf { it.key === key } + } + + fun addScreenTimeText( + key: Any, + timeText: @Composable (() -> Unit)?, + scrollState: ScrollableState?, + ) { + screenContent.add(PageContent(key, scrollState, timeText)) + } + + internal val appTimeText: MutableState<(@Composable (() -> Unit))> = + mutableStateOf({ TimeText() }) + internal val screenContent = mutableStateListOf() + + val timeText: @Composable (() -> Unit) + get() = { + val (scrollState, timeText) = currentContent() + + Box( + modifier = Modifier + .fillMaxSize() + .scrollAway { + scrollState ?: ScrollState(0) + }, + ) { + timeText() + } + } + + private fun currentContent(): Pair Unit)> { + var resultTimeText: @Composable (() -> Unit)? = null + var resultState: ScrollableState? = null + screenContent.forEach { + if (it.timeText != null) { + resultTimeText = it.timeText + } + if (it.scrollState != null) { + resultState = it.scrollState + } + } + return Pair(resultState, resultTimeText ?: appTimeText.value) + } + + internal data class PageContent( + val key: Any, + val scrollState: ScrollableState? = null, + val timeText: (@Composable () -> Unit)? = null, + ) +} diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScalingLazyColumnState.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScalingLazyColumnState.kt index 8e9d352fe3..f9e4983d5c 100644 --- a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScalingLazyColumnState.kt +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScalingLazyColumnState.kt @@ -19,8 +19,11 @@ package com.google.android.horologist.compose.layout +import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.foundation.gestures.ScrollableDefaults +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -70,7 +73,7 @@ public class ScalingLazyColumnState( public val userScrollEnabled: Boolean = true, public val scalingParams: ScalingParams = WearScalingLazyColumnDefaults.scalingParams(), public val hapticsEnabled: Boolean = true, -) { +) : ScrollableState { private var _state: ScalingLazyListState? = null public var state: ScalingLazyListState get() { @@ -86,6 +89,21 @@ public class ScalingLazyColumnState( _state = value } + override val canScrollBackward: Boolean + get() = state.canScrollBackward + override val canScrollForward: Boolean + get() = state.canScrollForward + override val isScrollInProgress: Boolean + get() = state.isScrollInProgress + override fun dispatchRawDelta(delta: Float): Float = state.dispatchRawDelta(delta) + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ) { + state.scroll(scrollPriority, block) + } + public sealed interface RotaryMode { public object Snap : RotaryMode public object Scroll : RotaryMode diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScreenScaffold.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScreenScaffold.kt new file mode 100644 index 0000000000..73cf0948fb --- /dev/null +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScreenScaffold.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2023 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. + */ + +@file:OptIn(ExperimentalWearFoundationApi::class) + +package com.google.android.horologist.compose.layout + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.wear.compose.foundation.ExperimentalWearFoundationApi +import androidx.wear.compose.foundation.OnFocusChange +import androidx.wear.compose.foundation.lazy.ScalingLazyListState +import androidx.wear.compose.material.HorizontalPageIndicator +import androidx.wear.compose.material.PageIndicatorState +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.Scaffold + +/** + * Navigation Route (Screen) Scaffold to place *inside* + * [androidx.wear.compose.navigation.composable]. The TimeText if set will override the + * [AppScaffold] timeText. + * + * @param modifier the Scaffold modifier. + * @param timeText the page specific time text. + * @param scrollState the ScrollableState to show in a default PositionIndicator. + * @param pageIndicatorState state for a HorizontalPager. + * @param positionIndicator set a non default PositionIndicator or disable with an no-op lambda. + * @param content the content block. + */ +@Composable +fun ScreenScaffold( + modifier: Modifier = Modifier, + timeText: (@Composable () -> Unit)? = null, + scrollState: ScrollableState? = null, + pageIndicatorState: PageIndicatorState? = null, + positionIndicator: (@Composable () -> Unit)? = null, + content: @Composable BoxScope.() -> Unit, +) { + val scaffoldState = LocalScaffoldState.current + + val key = remember { Any() } + + DisposableEffect(key) { + onDispose { + scaffoldState.removeScreenTimeText(key) + } + } + + OnFocusChange { focused -> + if (focused) { + scaffoldState.addScreenTimeText(key, timeText, scrollState) + } else { + scaffoldState.removeScreenTimeText(key) + } + } + + Scaffold( + modifier = modifier, + timeText = timeText, + pageIndicator = { + if (pageIndicatorState != null) { + HorizontalPageIndicator(pageIndicatorState = pageIndicatorState) + } + }, + positionIndicator = { + if (positionIndicator != null) { + positionIndicator() + } else if (scrollState is ScalingLazyColumnState) { + PositionIndicator(scalingLazyListState = scrollState.state) + } else if (scrollState is ScalingLazyListState) { + PositionIndicator(scalingLazyListState = scrollState) + } else if (scrollState is LazyListState) { + PositionIndicator(scrollState) + } else if (scrollState is ScrollState) { + PositionIndicator(scrollState) + } + }, + content = { Box { content() } }, + ) +} diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScrollAway.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScrollAway.kt index ff2eceb0cf..ead4d80d23 100644 --- a/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScrollAway.kt +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/layout/ScrollAway.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 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. @@ -16,128 +16,213 @@ package com.google.android.horologist.compose.layout +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed -import androidx.compose.ui.layout.layout -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp import androidx.wear.compose.foundation.lazy.ScalingLazyListState import androidx.wear.compose.material.scrollAway -import com.google.android.horologist.compose.navscaffold.ScalingLazyColumnScrollableState -import androidx.wear.compose.material.scrollAway as scrollAwayCompose +import kotlinx.coroutines.launch /** - * Scroll an item vertically in/out of view based on a [ScrollState]. - * Typically used to scroll a [TimeText] item out of view as the user starts to scroll a - * vertically scrollable [Column] of items upwards and bring additional items into view. + * Scroll an item vertically in/out of view based on a [ScalingLazyListState]. + * Typically used to scroll a [TimeText] item out of view as the user starts to scroll + * a [ScalingLazyColumn] of items upwards and bring additional items into view. * - * @param scrollState The [ScrollState] to used as the basis for the scroll-away. - * @param offset Adjustment to the starting point for scrolling away. Positive values result in - * the scroll away starting later. + * @param scalingLazyColumnState The list config. */ -@Deprecated( - "Replaced by Wear Compose scrollAway", - replaceWith = ReplaceWith( - "this.scrollAway(scrollState, offset)", - "androidx.wear.compose.material.scrollAway", - ), -) public fun Modifier.scrollAway( - scrollState: ScrollState, - offset: Dp = 0.dp, -): Modifier = scrollAwayCompose(scrollState, offset) + scalingLazyColumnState: ScalingLazyColumnState, +): Modifier = this.scrollAway { scalingLazyColumnState } /** - * Scroll an item vertically in/out of view based on a [LazyListState]. - * Typically used to scroll a [TimeText] item out of view as the user starts to scroll - * a [LazyColumn] of items upwards and bring additional items into view. + * Scroll an item vertically in/out of view based on a [ScrollState]. + * Typically used to scroll a [TimeText] item out of view as the user starts to scroll a + * vertically scrollable [Column] of items upwards and bring additional items into view. * - * @param scrollState The [LazyListState] to used as the basis for the scroll-away. - * @param itemIndex The item for which the scroll offset will trigger scrolling away. + * @param scrollState The [ScrollState] to used as the basis for the scroll-away. * @param offset Adjustment to the starting point for scrolling away. Positive values result in * the scroll away starting later. */ -@Deprecated( - "Replaced by Wear Compose scrollAway", - replaceWith = ReplaceWith( - "this.scrollAway(scrollState, itemIndex, offset)", - "androidx.wear.compose.material.scrollAway", - ), -) public fun Modifier.scrollAway( - scrollState: LazyListState, - itemIndex: Int = 0, - offset: Dp = 0.dp, -): Modifier = scrollAwayCompose(scrollState, itemIndex, offset) + scrollableState: () -> ScrollableState?, +): Modifier = scrollAwayImpl { + when (val scrollState = scrollableState()) { + is ScalingLazyColumnState -> { + val initialOffsetDp = scrollState.initialScrollPosition.offsetPx.toDp() -/** - * Scroll an item vertically in/out of view based on a [ScalingLazyListState]. - * Typically used to scroll a [TimeText] item out of view as the user starts to scroll - * a [ScalingLazyColumn] of items upwards and bring additional items into view. - * - * @param scrollState The [ScalingLazyListState] to used as the basis for the scroll-away. - * @param itemIndex The item for which the scroll offset will trigger scrolling away. - * @param offset Adjustment to the starting point for scrolling away. Positive values result in - * the scroll away starting later, negative values start scrolling away earlier. - */ -@Deprecated( - "Replaced by Wear Compose scrollAway", - replaceWith = ReplaceWith( - "this.scrollAway(scrollState, itemIndex, offset)", - "androidx.wear.compose.material.scrollAway", - ), -) -public fun Modifier.scrollAway( - scrollState: ScalingLazyListState, - itemIndex: Int = 1, - offset: Dp = 0.dp, -): Modifier = scrollAwayCompose(scrollState, itemIndex, offset) + ScrollParams( + valid = scrollState.initialScrollPosition.index < scrollState.state.layoutInfo.totalItemsCount, + isScrollInProgress = scrollState.isScrollInProgress, + yPx = scrollState.state.layoutInfo.visibleItemsInfo.find { it.index == scrollState.initialScrollPosition.index } + ?.let { + -it.offset - initialOffsetDp.toPx() + }, + ) + } -/** - * Scroll an item vertically in/out of view based on a [ScalingLazyListState]. - * Typically used to scroll a [TimeText] item out of view as the user starts to scroll - * a [ScalingLazyColumn] of items upwards and bring additional items into view. - * - * @param scalingLazyColumnState The list config. - */ -public fun Modifier.scrollAway( - scalingLazyColumnState: ScalingLazyColumnState, -): Modifier = composed { - val offset = with(LocalDensity.current) { - scalingLazyColumnState.initialScrollPosition.offsetPx.toDp() + is LazyListState -> { + ScrollParams( + valid = 0 < scrollState.layoutInfo.totalItemsCount, + isScrollInProgress = scrollState.isScrollInProgress, + yPx = scrollState.layoutInfo.visibleItemsInfo.find { it.index == 0 }?.let { + -it.offset - 0f + }, + ) + } + + is ScrollState -> { + ScrollParams( + valid = true, + isScrollInProgress = scrollState.isScrollInProgress, + yPx = scrollState.value.toFloat(), + ) + } + + else -> { + // Hide by display as offscreen + ScrollParams( + true, + false, + 10000f, + ) + } } - scrollAwayCompose( - scrollState = scalingLazyColumnState.state, - itemIndex = scalingLazyColumnState.initialScrollPosition.index, - offset = offset, - ) } -internal fun Modifier.scrollAway( - scrollState: State, +private fun Modifier.scrollAwayImpl( + scrollFn: Density.() -> ScrollParams, ): Modifier = composed { - when (val state = scrollState.value) { - is ScalingLazyColumnScrollableState -> { - val offsetDp = with(LocalDensity.current) { - state.initialOffsetPx.toDp() - } - this.scrollAway(state.scalingLazyListState, state.initialIndex, offsetDp) - } - is ScalingLazyListState -> this.scrollAway(state) - is LazyListState -> this.scrollAway(state) - is ScrollState -> this.scrollAway(state) - // Disabled - null -> this.hidden() - // Enabled but no scroll state - else -> this + val coroutineScope = rememberCoroutineScope() + var animatable by remember { + mutableStateOf?>(null) + } + var prevProgress by remember { + mutableFloatStateOf(0f) } + this.then( + @Suppress("ModifierInspectorInfo") + object : LayoutModifier { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val placeable = measurable.measure(constraints) + return layout(placeable.width, placeable.height) { + placeable.placeWithLayer(0, 0) { + val scrollParams = scrollFn() + val (motionFraction: Float, offsetY) = + if (!scrollParams.valid) { + // When the itemIndex is invalid, just show the content anyway. + 1f to 0f + } else if (scrollParams.yPx == null) { + // When itemIndex is valid but yPx is null, we infer that + // the item is not in the visible items list, so hide it. + 0f to 0f + } else { + // Scale, fade and scroll the content to scroll it away. + val anim: Animatable = + animatable ?: Animatable(prevProgress).also { animatable = it } + val targetProgress: Float = + (scrollParams.yPx / maxScrollOut.toPx()).coerceIn(0f, 1f) + var progress = 0f + if (scrollParams.isScrollInProgress) { + if (anim.targetValue != targetProgress) { + coroutineScope.launch { + anim.snapTo(targetProgress) + } + } + } else { + if (anim.targetValue != targetProgress) { + coroutineScope.launch { + anim.animateTo( + targetProgress, + tween(durationMillis = SHORT_4, easing = STANDARD), + ) + } + } + } + animatable?.let { + progress = it.value + } + prevProgress = targetProgress + val motionFraction: Float = + lerp(minMotionOut, maxMotionOut, progress) + val offsetY = -(maxOffset.toPx() * progress) + motionFraction to offsetY + } + + alpha = motionFraction + scaleX = motionFraction + scaleY = motionFraction + translationY = offsetY + transformOrigin = + TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f) + } + } + } + }, + ) } -internal fun Modifier.hidden(): Modifier = layout { _, _ -> layout(0, 0) {} } +private data class ScrollParams( + val valid: Boolean, + val isScrollInProgress: Boolean, + val yPx: Float?, +) + +// The scroll motion effects take place between 0dp and 36dp. +internal val maxScrollOut = 36.dp + +// The max offset to apply. +internal val maxOffset = 24.dp + +// Fade and scale motion effects are between 100% and 50%. +internal const val minMotionOut = 1f +internal const val maxMotionOut = 0.5f + +// See Wear Motion durations: https://carbon.googleplex.com/wear-os-3/pages/speed +internal const val SHORT_1 = 50 +internal const val SHORT_2 = 100 +internal const val SHORT_3 = 150 +internal const val SHORT_4 = 200 + +internal const val MEDIUM_1 = 250 +internal const val MEDIUM_2 = 300 +internal const val MEDIUM_3 = 350 +internal const val MEDIUM_4 = 400 + +internal const val LONG_1 = 450 +internal const val LONG_2 = 500 +internal const val LONG_3 = 550 +internal const val LONG_4 = 600 + +internal const val EXTRA_LONG_1 = 700 +internal const val EXTRA_LONG_2 = 800 +internal const val EXTRA_LONG_3 = 900 +internal const val EXTRA_LONG_4 = 1000 + +internal val STANDARD = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f) +internal val STANDARD_ACCELERATE = CubicBezierEasing(0.3f, 0.0f, 1.0f, 1.0f) +internal val STANDARD_DECELERATE = CubicBezierEasing(0.0f, 0.0f, 0.0f, 1.0f) diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/NavScaffoldViewModel.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/NavScaffoldViewModel.kt index 41d4b674d5..3e4d8adc89 100644 --- a/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/NavScaffoldViewModel.kt +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/NavScaffoldViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 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. diff --git a/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/WearNavScaffold.kt b/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/WearNavScaffold.kt index 8272a4717f..5c3e2a08fa 100644 --- a/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/WearNavScaffold.kt +++ b/compose-layout/src/main/java/com/google/android/horologist/compose/navscaffold/WearNavScaffold.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 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. @@ -32,7 +32,10 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -49,6 +52,7 @@ import androidx.wear.compose.material.PositionIndicator import androidx.wear.compose.material.Scaffold import androidx.wear.compose.material.TimeText import androidx.wear.compose.material.Vignette +import androidx.wear.compose.material.scrollAway import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.SwipeDismissableNavHostState import androidx.wear.compose.navigation.composable @@ -57,7 +61,6 @@ import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults import com.google.android.horologist.compose.layout.ScalingLazyColumnState -import com.google.android.horologist.compose.layout.scrollAway /** * A Navigation and Scroll aware [Scaffold]. @@ -132,6 +135,28 @@ public fun WearNavScaffold( } } +private fun Modifier.scrollAway( + scrollState: State, +): Modifier = composed { + when (val state = scrollState.value) { + is ScalingLazyColumnScrollableState -> { + val offsetDp = with(LocalDensity.current) { + state.initialOffsetPx.toDp() + } + this.scrollAway(state.scalingLazyListState, state.initialIndex, offsetDp) + } + is ScalingLazyListState -> this.scrollAway(state) + is LazyListState -> this.scrollAway(state) + is ScrollState -> this.scrollAway(state) + // Disabled + null -> this.hidden() + // Enabled but no scroll state + else -> this + } +} + +private fun Modifier.hidden(): Modifier = layout { _, _ -> layout(0, 0) {} } + @Composable private fun NavPositionIndicator(viewModel: NavScaffoldViewModel) { when (viewModel.scrollType) { diff --git a/compose-layout/src/test/java/com/google/android/horologist/compose/navscaffold/NavScaffoldTest.kt b/compose-layout/src/test/java/com/google/android/horologist/compose/navscaffold/NavScaffoldTest.kt index 3a4423a0ea..27f3aa2cdc 100644 --- a/compose-layout/src/test/java/com/google/android/horologist/compose/navscaffold/NavScaffoldTest.kt +++ b/compose-layout/src/test/java/com/google/android/horologist/compose/navscaffold/NavScaffoldTest.kt @@ -18,6 +18,7 @@ ExperimentalCoroutinesApi::class, ExperimentalWearFoundationApi::class, ) +@file:Suppress("DEPRECATION") package com.google.android.horologist.compose.navscaffold diff --git a/docs/compose-layout.md b/docs/compose-layout.md index 472b952f1d..eb17ecd012 100644 --- a/docs/compose-layout.md +++ b/docs/compose-layout.md @@ -53,49 +53,44 @@ Syncs the TimeText, PositionIndicator and Scaffold to the current navigation des state. The TimeText will scroll out of the way of content automatically. ```kotlin -WearNavScaffold( - startDestination = "home", - navController = navController -) { - scalingLazyColumnComposable( - "home", - scrollStateBuilder = { ScalingLazyListState(initialCenterItemIndex = 0) } - ) { - MenuScreen( - scrollState = it.scrollableState, - focusRequester = it.viewModel.focusRequester - ) - } - - scalingLazyColumnComposable( - "items", - scrollStateBuilder = { ScalingLazyListState() } +AppScaffold { + SwipeDismissableNavHost( + startDestination = "home", + navController = navController ) { - ScalingLazyColumn( - modifier = Modifier - .fillMaxSize() - .scrollableColumn(it.viewModel.focusRequester, it.scrollableState), - state = it.scrollableState + composable( + "home", ) { - items(100) { - Text("i = $it") + val columnState = rememberColumnState() + ScreenScaffold(scrollState = columnState) { + ScalingLazyColumn( + modifier = Modifier + .fillMaxSize(), + columnState = columnState + ) { + items(100) { + Text("i = $it") + } + } } } - } - scrollStateComposable( - "settings", - scrollStateBuilder = { ScrollState(0) } - ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(state = it.scrollableState) - .scrollableColumn(focusRequester = it.viewModel.focusRequester, scrollableState = it.scrollableState), - horizontalAlignment = Alignment.CenterHorizontally + composable( + "settings" ) { - (1..100).forEach { - Text("i = $it") + val scrollState = rememberScrollState() + ScreenScaffold(scrollState = scrollState) { + Column( + modifier = Modifier + .fillMaxSize() + .rotaryWithScroll(scrollState, rememberActiveFocusRequester()) + .verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally + ) { + (1..100).forEach { + Text("i = $it") + } + } } } } @@ -117,10 +112,6 @@ Box( ![](fill_max_rectangle.png){: loading=lazy width=70% align=center } -## Fade Away Modifier - -![](fade_away.png){: loading=lazy width=70% align=center } - ## AmbientAware composable `AmbientAware` allows your UI to react to ambient mode changes. For more information on how Ambient diff --git a/media/sample/src/main/java/com/google/android/horologist/mediasample/ui/app/UampWearApp.kt b/media/sample/src/main/java/com/google/android/horologist/mediasample/ui/app/UampWearApp.kt index 9fe143112d..3c08d0c822 100644 --- a/media/sample/src/main/java/com/google/android/horologist/mediasample/ui/app/UampWearApp.kt +++ b/media/sample/src/main/java/com/google/android/horologist/mediasample/ui/app/UampWearApp.kt @@ -28,10 +28,12 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.wear.compose.material.Text +import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.google.android.horologist.auth.ui.googlesignin.signin.GoogleSignInScreen -import com.google.android.horologist.compose.navscaffold.composable -import com.google.android.horologist.compose.navscaffold.scrollable +import com.google.android.horologist.compose.layout.PageScaffold +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberColumnState import com.google.android.horologist.media.ui.navigation.MediaNavController.navigateToCollection import com.google.android.horologist.media.ui.navigation.MediaNavController.navigateToCollections import com.google.android.horologist.media.ui.navigation.MediaNavController.navigateToLibrary @@ -80,13 +82,6 @@ fun UampWearApp( val appState by appViewModel.appState.collectAsStateWithLifecycle() - val timeText: @Composable (Modifier) -> Unit = { modifier -> - MediaInfoTimeText( - modifier = modifier, - mediaInfoTimeTextViewModel = mediaInfoTimeTextViewModel, - ) - } - UampTheme { MediaPlayerScaffold( playerScreen = { @@ -99,139 +94,175 @@ fun UampWearApp( }, ) }, - libraryScreen = { columnState -> - if (appState.streamingMode == true) { - UampStreamingBrowseScreen( - onPlaylistsClick = { navController.navigateToCollections() }, - onSettingsClick = { navController.navigateToSettings() }, - columnState = columnState, - ) - } else { - UampBrowseScreen( - uampBrowseScreenViewModel = hiltViewModel(), - onDownloadItemClick = { - navController.navigateToCollection( - it.playlistUiModel.id, - it.playlistUiModel.title, - ) - }, - onPlaylistsClick = { navController.navigateToCollections() }, - onSettingsClick = { navController.navigateToSettings() }, - columnState = columnState, - ) + libraryScreen = { + val columnState = rememberColumnState() + + PageScaffold(scrollState = columnState) { + if (appState.streamingMode == true) { + UampStreamingBrowseScreen( + onPlaylistsClick = { navController.navigateToCollections() }, + onSettingsClick = { navController.navigateToSettings() }, + columnState = columnState, + ) + } else { + UampBrowseScreen( + uampBrowseScreenViewModel = hiltViewModel(), + onDownloadItemClick = { + navController.navigateToCollection( + it.playlistUiModel.id, + it.playlistUiModel.title, + ) + }, + onPlaylistsClick = { navController.navigateToCollections() }, + onSettingsClick = { navController.navigateToSettings() }, + columnState = columnState, + ) + } } }, - categoryEntityScreen = { _, name, columnState -> - if (appState.streamingMode == true) { - val viewModel: UampStreamingPlaylistScreenViewModel = hiltViewModel() + categoryEntityScreen = { _, name -> + val columnState = rememberColumnState() - UampStreamingPlaylistScreen( - playlistName = name, - viewModel = viewModel, - onDownloadItemClick = { - navController.navigateToPlayer() - }, - onShuffleClick = { navController.navigateToPlayer() }, - onPlayClick = { navController.navigateToPlayer() }, - columnState = columnState, - ) - } else { - val uampEntityScreenViewModel: UampEntityScreenViewModel = hiltViewModel() + ScreenScaffold(scrollState = columnState) { + if (appState.streamingMode == true) { + val viewModel: UampStreamingPlaylistScreenViewModel = hiltViewModel() - UampEntityScreen( - playlistName = name, - uampEntityScreenViewModel = uampEntityScreenViewModel, - onDownloadItemClick = { - navController.navigateToPlayer() - }, - onShuffleClick = { navController.navigateToPlayer() }, - onPlayClick = { navController.navigateToPlayer() }, - onErrorDialogCancelClick = { navController.popBackStack() }, - columnState = columnState, - ) + UampStreamingPlaylistScreen( + playlistName = name, + viewModel = viewModel, + onDownloadItemClick = { + navController.navigateToPlayer() + }, + onShuffleClick = { navController.navigateToPlayer() }, + onPlayClick = { navController.navigateToPlayer() }, + columnState = columnState, + ) + } else { + val uampEntityScreenViewModel: UampEntityScreenViewModel = hiltViewModel() + + UampEntityScreen( + playlistName = name, + uampEntityScreenViewModel = uampEntityScreenViewModel, + onDownloadItemClick = { + navController.navigateToPlayer() + }, + onShuffleClick = { navController.navigateToPlayer() }, + onPlayClick = { navController.navigateToPlayer() }, + onErrorDialogCancelClick = { navController.popBackStack() }, + columnState = columnState, + ) + } } }, - mediaEntityScreen = { _ -> + mediaEntityScreen = { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Media XXX") } }, - playlistsScreen = { columnState -> + playlistsScreen = { val uampPlaylistsScreenViewModel: UampPlaylistsScreenViewModel = hiltViewModel() - UampPlaylistsScreen( - uampPlaylistsScreenViewModel = uampPlaylistsScreenViewModel, - onPlaylistItemClick = { playlistUiModel -> - navController.navigateToCollection( - playlistUiModel.id, - playlistUiModel.title, - ) - }, - onErrorDialogCancelClick = { navController.popBackStack() }, - columnState = columnState, - ) + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + UampPlaylistsScreen( + uampPlaylistsScreenViewModel = uampPlaylistsScreenViewModel, + onPlaylistItemClick = { playlistUiModel -> + navController.navigateToCollection( + playlistUiModel.id, + playlistUiModel.title, + ) + }, + onErrorDialogCancelClick = { navController.popBackStack() }, + columnState = columnState, + ) + } }, - settingsScreen = { columnState -> - UampSettingsScreen( - columnState = columnState, - viewModel = hiltViewModel(), - navController = navController, - ) + settingsScreen = { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + UampSettingsScreen( + columnState = columnState, + viewModel = hiltViewModel(), + navController = navController, + ) + } }, navHostState = navHostState, snackbarViewModel = hiltViewModel(), volumeViewModel = volumeViewModel, - timeText = timeText, + timeText = { + MediaInfoTimeText( + mediaInfoTimeTextViewModel = mediaInfoTimeTextViewModel, + ) + }, deepLinkPrefix = appViewModel.deepLinkPrefix, navController = navController, additionalNavRoutes = { - scrollable( + composable( route = AudioDebug.navRoute, arguments = AudioDebug.arguments, deepLinks = AudioDebug.deepLinks(appViewModel.deepLinkPrefix), ) { - AudioDebugScreen( - columnState = it.columnState, - audioDebugScreenViewModel = hiltViewModel(), - ) + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + AudioDebugScreen( + columnState = columnState, + audioDebugScreenViewModel = hiltViewModel(), + ) + } } - scrollable( + composable( route = Samples.navRoute, arguments = Samples.arguments, deepLinks = Samples.deepLinks(appViewModel.deepLinkPrefix), ) { - SamplesScreen( - columnState = it.columnState, - samplesScreenViewModel = hiltViewModel(), - navController = navController, - ) + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SamplesScreen( + columnState = columnState, + samplesScreenViewModel = hiltViewModel(), + navController = navController, + ) + } } - scrollable( + composable( route = DeveloperOptions.navRoute, arguments = DeveloperOptions.arguments, deepLinks = DeveloperOptions.deepLinks(appViewModel.deepLinkPrefix), ) { - DeveloperOptionsScreen( - columnState = it.columnState, - developerOptionsScreenViewModel = hiltViewModel(), - navController = navController, - ) + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + DeveloperOptionsScreen( + columnState = columnState, + developerOptionsScreenViewModel = hiltViewModel(), + navController = navController, + ) + } } - scrollable( + composable( route = GoogleSignInPromptScreen.navRoute, ) { - GoogleSignInPromptScreen( - navController = navController, - columnState = it.columnState, - viewModel = hiltViewModel(), - ) + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + GoogleSignInPromptScreen( + navController = navController, + columnState = columnState, + viewModel = hiltViewModel(), + ) + } } composable(route = GoogleSignInScreen.navRoute) { diff --git a/media/ui/api/current.api b/media/ui/api/current.api index 1fb6312dee..bba3816035 100644 --- a/media/ui/api/current.api +++ b/media/ui/api/current.api @@ -253,7 +253,7 @@ package com.google.android.horologist.media.ui.navigation { } public final class MediaPlayerScaffoldKt { - method @androidx.compose.runtime.Composable public static void MediaPlayerScaffold(com.google.android.horologist.media.ui.snackbar.SnackbarViewModel snackbarViewModel, com.google.android.horologist.audio.ui.VolumeViewModel volumeViewModel, kotlin.jvm.functions.Function0 playerScreen, kotlin.jvm.functions.Function1 libraryScreen, kotlin.jvm.functions.Function3 categoryEntityScreen, kotlin.jvm.functions.Function1 mediaEntityScreen, kotlin.jvm.functions.Function1 playlistsScreen, kotlin.jvm.functions.Function1 settingsScreen, String deepLinkPrefix, androidx.navigation.NavHostController navController, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0 volumeScreen, optional kotlin.jvm.functions.Function1 timeText, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState navHostState, optional kotlin.jvm.functions.Function1 additionalNavRoutes); + method @androidx.compose.runtime.Composable public static void MediaPlayerScaffold(com.google.android.horologist.media.ui.snackbar.SnackbarViewModel snackbarViewModel, com.google.android.horologist.audio.ui.VolumeViewModel volumeViewModel, kotlin.jvm.functions.Function0 playerScreen, kotlin.jvm.functions.Function0 libraryScreen, kotlin.jvm.functions.Function2 categoryEntityScreen, kotlin.jvm.functions.Function0 mediaEntityScreen, kotlin.jvm.functions.Function0 playlistsScreen, kotlin.jvm.functions.Function0 settingsScreen, String deepLinkPrefix, androidx.navigation.NavHostController navController, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0 volumeScreen, optional kotlin.jvm.functions.Function0 timeText, optional androidx.wear.compose.navigation.SwipeDismissableNavHostState navHostState, optional kotlin.jvm.functions.Function1 additionalNavRoutes); } public class NavigationScreens { @@ -479,7 +479,7 @@ package com.google.android.horologist.media.ui.screens.player { package com.google.android.horologist.media.ui.screens.playerlibrarypager { public final class PlayerLibraryPagerScreenKt { - method @androidx.compose.runtime.Composable public static void PlayerLibraryPagerScreen(androidx.compose.foundation.pager.PagerState pagerState, kotlin.jvm.functions.Function0 volumeUiState, kotlinx.coroutines.flow.Flow displayVolumeIndicatorEvents, kotlin.jvm.functions.Function1 timeText, kotlin.jvm.functions.Function0 playerScreen, kotlin.jvm.functions.Function1 libraryScreen, androidx.navigation.NavBackStackEntry backStack, optional androidx.compose.ui.Modifier modifier); + method @androidx.compose.runtime.Composable public static void PlayerLibraryPagerScreen(androidx.compose.foundation.pager.PagerState pagerState, kotlin.jvm.functions.Function0 volumeUiState, kotlinx.coroutines.flow.Flow displayVolumeIndicatorEvents, kotlin.jvm.functions.Function0 playerScreen, kotlin.jvm.functions.Function0 libraryScreen, androidx.navigation.NavBackStackEntry backStack, optional androidx.compose.ui.Modifier modifier); } } diff --git a/media/ui/src/main/java/com/google/android/horologist/media/ui/navigation/MediaPlayerScaffold.kt b/media/ui/src/main/java/com/google/android/horologist/media/ui/navigation/MediaPlayerScaffold.kt index 8b5d4d2340..db3e3f1423 100644 --- a/media/ui/src/main/java/com/google/android/horologist/media/ui/navigation/MediaPlayerScaffold.kt +++ b/media/ui/src/main/java/com/google/android/horologist/media/ui/navigation/MediaPlayerScaffold.kt @@ -21,7 +21,6 @@ package com.google.android.horologist.media.ui.navigation import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -31,15 +30,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.wear.compose.material.TimeText +import androidx.wear.compose.navigation.SwipeDismissableNavHost import androidx.wear.compose.navigation.SwipeDismissableNavHostState +import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.google.android.horologist.audio.ui.VolumeScreen import com.google.android.horologist.audio.ui.VolumeViewModel -import com.google.android.horologist.compose.layout.ScalingLazyColumnState -import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel -import com.google.android.horologist.compose.navscaffold.WearNavScaffold -import com.google.android.horologist.compose.navscaffold.composable -import com.google.android.horologist.compose.navscaffold.scrollable +import com.google.android.horologist.compose.layout.AppScaffold +import com.google.android.horologist.compose.layout.ScreenScaffold import com.google.android.horologist.compose.snackbar.DialogSnackbarHost import com.google.android.horologist.media.ui.screens.playerlibrarypager.PlayerLibraryPagerScreen import com.google.android.horologist.media.ui.snackbar.SnackbarViewModel @@ -60,126 +58,120 @@ import com.google.android.horologist.media.ui.snackbar.SnackbarViewModel * @param deepLinkPrefix the app specific prefix for external deeplinks * @param navController the media focused navigation controller. * @param additionalNavRoutes additional nav routes exposed for extra screens. - * @param pagerState the [PagerState] controlling the Player / Browse screen position. * @param navHostState the [SwipeDismissableNavHostState] including swipe to dismiss state. + * @param settingsScreen the settings screen. + * @param timeText the TimeText() composable. + * @param volumeScreen the volume screen. */ @Composable public fun MediaPlayerScaffold( snackbarViewModel: SnackbarViewModel, volumeViewModel: VolumeViewModel, playerScreen: @Composable () -> Unit, - libraryScreen: @Composable (ScalingLazyColumnState) -> Unit, - categoryEntityScreen: @Composable (id: String, name: String, ScalingLazyColumnState) -> Unit, - mediaEntityScreen: @Composable (ScalingLazyColumnState) -> Unit, - playlistsScreen: @Composable (ScalingLazyColumnState) -> Unit, - settingsScreen: @Composable (ScalingLazyColumnState) -> Unit, + libraryScreen: @Composable () -> Unit, + categoryEntityScreen: @Composable (id: String, name: String) -> Unit, + mediaEntityScreen: @Composable () -> Unit, + playlistsScreen: @Composable () -> Unit, + settingsScreen: @Composable () -> Unit, deepLinkPrefix: String, navController: NavHostController, modifier: Modifier = Modifier, - volumeScreen: @Composable () -> Unit = { - VolumeScreen(volumeViewModel = volumeViewModel) - }, - timeText: @Composable (Modifier) -> Unit = { - TimeText(modifier = it) - }, + volumeScreen: @Composable () -> Unit = { VolumeScreen(volumeViewModel = volumeViewModel) }, + timeText: @Composable () -> Unit = { TimeText() }, navHostState: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(), additionalNavRoutes: NavGraphBuilder.() -> Unit = {}, ) { - WearNavScaffold( - startDestination = NavigationScreens.Player.navRoute, - navController = navController, - modifier = modifier.background(Color.Transparent), - snackbar = { - DialogSnackbarHost( - modifier = Modifier.fillMaxSize(), - hostState = snackbarViewModel.snackbarHostState, - ) - }, - timeText = timeText, - state = navHostState, + AppScaffold( + timeText = { timeText() }, ) { - composable( - route = NavigationScreens.Player.navRoute, - arguments = NavigationScreens.Player.arguments, - deepLinks = NavigationScreens.Player.deepLinks(deepLinkPrefix), + SwipeDismissableNavHost( + startDestination = NavigationScreens.Player.navRoute, + navController = navController, + modifier = modifier.background(Color.Transparent), + state = navHostState, ) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - it.positionIndicatorMode = NavScaffoldViewModel.PositionIndicatorMode.Off - - val volumeState by volumeViewModel.volumeUiState.collectAsStateWithLifecycle() - val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) - - PlayerLibraryPagerScreen( - pagerState = pagerState, - volumeUiState = { volumeState }, - displayVolumeIndicatorEvents = volumeViewModel.displayIndicatorEvents, - timeText = timeText, - playerScreen = { - playerScreen() - }, - libraryScreen = { listState -> - libraryScreen(listState) - }, - backStack = it.backStackEntry, - ) + composable( + route = NavigationScreens.Player.navRoute, + arguments = NavigationScreens.Player.arguments, + deepLinks = NavigationScreens.Player.deepLinks(deepLinkPrefix), + ) { + val volumeState by volumeViewModel.volumeUiState.collectAsStateWithLifecycle() + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) + + PlayerLibraryPagerScreen( + pagerState = pagerState, + volumeUiState = { volumeState }, + displayVolumeIndicatorEvents = volumeViewModel.displayIndicatorEvents, + playerScreen = { + playerScreen() + }, + libraryScreen = { + libraryScreen() + }, + backStack = it, + ) + } + + composable( + route = NavigationScreens.Collections.navRoute, + arguments = NavigationScreens.Collections.arguments, + deepLinks = NavigationScreens.Collections.deepLinks(deepLinkPrefix), + ) { + playlistsScreen() + } + + composable( + route = NavigationScreens.Settings.navRoute, + + arguments = NavigationScreens.Settings.arguments, + deepLinks = NavigationScreens.Settings.deepLinks(deepLinkPrefix), + ) { + settingsScreen() + } + + composable( + route = NavigationScreens.Volume.navRoute, + arguments = NavigationScreens.Volume.arguments, + deepLinks = NavigationScreens.Volume.deepLinks(deepLinkPrefix), + ) { + ScreenScaffold(timeText = {}) { + volumeScreen() + } + } + + composable( + route = NavigationScreens.MediaItem.navRoute, + + arguments = NavigationScreens.MediaItem.arguments, + deepLinks = NavigationScreens.MediaItem.deepLinks(deepLinkPrefix), + ) { + mediaEntityScreen() + } + + composable( + route = NavigationScreens.Collection.navRoute, + + arguments = NavigationScreens.Collection.arguments, + deepLinks = NavigationScreens.Collection.deepLinks(deepLinkPrefix), + ) { + val arguments = it.arguments + val id = arguments?.getString(NavigationScreens.Collection.id) + val name = arguments?.getString(NavigationScreens.Collection.name) + checkNotNull(id) + checkNotNull(name) + + categoryEntityScreen( + id, + name, + ) + } + + additionalNavRoutes() } - scrollable( - route = NavigationScreens.Collections.navRoute, - - arguments = NavigationScreens.Collections.arguments, - deepLinks = NavigationScreens.Collections.deepLinks(deepLinkPrefix), - ) { - playlistsScreen(it.columnState) - } - - scrollable( - route = NavigationScreens.Settings.navRoute, - - arguments = NavigationScreens.Settings.arguments, - deepLinks = NavigationScreens.Settings.deepLinks(deepLinkPrefix), - ) { - settingsScreen(it.columnState) - } - - composable( - route = NavigationScreens.Volume.navRoute, - arguments = NavigationScreens.Volume.arguments, - deepLinks = NavigationScreens.Volume.deepLinks(deepLinkPrefix), - ) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - - volumeScreen() - } - - scrollable( - route = NavigationScreens.MediaItem.navRoute, - - arguments = NavigationScreens.MediaItem.arguments, - deepLinks = NavigationScreens.MediaItem.deepLinks(deepLinkPrefix), - ) { - mediaEntityScreen(it.columnState) - } - - scrollable( - route = NavigationScreens.Collection.navRoute, - - arguments = NavigationScreens.Collection.arguments, - deepLinks = NavigationScreens.Collection.deepLinks(deepLinkPrefix), - ) { scaffoldContext -> - val arguments = scaffoldContext.backStackEntry.arguments - val id = arguments?.getString(NavigationScreens.Collection.id) - val name = arguments?.getString(NavigationScreens.Collection.name) - checkNotNull(id) - checkNotNull(name) - - categoryEntityScreen( - id, - name, - scaffoldContext.columnState, - ) - } - - additionalNavRoutes() + DialogSnackbarHost( + modifier = Modifier.fillMaxSize(), + hostState = snackbarViewModel.snackbarHostState, + ) } } diff --git a/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/playerlibrarypager/PlayerLibraryPagerScreen.kt b/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/playerlibrarypager/PlayerLibraryPagerScreen.kt index 42a4430bd8..853f7f0034 100644 --- a/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/playerlibrarypager/PlayerLibraryPagerScreen.kt +++ b/media/ui/src/main/java/com/google/android/horologist/media/ui/screens/playerlibrarypager/PlayerLibraryPagerScreen.kt @@ -26,13 +26,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.navigation.NavBackStackEntry -import androidx.wear.compose.material.PositionIndicator -import androidx.wear.compose.material.Scaffold import com.google.android.horologist.audio.ui.VolumePositionIndicator import com.google.android.horologist.audio.ui.VolumeUiState -import com.google.android.horologist.compose.layout.ScalingLazyColumnState +import com.google.android.horologist.compose.layout.PageScaffold import com.google.android.horologist.compose.layout.belowTimeTextPreview -import com.google.android.horologist.compose.layout.scrollAway import com.google.android.horologist.compose.pager.PagerScreen import com.google.android.horologist.media.ui.navigation.NavigationScreens import kotlinx.coroutines.flow.Flow @@ -47,9 +44,8 @@ public fun PlayerLibraryPagerScreen( pagerState: PagerState, volumeUiState: () -> VolumeUiState, displayVolumeIndicatorEvents: Flow, - timeText: @Composable (Modifier) -> Unit, playerScreen: @Composable () -> Unit, - libraryScreen: @Composable (ScalingLazyColumnState) -> Unit, + libraryScreen: @Composable () -> Unit, backStack: NavBackStackEntry, modifier: Modifier = Modifier, ) { @@ -73,10 +69,7 @@ public fun PlayerLibraryPagerScreen( ) { page -> when (page) { 0 -> { - Scaffold( - timeText = { - timeText(Modifier) - }, + PageScaffold( positionIndicator = { VolumePositionIndicator(volumeUiState = volumeUiState, displayIndicatorEvents = displayVolumeIndicatorEvents) }, @@ -87,17 +80,10 @@ public fun PlayerLibraryPagerScreen( 1 -> { val config = belowTimeTextPreview() - Scaffold( - timeText = { - timeText(Modifier.scrollAway(config)) - }, - positionIndicator = { - PositionIndicator( - scalingLazyListState = config.state, - ) - }, + PageScaffold( + scrollState = config, ) { - libraryScreen(config) + libraryScreen() } } } diff --git a/sample/src/main/java/com/google/android/horologist/navsample/FillerScreen.kt b/sample/src/main/java/com/google/android/horologist/navsample/FillerScreen.kt index 8431a49d71..8ff2272eba 100644 --- a/sample/src/main/java/com/google/android/horologist/navsample/FillerScreen.kt +++ b/sample/src/main/java/com/google/android/horologist/navsample/FillerScreen.kt @@ -30,10 +30,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.ExperimentalWearFoundationApi -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.lazy.ScalingLazyListState import androidx.wear.compose.foundation.rememberActiveFocusRequester import androidx.wear.compose.material.Text +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnState import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll @Composable @@ -45,17 +45,12 @@ fun FillerScreen(label: String, modifier: Modifier = Modifier) { @Composable fun BigScalingLazyColumn( - scrollState: ScalingLazyListState, + columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, ) { - val focusRequester = rememberActiveFocusRequester() - ScalingLazyColumn( - modifier = modifier - .fillMaxSize() - .rotaryWithScroll(scrollState, focusRequester), - state = scrollState, - horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + columnState = columnState, ) { items(100) { Text("i = $it") diff --git a/sample/src/main/java/com/google/android/horologist/navsample/NavMenuScreen.kt b/sample/src/main/java/com/google/android/horologist/navsample/NavMenuScreen.kt index fe149ad831..f2f1689e6c 100644 --- a/sample/src/main/java/com/google/android/horologist/navsample/NavMenuScreen.kt +++ b/sample/src/main/java/com/google/android/horologist/navsample/NavMenuScreen.kt @@ -18,33 +18,22 @@ package com.google.android.horologist.navsample -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.wear.compose.foundation.ExperimentalWearFoundationApi -import androidx.wear.compose.foundation.lazy.AutoCenteringParams -import androidx.wear.compose.foundation.lazy.ScalingLazyColumn -import androidx.wear.compose.foundation.lazy.ScalingLazyListState -import androidx.wear.compose.foundation.rememberActiveFocusRequester -import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnState import com.google.android.horologist.sample.SampleChip @Composable fun NavMenuScreen( modifier: Modifier = Modifier, navigateToRoute: (String) -> Unit, - scrollState: ScalingLazyListState, + columnState: ScalingLazyColumnState, ) { - val focusRequester = rememberActiveFocusRequester() - ScalingLazyColumn( - modifier = modifier - .fillMaxSize() - .rotaryWithScroll(scrollState, focusRequester), - state = scrollState, - horizontalAlignment = Alignment.CenterHorizontally, - autoCentering = AutoCenteringParams(itemIndex = 0), + modifier = modifier, + columnState = columnState, ) { item { SampleChip( diff --git a/sample/src/main/java/com/google/android/horologist/navsample/NavWearApp.kt b/sample/src/main/java/com/google/android/horologist/navsample/NavWearApp.kt index 440ce672f4..e5d7d6cf58 100644 --- a/sample/src/main/java/com/google/android/horologist/navsample/NavWearApp.kt +++ b/sample/src/main/java/com/google/android/horologist/navsample/NavWearApp.kt @@ -19,10 +19,10 @@ package com.google.android.horologist.navsample import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -35,15 +35,14 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.material.Button import androidx.wear.compose.material.Chip import androidx.wear.compose.material.Text -import androidx.wear.compose.material.VignettePosition import androidx.wear.compose.material.dialog.Alert +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.google.android.horologist.audio.ui.VolumeScreen -import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel -import com.google.android.horologist.compose.navscaffold.WearNavScaffold -import com.google.android.horologist.compose.navscaffold.composable -import com.google.android.horologist.compose.navscaffold.scrollStateComposable -import com.google.android.horologist.compose.navscaffold.scrollable +import com.google.android.horologist.compose.layout.AppScaffold +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberColumnState import com.google.android.horologist.compose.pager.PagerScreen import com.google.android.horologist.compose.snackbar.DialogSnackbarHost import com.google.android.horologist.navsample.snackbar.SnackbarViewModel @@ -63,97 +62,106 @@ fun NavWearApp( val state by networkStatusViewModel.state.collectAsStateWithLifecycle() - WearNavScaffold( - startDestination = NavScreen.Menu.route, - navController = navController, - snackbar = { - DialogSnackbarHost( - hostState = snackbarViewModel.snackbarHostState, - modifier = Modifier.fillMaxSize(), - ) - }, + AppScaffold( timeText = { DataUsageTimeText( - modifier = it, showData = true, networkStatus = state.networks, networkUsage = state.dataUsage, ) }, - state = navState, ) { - scrollable( - NavScreen.Menu.route, + SwipeDismissableNavHost( + startDestination = NavScreen.Menu.route, + navController = navController, + state = navState, ) { - NavMenuScreen( - navigateToRoute = { route -> navController.navigate(route) }, - scrollState = it.scrollableState, - ) - } + composable( + NavScreen.Menu.route, + ) { + val columnState = rememberColumnState() - scrollable( - NavScreen.ScalingLazyColumn.route, - ) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.ScrollAway - it.viewModel.vignettePosition = - NavScaffoldViewModel.VignetteMode.On(VignettePosition.TopAndBottom) - it.positionIndicatorMode = - NavScaffoldViewModel.PositionIndicatorMode.On - - BigScalingLazyColumn( - scrollState = it.scrollableState, - ) - } + ScreenScaffold(scrollState = columnState) { + NavMenuScreen( + navigateToRoute = { route -> navController.navigate(route) }, + columnState = columnState, + ) + } + } - scrollStateComposable( - NavScreen.Column.route, - scrollStateBuilder = { ScrollState(initial = 0) }, - ) { - BigColumn( - scrollState = it.scrollableState, - ) - } + composable( + NavScreen.ScalingLazyColumn.route, + ) { + val columnState = rememberColumnState() - composable(NavScreen.Dialog.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + ScreenScaffold(scrollState = columnState) { + BigScalingLazyColumn( + columnState = columnState, + ) + } + } + + composable( + NavScreen.Column.route, + ) { + val scrollState = rememberScrollState() - Alert(title = { Text("Error") }) { - item { - Chip(onClick = {}, label = { Text("Hello") }) + ScreenScaffold(scrollState = scrollState) { + BigColumn( + scrollState = scrollState, + ) } } - } - composable(NavScreen.Snackbar.route) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Button(onClick = { snackbarViewModel.showMessage("Test") }) { - Text(text = "Test") + composable(NavScreen.Dialog.route) { + ScreenScaffold { + Alert(title = { Text("Error") }) { + item { + Chip(onClick = {}, label = { Text("Hello") }) + } + } } } - } - composable(NavScreen.Pager.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - - val pagerState = rememberPagerState { 10 } - PagerScreen( - // When using Modifier.edgeSwipeToDismiss, it is required that the element on - // which the modifier applies exists within a SwipeToDismissBox which shares - // the same state. Here, swipeDismissState is shared with - // our SwipeDismissableNavHost, which in turns passes it to its SwipeToDismissBox. - modifier = Modifier - .fillMaxSize() - .edgeSwipeToDismiss(swipeDismissState), - state = pagerState, - ) { + composable(NavScreen.Snackbar.route) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Screen $it") + Button(onClick = { snackbarViewModel.showMessage("Test") }) { + Text(text = "Test") + } + } + } + + composable(NavScreen.Pager.route) { + ScreenScaffold { + val pagerState = rememberPagerState { 10 } + PagerScreen( + // When using Modifier.edgeSwipeToDismiss, it is required that the element on + // which the modifier applies exists within a SwipeToDismissBox which shares + // the same state. Here, swipeDismissState is shared with + // our SwipeDismissableNavHost, which in turns passes it to its SwipeToDismissBox. + modifier = Modifier + .fillMaxSize() + .edgeSwipeToDismiss(swipeDismissState), + state = pagerState, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text(text = "Screen $it") + } + } } } - } - composable(NavScreen.Volume.route) { - VolumeScreen() + composable(NavScreen.Volume.route) { + VolumeScreen() + } } + + DialogSnackbarHost( + hostState = snackbarViewModel.snackbarHostState, + modifier = Modifier.fillMaxSize(), + ) } } diff --git a/sample/src/main/java/com/google/android/horologist/sample/SampleWearApp.kt b/sample/src/main/java/com/google/android/horologist/sample/SampleWearApp.kt index e2f1eee603..f2b7d5cb96 100644 --- a/sample/src/main/java/com/google/android/horologist/sample/SampleWearApp.kt +++ b/sample/src/main/java/com/google/android/horologist/sample/SampleWearApp.kt @@ -16,6 +16,8 @@ package com.google.android.horologist.sample +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -24,20 +26,18 @@ import androidx.compose.runtime.setValue import androidx.navigation.NavType import androidx.navigation.navArgument import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState +import androidx.wear.compose.navigation.SwipeDismissableNavHost +import androidx.wear.compose.navigation.composable import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.google.android.horologist.audio.ui.VolumeScreen import com.google.android.horologist.composables.DatePicker import com.google.android.horologist.composables.TimePicker import com.google.android.horologist.composables.TimePickerWith12HourClock -import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults +import com.google.android.horologist.compose.layout.AppScaffold import com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode -import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel -import com.google.android.horologist.compose.navscaffold.WearNavScaffold -import com.google.android.horologist.compose.navscaffold.composable -import com.google.android.horologist.compose.navscaffold.lazyListComposable -import com.google.android.horologist.compose.navscaffold.scrollStateComposable -import com.google.android.horologist.compose.navscaffold.scrollable +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberColumnState import com.google.android.horologist.materialcomponents.SampleButtonScreen import com.google.android.horologist.materialcomponents.SampleChipIconWithProgressScreen import com.google.android.horologist.materialcomponents.SampleChipScreen @@ -73,234 +73,318 @@ fun SampleWearApp() { var time by remember { mutableStateOf(LocalDateTime.now()) } - WearNavScaffold( - startDestination = Screen.Menu.route, - navController = navController, - state = navHostState, - ) { - scrollable( - route = Screen.Menu.route, + AppScaffold { + SwipeDismissableNavHost( + startDestination = Screen.Menu.route, + navController = navController, + state = navHostState, ) { - MenuScreen( - navigateToRoute = { route -> navController.navigate(route) }, - time = time, - columnState = it.columnState, - ) - } - scrollable( - Screen.Network.route, - columnStateFactory = ScalingLazyColumnDefaults.responsive(), - ) { - NetworkScreen( - columnState = it.columnState, - ) - } - composable(Screen.FillMaxRectangle.route) { - FillMaxRectangleScreen() - } - composable(Screen.Volume.route) { - VolumeScreen() - } - lazyListComposable(Screen.ScrollAway.route) { - ScrollScreenLazyColumn( - scrollState = it.scrollableState, - ) - } - scrollable( - Screen.ScrollAwaySLC.route, - ) { - ScrollAwayScreenScalingLazyColumn( - columnState = it.columnState, - ) - } - scrollStateComposable( - Screen.ScrollAwayColumn.route, - ) { - ScrollAwayScreenColumn( - scrollState = it.scrollableState, - ) - } - composable(Screen.DatePicker.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + composable( + route = Screen.Menu.route, + ) { + val columnState = rememberColumnState() - DatePicker( - date = time.toLocalDate(), - onDateConfirm = { - time = time.toLocalTime().atDate(it) - navController.popBackStack() - }, - ) - } - composable(Screen.TimePicker.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + ScreenScaffold(scrollState = columnState) { + MenuScreen( + navigateToRoute = { route -> navController.navigate(route) }, + time = time, + columnState = columnState, + ) + } + } + composable( + Screen.Network.route, + ) { + val columnState = rememberColumnState() - TimePickerWith12HourClock( - time = time.toLocalTime(), - onTimeConfirm = { - time = time.toLocalDate().atTime(it) - navController.popBackStack() - }, - ) - } - composable(Screen.TimeWithSecondsPicker.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + ScreenScaffold(scrollState = columnState) { + NetworkScreen( + columnState = columnState, + ) + } + } + composable(Screen.FillMaxRectangle.route) { + FillMaxRectangleScreen() + } + composable(Screen.Volume.route) { + VolumeScreen() + } + composable(Screen.ScrollAway.route) { + val scrollState = rememberLazyListState() + ScreenScaffold(scrollState = scrollState) { + ScrollScreenLazyColumn( + scrollState = scrollState, + ) + } + } + composable( + Screen.ScrollAwaySLC.route, + ) { + val columnState = rememberColumnState() - TimePicker( - time = time.toLocalTime(), - onTimeConfirm = { - time = time.toLocalDate().atTime(it) - navController.popBackStack() - }, - ) - } - composable(Screen.TimeWithoutSecondsPicker.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off + ScreenScaffold(scrollState = columnState) { + ScrollAwayScreenScalingLazyColumn( + columnState = columnState, + ) + } + } + composable( + Screen.ScrollAwayColumn.route, + ) { + val scrollState = rememberScrollState() + ScreenScaffold(scrollState = scrollState) { + ScrollAwayScreenColumn( + scrollState = scrollState, + ) + } + } + composable(Screen.DatePicker.route) { + DatePicker( + date = time.toLocalDate(), + onDateConfirm = { + time = time.toLocalTime().atDate(it) + navController.popBackStack() + }, + ) + } + composable(Screen.TimePicker.route) { + TimePickerWith12HourClock( + time = time.toLocalTime(), + onTimeConfirm = { + time = time.toLocalDate().atTime(it) + navController.popBackStack() + }, + ) + } + composable(Screen.TimeWithSecondsPicker.route) { + TimePicker( + time = time.toLocalTime(), + onTimeConfirm = { + time = time.toLocalDate().atTime(it) + navController.popBackStack() + }, + ) + } + composable(Screen.TimeWithoutSecondsPicker.route) { + TimePicker( + time = time.toLocalTime(), + onTimeConfirm = { + time = time.toLocalDate().atTime(it) + navController.popBackStack() + }, + showSeconds = false, + ) + } + composable( + route = Screen.MaterialButtonsScreen.route, + ) { + val columnState = rememberColumnState() - TimePicker( - time = time.toLocalTime(), - onTimeConfirm = { - time = time.toLocalDate().atTime(it) - navController.popBackStack() - }, - showSeconds = false, - ) - } - scrollable( - route = Screen.MaterialButtonsScreen.route, - ) { - SampleButtonScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialChipsScreen.route, - ) { - SampleChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialChipIconWithProgressScreen.route, - ) { - SampleChipIconWithProgressScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialCompactChipsScreen.route, - ) { - SampleCompactChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialConfirmationScreen.route, - ) { - SampleConfirmationScreen() - } - scrollable( - route = Screen.MaterialIconScreen.route, - ) { - SampleIconScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialOutlinedChipScreen.route, - ) { - SampleOutlinedChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialOutlinedCompactChipScreen.route, - ) { - SampleOutlinedCompactChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialSplitToggleChipScreen.route, - ) { - SampleSplitToggleChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialStepperScreen.route, - ) { - SampleStepperScreen() - } - scrollable( - route = Screen.MaterialTitleScreen.route, - ) { - SampleTitleScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialToggleButtonScreen.route, - ) { - SampleToggleButtonScreen(columnState = it.columnState) - } - scrollable( - route = Screen.MaterialToggleChipScreen.route, - ) { - SampleToggleChipScreen(columnState = it.columnState) - } - scrollable( - route = Screen.SectionedListMenuScreen.route, - ) { - SectionedListMenuScreen( - navigateToRoute = { route -> navController.navigate(route) }, - columnState = it.columnState, - ) - } - scrollable( - Screen.SectionedListStatelessScreen.route, - ) { - SectionedListStatelessScreen( - columnState = it.columnState, - ) - } - scrollable( - Screen.SectionedListStatefulScreen.route, - ) { - SectionedListStatefulScreen( - columnState = it.columnState, - ) - } - scrollable( - Screen.SectionedListExpandableScreen.route, - ) { - SectionedListExpandableScreen( - columnState = it.columnState, - ) - } - scrollable( - route = Screen.RotaryMenuScreen.route, - ) { - RotaryMenuScreen( - navigateToRoute = { route -> navController.navigate(route) }, - columnState = it.columnState, - ) - } - composable(route = Screen.RotaryScrollScreen.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - RotaryScrollScreen() - } - composable(route = Screen.RotaryScrollReversedScreen.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - RotaryScrollScreen(reverseDirection = true) - } - composable(route = Screen.RotaryScrollWithFlingScreen.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - RotaryScrollWithFlingOrSnapScreen(rotaryMode = RotaryMode.Scroll) - } - composable(route = Screen.RotarySnapListScreen.route) { - it.timeTextMode = NavScaffoldViewModel.TimeTextMode.Off - RotaryScrollWithFlingOrSnapScreen(rotaryMode = RotaryMode.Snap) - } - scrollable( - route = Screen.Paging.route, - columnStateFactory = ScalingLazyColumnDefaults.responsive(), - ) { - PagingScreen(navController = navController, columnState = it.columnState) - } - composable( - route = Screen.PagingItem.route, - arguments = listOf( - navArgument("id") { - type = NavType.IntType - }, - ), - ) { - PagingItemScreen(it.arguments!!.getInt("id")) - } - composable(route = Screen.PagerScreen.route) { - SamplePagerScreen(swipeToDismissBoxState) + ScreenScaffold(scrollState = columnState) { + SampleButtonScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialChipsScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleChipScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialChipIconWithProgressScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleChipIconWithProgressScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialCompactChipsScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleCompactChipScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialConfirmationScreen.route, + ) { + ScreenScaffold(timeText = {}) { + SampleConfirmationScreen() + } + } + composable( + route = Screen.MaterialIconScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleIconScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialOutlinedChipScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleOutlinedChipScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialOutlinedCompactChipScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleOutlinedCompactChipScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialSplitToggleChipScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleSplitToggleChipScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialStepperScreen.route, + ) { + SampleStepperScreen() + } + composable( + route = Screen.MaterialTitleScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleTitleScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialToggleButtonScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleToggleButtonScreen(columnState = columnState) + } + } + composable( + route = Screen.MaterialToggleChipScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SampleToggleChipScreen(columnState = columnState) + } + } + composable( + route = Screen.SectionedListMenuScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SectionedListMenuScreen( + navigateToRoute = { route -> navController.navigate(route) }, + columnState = columnState, + ) + } + } + composable( + Screen.SectionedListStatelessScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SectionedListStatelessScreen( + columnState = columnState, + ) + } + } + composable( + Screen.SectionedListStatefulScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SectionedListStatefulScreen( + columnState = columnState, + ) + } + } + composable( + Screen.SectionedListExpandableScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + SectionedListExpandableScreen( + columnState = columnState, + ) + } + } + composable( + route = Screen.RotaryMenuScreen.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + RotaryMenuScreen( + navigateToRoute = { route -> navController.navigate(route) }, + columnState = columnState, + ) + } + } + composable(route = Screen.RotaryScrollScreen.route) { + ScreenScaffold(timeText = {}) { + RotaryScrollScreen() + } + } + composable(route = Screen.RotaryScrollReversedScreen.route) { + ScreenScaffold(timeText = {}) { + RotaryScrollScreen(reverseDirection = true) + } + } + composable(route = Screen.RotaryScrollWithFlingScreen.route) { + ScreenScaffold(timeText = {}) { + RotaryScrollWithFlingOrSnapScreen(RotaryMode.Scroll) + } + } + composable(route = Screen.RotarySnapListScreen.route) { + ScreenScaffold(timeText = {}) { + RotaryScrollWithFlingOrSnapScreen(RotaryMode.Snap) + } + } + composable( + route = Screen.Paging.route, + ) { + val columnState = rememberColumnState() + + ScreenScaffold(scrollState = columnState) { + PagingScreen(navController = navController, columnState = columnState) + } + } + composable( + route = Screen.PagingItem.route, + arguments = listOf( + navArgument("id") { + type = NavType.IntType + }, + ), + ) { + PagingItemScreen(it.arguments!!.getInt("id")) + } + composable(route = Screen.PagerScreen.route) { + SamplePagerScreen(swipeToDismissBoxState) + } } } } diff --git a/sample/src/test/kotlin/com/google/android/horologist/screensizes/MediaPlayerLibraryTest.kt b/sample/src/test/kotlin/com/google/android/horologist/screensizes/MediaPlayerLibraryTest.kt index a50ce9ad09..8848e50b6a 100644 --- a/sample/src/test/kotlin/com/google/android/horologist/screensizes/MediaPlayerLibraryTest.kt +++ b/sample/src/test/kotlin/com/google/android/horologist/screensizes/MediaPlayerLibraryTest.kt @@ -77,7 +77,7 @@ class MediaPlayerLibraryTest(device: Device) : ScreenSizeTest(device = device, s modifier = Modifier.fillMaxSize(), timeText = { TimeText( - modifier = Modifier.scrollAway(scalingLazyColumnState = columnState), + modifier = Modifier.scrollAway(columnState), timeSource = FixedTimeSource, ) },