1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:Suppress("ObjectLiteralToLambda")
18 @file:OptIn(ExperimentalHorologistApi::class, ExperimentalWearFoundationApi::class)
19 
20 package com.google.android.horologist.compose.layout
21 
22 import androidx.compose.foundation.gestures.FlingBehavior
23 import androidx.compose.foundation.gestures.ScrollableDefaults
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.PaddingValues
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.saveable.rememberSaveable
28 import androidx.compose.ui.Alignment
29 import androidx.compose.ui.Modifier
30 import androidx.compose.ui.unit.dp
31 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
32 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
33 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
34 import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
35 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
36 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
37 import androidx.wear.compose.foundation.lazy.ScalingParams
38 import androidx.wear.compose.foundation.rememberActiveFocusRequester
39 import com.google.android.horologist.annotations.ExperimentalHorologistApi
40 import com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode
41 import com.google.android.horologist.compose.rotaryinput.rememberDisabledHaptic
42 import com.google.android.horologist.compose.rotaryinput.rememberRotaryHapticHandler
43 import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
44 import com.google.android.horologist.compose.rotaryinput.rotaryWithSnap
45 import com.google.android.horologist.compose.rotaryinput.toRotaryScrollAdapter
46 import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults as WearScalingLazyColumnDefaults
47 
48 /**
49  * A Config and State object wrapping up all configuration for a [ScalingLazyColumn].
50  * This allows defaults such as [ScalingLazyColumnDefaults.belowTimeText].
51  */
52 @ExperimentalHorologistApi
53 public class ScalingLazyColumnState(
54         public val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
55         public val autoCentering: AutoCenteringParams? = AutoCenteringParams(
56                 initialScrollPosition.index,
57                 initialScrollPosition.offsetPx,
58         ),
59         public val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
60         public val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
61         public val rotaryMode: RotaryMode = RotaryMode.Scroll,
62         public val reverseLayout: Boolean = false,
63         public val verticalArrangement: Arrangement.Vertical =
64                 Arrangement.spacedBy(
65                         space = 4.dp,
66                         alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom,
67                 ),
68         public val horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
69         public val flingBehavior: FlingBehavior? = null,
70         public val userScrollEnabled: Boolean = true,
71         public val scalingParams: ScalingParams = WearScalingLazyColumnDefaults.scalingParams(),
72         public val hapticsEnabled: Boolean = true,
73 ) {
74     private var _state: ScalingLazyListState? = null
75     public var state: ScalingLazyListState
76         get() {
77             if (_state == null) {
78                 _state = ScalingLazyListState(
79                         initialScrollPosition.index,
80                         initialScrollPosition.offsetPx,
81                 )
82             }
83             return _state!!
84         }
85         set(value) {
86             _state = value
87         }
88 
89     public sealed interface RotaryMode {
90         public object Snap : RotaryMode
91         public object Scroll : RotaryMode
92 
93         @Deprecated(
94                 "Use RotaryMode.Scroll instead",
95                 replaceWith = ReplaceWith("RotaryMode.Scroll"),
96         )
97         public object Fling : RotaryMode
98     }
99 
100     public data class ScrollPosition(
101             val index: Int,
102             val offsetPx: Int,
103     )
104 
105     public fun interface Factory {
106         @Composable
createnull107         public fun create(): ScalingLazyColumnState
108     }
109 }
110 
111 @Composable
112 public fun rememberColumnState(
113         factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.belowTimeText(),
114 ): ScalingLazyColumnState {
115     val columnState = factory.create()
116 
117     columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) {
118         columnState.state
119     }
120 
121     return columnState
122 }
123 
124 @ExperimentalHorologistApi
125 @Composable
ScalingLazyColumnnull126 public fun ScalingLazyColumn(
127         columnState: ScalingLazyColumnState,
128         modifier: Modifier = Modifier,
129         content: ScalingLazyListScope.() -> Unit,
130 ) {
131     val focusRequester = rememberActiveFocusRequester()
132 
133     val rotaryHaptics = if (columnState.hapticsEnabled) {
134         rememberRotaryHapticHandler(columnState.state)
135     } else {
136         rememberDisabledHaptic()
137     }
138     val modifierWithRotary = when (columnState.rotaryMode) {
139         RotaryMode.Snap -> modifier.rotaryWithSnap(
140                 focusRequester = focusRequester,
141                 rotaryScrollAdapter = columnState.state.toRotaryScrollAdapter(),
142                 reverseDirection = columnState.reverseLayout,
143                 rotaryHaptics = rotaryHaptics,
144         )
145 
146         else -> modifier.rotaryWithScroll(
147                 focusRequester = focusRequester,
148                 scrollableState = columnState.state,
149                 reverseDirection = columnState.reverseLayout,
150                 rotaryHaptics = rotaryHaptics,
151         )
152     }
153 
154     ScalingLazyColumn(
155             modifier = modifierWithRotary,
156             state = columnState.state,
157             contentPadding = columnState.contentPadding,
158             reverseLayout = columnState.reverseLayout,
159             verticalArrangement = columnState.verticalArrangement,
160             horizontalAlignment = columnState.horizontalAlignment,
161             flingBehavior = columnState.flingBehavior ?: ScrollableDefaults.flingBehavior(),
162             userScrollEnabled = columnState.userScrollEnabled,
163             scalingParams = columnState.scalingParams,
164             anchorType = columnState.anchorType,
165             autoCentering = columnState.autoCentering,
166             content = content,
167     )
168 }
169