1 /*
<lambda>null2 * Copyright 2023 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 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.material3.adaptive.layout
18
19 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
20 import androidx.compose.runtime.Immutable
21 import androidx.compose.runtime.saveable.Saver
22 import androidx.compose.runtime.saveable.listSaver
23 import androidx.compose.ui.util.fastForEachReversed
24
25 /**
26 * Calculates the current adapted value of [ThreePaneScaffold] according to the given
27 * [maxHorizontalPartitions], [adaptStrategies] and [currentDestination]. The returned value can be
28 * used as a unique representation of the current layout structure.
29 *
30 * The function will treat the current destination as the highest priority and then adapt the rest
31 * panes according to the order of [ThreePaneScaffoldRole.Primary],
32 * [ThreePaneScaffoldRole.Secondary] and [ThreePaneScaffoldRole.Tertiary]. If there are still
33 * remaining partitions to put the pane, the pane will be set as [PaneAdaptedValue.Expanded],
34 * otherwise it will be adapted according to its associated [AdaptStrategy].
35 *
36 * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
37 * how many expanded panes can be shown at the same time.
38 * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
39 * the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
40 * @param currentDestination The current destination item, which will be treated as having the
41 * highest priority, can be `null`.
42 * @param maxVerticalPartitions The maximum allowed partitions along the vertical axis, by default
43 * it will be 1 and in this case no reflowed panes will be allowed; if the value equals to or
44 * larger than 2, reflowed panes are allowed, besides the expanded pane in the same horizontal
45 * partition.
46 */
47 @ExperimentalMaterial3AdaptiveApi
48 fun calculateThreePaneScaffoldValue(
49 maxHorizontalPartitions: Int,
50 adaptStrategies: ThreePaneScaffoldAdaptStrategies,
51 currentDestination: ThreePaneScaffoldDestinationItem<*>?,
52 maxVerticalPartitions: Int = 1
53 ): ThreePaneScaffoldValue =
54 calculateThreePaneScaffoldValue(
55 maxHorizontalPartitions,
56 adaptStrategies,
57 listOfNotNull(currentDestination),
58 maxVerticalPartitions
59 )
60
61 /**
62 * Calculates the current adapted value of [ThreePaneScaffold] according to the given
63 * [maxHorizontalPartitions], [adaptStrategies] and [destinationHistory]. The returned value can be
64 * used as a unique representation of the current layout structure.
65 *
66 * The function will treat the current focus as the highest priority and then adapt the rest panes
67 * according to the order of [ThreePaneScaffoldRole.Primary], [ThreePaneScaffoldRole.Secondary] and
68 * [ThreePaneScaffoldRole.Tertiary]. If there are still remaining partitions to put the pane, the
69 * pane will be set as [PaneAdaptedValue.Expanded], otherwise it will be adapted according to its
70 * associated [AdaptStrategy].
71 *
72 * @param maxHorizontalPartitions The maximum allowed partitions along the horizontal axis, i.e.,
73 * how many expanded panes can be shown at the same time.
74 * @param adaptStrategies The adapt strategies of each pane role that [ThreePaneScaffold] supports,
75 * the default value will be [ThreePaneScaffoldDefaults.adaptStrategies].
76 * @param destinationHistory The history of past destination items. The last destination will have
77 * the highest priority, and the second last destination will have the second highest priority,
78 * and so forth until all panes have a priority assigned. Note that the last destination is
79 * supposed to be the last item of the provided list.
80 * @param maxVerticalPartitions The maximum allowed partitions along the vertical axis, by default
81 * it will be 1 and in this case no reflowed panes will be allowed; if the value equals to or
82 * larger than 2, reflowed panes are allowed, besides the expanded pane in the same horizontal
83 * partition.
84 */
85 @ExperimentalMaterial3AdaptiveApi
86 fun calculateThreePaneScaffoldValue(
87 maxHorizontalPartitions: Int,
88 adaptStrategies: ThreePaneScaffoldAdaptStrategies,
89 destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>,
90 maxVerticalPartitions: Int = 1
91 ): ThreePaneScaffoldValue {
92 var expandedCount = 0
93 var primaryPaneAdaptedValue: PaneAdaptedValue? = null
94 var secondaryPaneAdaptedValue: PaneAdaptedValue? = null
95 var tertiaryPaneAdaptedValue: PaneAdaptedValue? = null
96
97 fun getAdaptedValue(role: ThreePaneScaffoldRole) =
98 when (role) {
99 ThreePaneScaffoldRole.Primary -> primaryPaneAdaptedValue
100 ThreePaneScaffoldRole.Secondary -> secondaryPaneAdaptedValue
101 ThreePaneScaffoldRole.Tertiary -> tertiaryPaneAdaptedValue
102 }
103
104 fun setAdaptedValue(role: ThreePaneScaffoldRole, value: PaneAdaptedValue) {
105 when (role) {
106 ThreePaneScaffoldRole.Primary -> primaryPaneAdaptedValue = value
107 ThreePaneScaffoldRole.Secondary -> secondaryPaneAdaptedValue = value
108 ThreePaneScaffoldRole.Tertiary -> tertiaryPaneAdaptedValue = value
109 }
110 }
111
112 var checkReflowedPane =
113 maxHorizontalPartitions == 1 &&
114 maxVerticalPartitions > 1 &&
115 (adaptStrategies[ThreePaneScaffoldRole.Primary] is AdaptStrategy.Reflow ||
116 adaptStrategies[ThreePaneScaffoldRole.Secondary] is AdaptStrategy.Reflow ||
117 adaptStrategies[ThreePaneScaffoldRole.Tertiary] is AdaptStrategy.Reflow)
118
119 run {
120 forEachPaneByPriority(destinationHistory) { pane ->
121 val hasAvailablePartition = expandedCount < maxHorizontalPartitions
122 if (!hasAvailablePartition && !checkReflowedPane) {
123 return@run // No need to check more panes, break;
124 }
125 if (getAdaptedValue(pane) != null) {
126 return@forEachPaneByPriority // Pane already adapted, continue;
127 }
128 var reflowedPane: ThreePaneScaffoldRole? = null
129 var anchorPane: ThreePaneScaffoldRole = pane
130 var anchorPaneValue: PaneAdaptedValue? = null
131 if (checkReflowedPane) {
132 (adaptStrategies[pane] as? AdaptStrategy.Reflow)?.apply {
133 (this.targetPane as? ThreePaneScaffoldRole)?.apply {
134 reflowedPane = pane
135 anchorPane = this
136 anchorPaneValue = getAdaptedValue(anchorPane)
137 }
138 }
139 }
140 when (anchorPaneValue) {
141 null ->
142 if (hasAvailablePartition) {
143 // Expand the anchor pane to reflow the pane
144 setAdaptedValue(anchorPane, PaneAdaptedValue.Expanded)
145 expandedCount++
146 } else {
147 // Cannot expand the anchor pane, continue;
148 return@forEachPaneByPriority
149 }
150 PaneAdaptedValue.Expanded -> {
151 // Anchor pane is expanded, do nothing
152 }
153 else -> return@forEachPaneByPriority // Anchor pane is not expanded, continue;
154 }
155 reflowedPane?.apply {
156 setAdaptedValue(this, PaneAdaptedValue.Reflowed(anchorPane))
157 checkReflowedPane = false
158 }
159 }
160 }
161 return ThreePaneScaffoldValue(
162 primary = primaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
163 secondary = secondaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
164 tertiary = tertiaryPaneAdaptedValue ?: PaneAdaptedValue.Hidden,
165 )
166 }
167
168 @ExperimentalMaterial3AdaptiveApi
forEachPaneByPrioritynull169 private inline fun forEachPaneByPriority(
170 destinationHistory: List<ThreePaneScaffoldDestinationItem<*>>,
171 action: (ThreePaneScaffoldRole) -> Unit
172 ) {
173 destinationHistory.fastForEachReversed { action(it.pane) }
174 action(ThreePaneScaffoldRole.Primary)
175 action(ThreePaneScaffoldRole.Secondary)
176 action(ThreePaneScaffoldRole.Tertiary)
177 }
178
179 /**
180 * The adapted value of [ThreePaneScaffold]. It contains each pane's adapted value.
181 * [ThreePaneScaffold] will use the adapted values to decide which panes should be displayed and how
182 * they should be displayed. With other input parameters of [ThreePaneScaffold] fixed, each possible
183 * instance of this class should represent a unique state of [ThreePaneScaffold] and developers can
184 * compare two [ThreePaneScaffoldValue] to decide if there is a layout structure change.
185 *
186 * For a Material-opinionated layout, it's suggested to use [calculateThreePaneScaffoldValue] to
187 * calculate the current scaffold value.
188 *
189 * @param primary [PaneAdaptedValue] of the primary pane of [ThreePaneScaffold]
190 * @param secondary [PaneAdaptedValue] of the secondary pane of [ThreePaneScaffold]
191 * @param tertiary [PaneAdaptedValue] of the tertiary pane of [ThreePaneScaffold]
192 * @constructor create an instance of [ThreePaneScaffoldValue]
193 */
194 @ExperimentalMaterial3AdaptiveApi
195 @Immutable
196 class ThreePaneScaffoldValue(
197 val primary: PaneAdaptedValue,
198 val secondary: PaneAdaptedValue,
199 val tertiary: PaneAdaptedValue
200 ) : PaneScaffoldValue<ThreePaneScaffoldRole>, PaneExpansionStateKeyProvider {
<lambda>null201 internal val expandedCount by lazy {
202 var count = 0
203 if (primary == PaneAdaptedValue.Expanded) {
204 count++
205 }
206 if (secondary == PaneAdaptedValue.Expanded) {
207 count++
208 }
209 if (tertiary == PaneAdaptedValue.Expanded) {
210 count++
211 }
212 count
213 }
214
<lambda>null215 override val paneExpansionStateKey by lazy {
216 if (expandedCount != 2) {
217 PaneExpansionStateKey.Default
218 } else {
219 val expandedPanes = Array<ThreePaneScaffoldRole?>(2) { null }
220 var count = 0
221 if (primary == PaneAdaptedValue.Expanded) {
222 expandedPanes[count++] = ThreePaneScaffoldRole.Primary
223 }
224 if (secondary == PaneAdaptedValue.Expanded) {
225 expandedPanes[count++] = ThreePaneScaffoldRole.Secondary
226 }
227 if (tertiary == PaneAdaptedValue.Expanded) {
228 expandedPanes[count] = ThreePaneScaffoldRole.Tertiary
229 }
230 TwoPaneExpansionStateKeyImpl(expandedPanes[0]!!, expandedPanes[1]!!)
231 }
232 }
233
equalsnull234 override fun equals(other: Any?): Boolean {
235 if (this === other) return true
236 if (other !is ThreePaneScaffoldValue) return false
237 if (primary != other.primary) return false
238 if (secondary != other.secondary) return false
239 if (tertiary != other.tertiary) return false
240 return true
241 }
242
hashCodenull243 override fun hashCode(): Int {
244 var result = primary.hashCode()
245 result = 31 * result + secondary.hashCode()
246 result = 31 * result + tertiary.hashCode()
247 return result
248 }
249
toStringnull250 override fun toString(): String {
251 return "ThreePaneScaffoldValue(primary=$primary, " +
252 "secondary=$secondary, " +
253 "tertiary=$tertiary)"
254 }
255
getnull256 override operator fun get(role: ThreePaneScaffoldRole): PaneAdaptedValue =
257 when (role) {
258 ThreePaneScaffoldRole.Primary -> primary
259 ThreePaneScaffoldRole.Secondary -> secondary
260 ThreePaneScaffoldRole.Tertiary -> tertiary
261 }
262 }
263
264 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
265 internal class TwoPaneExpansionStateKeyImpl(
266 val firstExpandedPane: ThreePaneScaffoldRole,
267 val secondExpandedPane: ThreePaneScaffoldRole
268 ) : PaneExpansionStateKey {
hashCodenull269 override fun hashCode(): Int {
270 return firstExpandedPane.hashCode() * 31 + secondExpandedPane.hashCode()
271 }
272
equalsnull273 override fun equals(other: Any?): Boolean {
274 if (this === other) return true
275 val otherKey = other as? TwoPaneExpansionStateKeyImpl ?: return false
276 return firstExpandedPane == otherKey.firstExpandedPane &&
277 secondExpandedPane == otherKey.secondExpandedPane
278 }
279
280 companion object {
savernull281 fun saver(): Saver<TwoPaneExpansionStateKeyImpl, Any> =
282 listSaver(
283 save = { listOf(it.firstExpandedPane, it.secondExpandedPane) },
<lambda>null284 restore = {
285 TwoPaneExpansionStateKeyImpl(
286 firstExpandedPane = it[0],
287 secondExpandedPane = it[1]
288 )
289 }
290 )
291 }
292 }
293