Skip to content

Commit

Permalink
App/Screen/Page Scaffold (#1886)
Browse files Browse the repository at this point in the history

---------

Co-authored-by: yschimke <[email protected]>
  • Loading branch information
yschimke and yschimke authored Dec 19, 2023
1 parent e240de5 commit a8f0fa7
Show file tree
Hide file tree
Showing 23 changed files with 1,377 additions and 859 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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,
) {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -182,10 +183,11 @@ public fun TimePicker(
}
}

Box(
ScreenScaffold(
modifier = modifier
.fillMaxSize()
.alpha(fullyDrawn.value),
timeText = {},
) {
Column(
verticalArrangement = Arrangement.Center,
Expand Down Expand Up @@ -398,10 +400,12 @@ public fun TimePickerWith12HourClock(
}
}
}
Box(

ScreenScaffold(
modifier = modifier
.fillMaxSize()
.alpha(fullyDrawn.value),
timeText = {},
) {
Column(
modifier = modifier.fillMaxSize(),
Expand Down
24 changes: 20 additions & 4 deletions compose-layout/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -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<kotlin.Unit> timeText, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
}

public final class BelowTimeTextPreviewKt {
method @androidx.compose.runtime.Composable public static com.google.android.horologist.compose.layout.ScalingLazyColumnState belowTimeTextPreview();
}
Expand All @@ -64,15 +68,20 @@ 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<kotlin.Unit>? timeText, optional androidx.compose.foundation.gestures.ScrollableState? scrollState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? positionIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> 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);
method @com.google.android.horologist.annotations.ExperimentalHorologistApi public com.google.android.horologist.compose.layout.ScalingLazyColumnState.Factory scalingLazyColumnDefaults(optional com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode rotaryMode, optional int initialCenterIndex, optional int initialCenterOffset, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.foundation.lazy.AutoCenteringParams? autoCentering, optional int anchorType, optional boolean hapticsEnabled, optional boolean reverseLayout);
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();
Expand All @@ -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<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
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;
Expand Down Expand Up @@ -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<kotlin.Unit>? timeText, optional androidx.compose.foundation.gestures.ScrollableState? scrollState, optional androidx.wear.compose.material.PageIndicatorState? pageIndicatorState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? positionIndicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> 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<? extends androidx.compose.foundation.gestures.ScrollableState> scrollableState);
}

public final class StateUtils {
Expand Down
Original file line number Diff line number Diff line change
@@ -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() }
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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<PageContent>()

val timeText: @Composable (() -> Unit)
get() = {
val (scrollState, timeText) = currentContent()

Box(
modifier = Modifier
.fillMaxSize()
.scrollAway {
scrollState ?: ScrollState(0)
},
) {
timeText()
}
}

private fun currentContent(): Pair<ScrollableState?, @Composable (() -> 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,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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
Expand Down
Loading

0 comments on commit a8f0fa7

Please sign in to comment.