1 /*
2 * 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 @file:Suppress("DEPRECATED") // Deprecated import WindowWidthSizeClass.
18
19 package androidx.compose.material3.adaptive.layout
20
21 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
22 import androidx.compose.material3.adaptive.Posture
23 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
24 import androidx.compose.material3.adaptive.allVerticalHingeBounds
25 import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
26 import androidx.compose.material3.adaptive.occludingVerticalHingeBounds
27 import androidx.compose.material3.adaptive.separatingVerticalHingeBounds
28 import androidx.compose.runtime.Immutable
29 import androidx.compose.ui.geometry.Rect
30 import androidx.compose.ui.unit.Dp
31 import androidx.compose.ui.unit.dp
32 import kotlin.jvm.JvmInline
33
34 /**
35 * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
36 * method with [currentWindowAdaptiveInfo] to acquire Material-recommended adaptive layout settings
37 * of the current activity window.
38 *
39 * See more details on the [Material design guideline site]
40 * (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).
41 *
42 * @param windowAdaptiveInfo [WindowAdaptiveInfo] that collects useful information in making layout
43 * adaptation decisions like [WindowSizeClass].
44 * @param verticalHingePolicy [HingePolicy] that decides how layouts are supposed to address
45 * vertical hinges.
46 * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
47 */
48 @ExperimentalMaterial3AdaptiveApi
49 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
calculatePaneScaffoldDirectivenull50 fun calculatePaneScaffoldDirective(
51 windowAdaptiveInfo: WindowAdaptiveInfo,
52 verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
53 ): PaneScaffoldDirective {
54 val maxHorizontalPartitions: Int
55 val horizontalPartitionSpacerSize: Dp
56 when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
57 androidx.window.core.layout.WindowWidthSizeClass.COMPACT -> {
58 maxHorizontalPartitions = 1
59 horizontalPartitionSpacerSize = 0.dp
60 }
61 androidx.window.core.layout.WindowWidthSizeClass.MEDIUM -> {
62 maxHorizontalPartitions = 1
63 horizontalPartitionSpacerSize = 0.dp
64 }
65 else -> {
66 maxHorizontalPartitions = 2
67 horizontalPartitionSpacerSize = 24.dp
68 }
69 }
70 val maxVerticalPartitions: Int
71 val verticalPartitionSpacerSize: Dp
72
73 // TODO(conradchen): Confirm the table top mode settings
74 if (
75 windowAdaptiveInfo.windowPosture.isTabletop ||
76 (maxHorizontalPartitions == 1 &&
77 windowAdaptiveInfo.windowSizeClass.windowHeightSizeClass ==
78 androidx.window.core.layout.WindowHeightSizeClass.EXPANDED)
79 ) {
80 maxVerticalPartitions = 2
81 verticalPartitionSpacerSize = 24.dp
82 } else {
83 maxVerticalPartitions = 1
84 verticalPartitionSpacerSize = 0.dp
85 }
86
87 // TODO(conradchen): add 412.dp for L/XL window size class when they are available
88 val defaultPanePreferredWidth = PaneScaffoldDirective.DefaultPreferredWidth
89
90 val defaultPanePreferredHeight = PaneScaffoldDirective.DefaultPreferredHeight
91
92 return PaneScaffoldDirective(
93 maxHorizontalPartitions = maxHorizontalPartitions,
94 horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
95 maxVerticalPartitions = maxVerticalPartitions,
96 verticalPartitionSpacerSize = verticalPartitionSpacerSize,
97 defaultPanePreferredWidth = defaultPanePreferredWidth,
98 defaultPanePreferredHeight = defaultPanePreferredHeight,
99 excludedBounds =
100 getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy),
101 )
102 }
103
104 /**
105 * Calculates the recommended [PaneScaffoldDirective] from a given [WindowAdaptiveInfo]. Use this
106 * method with [currentWindowAdaptiveInfo] to acquire Material-recommended dense-mode adaptive
107 * layout settings of the current activity window. Note that this function results in a dual-pane
108 * layout when the [WindowWidthSizeClass] is [WindowWidthSizeClass.MEDIUM], while
109 * [calculatePaneScaffoldDirective] results in a single-pane layout instead. We recommend to use
110 * [calculatePaneScaffoldDirective], unless you have a strong use case to show two panes on a
111 * medium-width window, which can make your layout look too packed.
112 *
113 * See more details on the [Material design guideline site]
114 * (https://m3.material.io/foundations/layout/applying-layout/window-size-classes).
115 *
116 * @param windowAdaptiveInfo [WindowAdaptiveInfo] that collects useful information in making layout
117 * adaptation decisions like [WindowSizeClass].
118 * @param verticalHingePolicy [HingePolicy] that decides how layouts are supposed to address
119 * vertical hinges.
120 * @return an [PaneScaffoldDirective] to be used to decide adaptive layout states.
121 */
122 @ExperimentalMaterial3AdaptiveApi
123 @Suppress("DEPRECATION") // WindowWidthSizeClass is deprecated
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidthnull124 fun calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
125 windowAdaptiveInfo: WindowAdaptiveInfo,
126 verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
127 ): PaneScaffoldDirective {
128 val isMediumWidth =
129 windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass ==
130 androidx.window.core.layout.WindowWidthSizeClass.MEDIUM
131 val isTableTop = windowAdaptiveInfo.windowPosture.isTabletop
132 return with(calculatePaneScaffoldDirective(windowAdaptiveInfo, verticalHingePolicy)) {
133 copy(
134 maxHorizontalPartitions = if (isMediumWidth) 2 else maxHorizontalPartitions,
135 horizontalPartitionSpacerSize =
136 if (isMediumWidth) {
137 24.dp
138 } else {
139 horizontalPartitionSpacerSize
140 },
141 maxVerticalPartitions = if (isMediumWidth && !isTableTop) 1 else maxVerticalPartitions,
142 verticalPartitionSpacerSize =
143 if (isMediumWidth && !isTableTop) 0.dp else verticalPartitionSpacerSize
144 )
145 }
146 }
147
getExcludedVerticalBoundsnull148 private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List<Rect> {
149 return when (hingePolicy) {
150 HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds
151 HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds
152 HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds
153 else -> emptyList()
154 }
155 }
156
157 /**
158 * Top-level directives about how a pane scaffold should be arranged and spaced, like how many
159 * partitions the layout can be split into and what should be the gutter size.
160 *
161 * @property maxHorizontalPartitions the max number of partitions along the horizontal axis the
162 * layout can be split into.
163 * @property horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
164 * equivalent to the left/right margins the horizontal partitions.
165 * @property maxVerticalPartitions the max number of partitions along the vertical axis the layout
166 * can be split into.
167 * @property verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
168 * equivalent to the top/bottom margins of the vertical partitions.
169 * @property defaultPanePreferredWidth Default preferred width of panes that will be used by the
170 * scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane; see
171 * [PaneScaffoldScope.preferredWidth] for more info about how and when preferred width will be
172 * used.
173 * @property defaultPanePreferredHeight Default preferred height of panes that will be used by the
174 * scaffold if there's no [PaneScaffoldScope.preferredHeight] provided with a pane; see
175 * [PaneScaffoldScope.preferredHeight] for more info about how and when preferred height will be
176 * used.
177 * @property excludedBounds the bounds of all areas in the window that the layout needs to avoid
178 * displaying anything upon it. Usually these bounds represent where physical hinges are.
179 */
180 @Immutable
181 class PaneScaffoldDirective(
182 val maxHorizontalPartitions: Int,
183 val horizontalPartitionSpacerSize: Dp,
184 val maxVerticalPartitions: Int,
185 val verticalPartitionSpacerSize: Dp,
186 val defaultPanePreferredWidth: Dp,
187 val defaultPanePreferredHeight: Dp,
188 val excludedBounds: List<Rect>
189 ) {
190 constructor(
191 maxHorizontalPartitions: Int,
192 horizontalPartitionSpacerSize: Dp,
193 maxVerticalPartitions: Int,
194 verticalPartitionSpacerSize: Dp,
195 defaultPanePreferredWidth: Dp,
196 excludedBounds: List<Rect>,
197 ) : this(
198 maxHorizontalPartitions = maxHorizontalPartitions,
199 horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
200 maxVerticalPartitions = maxVerticalPartitions,
201 verticalPartitionSpacerSize = verticalPartitionSpacerSize,
202 defaultPanePreferredWidth = defaultPanePreferredWidth,
203 defaultPanePreferredHeight = DefaultPreferredHeight,
204 excludedBounds = excludedBounds,
205 )
206
207 /**
208 * Returns a new copy of [PaneScaffoldDirective] with specified fields overwritten. Use this
209 * method to create a custom [PaneScaffoldDirective] from the default instance or the result of
210 * [calculatePaneScaffoldDirective].
211 *
212 * @param maxHorizontalPartitions the max number of partitions along the horizontal axis the
213 * layout can be split into.
214 * @param horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
215 * equivalent to the left/right margins the horizontal partitions.
216 * @param maxVerticalPartitions the max number of partitions along the vertical axis the layout
217 * can be split into.
218 * @param verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
219 * equivalent to the top/bottom margins of the vertical partitions.
220 * @param defaultPanePreferredWidth Default preferred width of panes that will be used by the
221 * scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane.
222 * @param excludedBounds the bounds of all areas in the window that the layout needs to avoid
223 * displaying anything upon it. Usually these bounds represent where physical hinges are.
224 * @param defaultPanePreferredHeight Default preferred height of panes that will be used by the
225 * scaffold if there's no [PaneScaffoldScope.preferredHeight] provided with a pane.
226 */
copynull227 fun copy(
228 maxHorizontalPartitions: Int = this.maxHorizontalPartitions,
229 horizontalPartitionSpacerSize: Dp = this.horizontalPartitionSpacerSize,
230 maxVerticalPartitions: Int = this.maxVerticalPartitions,
231 verticalPartitionSpacerSize: Dp = this.verticalPartitionSpacerSize,
232 defaultPanePreferredWidth: Dp = this.defaultPanePreferredWidth,
233 excludedBounds: List<Rect> = this.excludedBounds,
234 defaultPanePreferredHeight: Dp = this.defaultPanePreferredHeight
235 ): PaneScaffoldDirective =
236 PaneScaffoldDirective(
237 maxHorizontalPartitions = maxHorizontalPartitions,
238 horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
239 maxVerticalPartitions = maxVerticalPartitions,
240 verticalPartitionSpacerSize = verticalPartitionSpacerSize,
241 defaultPanePreferredWidth = defaultPanePreferredWidth,
242 defaultPanePreferredHeight = defaultPanePreferredHeight,
243 excludedBounds = excludedBounds,
244 )
245
246 /**
247 * Returns a new copy of [PaneScaffoldDirective] with specified fields overwritten. Use this
248 * method to create a custom [PaneScaffoldDirective] from the default instance or the result of
249 * [calculatePaneScaffoldDirective].
250 *
251 * @param maxHorizontalPartitions the max number of partitions along the horizontal axis the
252 * layout can be split into.
253 * @param horizontalPartitionSpacerSize Size of the spacers between horizontal partitions. It's
254 * equivalent to the left/right margins the horizontal partitions.
255 * @param maxVerticalPartitions the max number of partitions along the vertical axis the layout
256 * can be split into.
257 * @param verticalPartitionSpacerSize Size of the spacers between vertical partitions. It's
258 * equivalent to the top/bottom margins of the vertical partitions.
259 * @param defaultPanePreferredWidth Default preferred width of panes that will be used by the
260 * scaffold if there's no [PaneScaffoldScope.preferredWidth] provided with a pane.
261 * @param excludedBounds the bounds of all areas in the window that the layout needs to avoid
262 * displaying anything upon it. Usually these bounds represent where physical hinges are.
263 */
264 @Deprecated(
265 "Maintained for binary compatibility. Use version with defaultPanePreferredHeight instead.",
266 level = DeprecationLevel.HIDDEN
267 )
268 fun copy(
269 maxHorizontalPartitions: Int = this.maxHorizontalPartitions,
270 horizontalPartitionSpacerSize: Dp = this.horizontalPartitionSpacerSize,
271 maxVerticalPartitions: Int = this.maxVerticalPartitions,
272 verticalPartitionSpacerSize: Dp = this.verticalPartitionSpacerSize,
273 defaultPanePreferredWidth: Dp = this.defaultPanePreferredWidth,
274 excludedBounds: List<Rect> = this.excludedBounds
275 ): PaneScaffoldDirective =
276 PaneScaffoldDirective(
277 maxHorizontalPartitions = maxHorizontalPartitions,
278 horizontalPartitionSpacerSize = horizontalPartitionSpacerSize,
279 maxVerticalPartitions = maxVerticalPartitions,
280 verticalPartitionSpacerSize = verticalPartitionSpacerSize,
281 defaultPanePreferredWidth = defaultPanePreferredWidth,
282 excludedBounds = excludedBounds,
283 )
284
285 override fun equals(other: Any?): Boolean {
286 if (this === other) return true
287 if (other !is PaneScaffoldDirective) return false
288 if (maxHorizontalPartitions != other.maxHorizontalPartitions) return false
289 if (horizontalPartitionSpacerSize != other.horizontalPartitionSpacerSize) return false
290 if (maxVerticalPartitions != other.maxVerticalPartitions) return false
291 if (verticalPartitionSpacerSize != other.verticalPartitionSpacerSize) return false
292 if (defaultPanePreferredWidth != other.defaultPanePreferredWidth) return false
293 if (defaultPanePreferredHeight != other.defaultPanePreferredHeight) return false
294 if (excludedBounds != other.excludedBounds) return false
295 return true
296 }
297
hashCodenull298 override fun hashCode(): Int {
299 var result = maxHorizontalPartitions
300 result = 31 * result + horizontalPartitionSpacerSize.hashCode()
301 result = 31 * result + maxVerticalPartitions
302 result = 31 * result + verticalPartitionSpacerSize.hashCode()
303 result = 31 * result + defaultPanePreferredWidth.hashCode()
304 result = 31 * result + defaultPanePreferredHeight.hashCode()
305 result = 31 * result + excludedBounds.hashCode()
306 return result
307 }
308
toStringnull309 override fun toString(): String {
310 return "PaneScaffoldDirective(maxHorizontalPartitions=$maxHorizontalPartitions, " +
311 "horizontalPartitionSpacerSize=$horizontalPartitionSpacerSize, " +
312 "maxVerticalPartitions=$maxVerticalPartitions, " +
313 "verticalPartitionSpacerSize=$verticalPartitionSpacerSize, " +
314 "defaultPanePreferredWidth=$defaultPanePreferredWidth, " +
315 "defaultPanePreferredHeight=$defaultPanePreferredHeight, " +
316 "number of excluded bounds=${excludedBounds.size})"
317 }
318
319 companion object {
320 internal val DefaultPreferredWidth = 360.dp
321 internal val DefaultPreferredHeight = 420.dp
322
323 /**
324 * A default instance of [PaneScaffoldDirective] that suggests a single-pane layout that
325 * occupies the full window. To create a customized [PaneScaffoldDirective], you can use
326 * [PaneScaffoldDirective.copy] on the default instance to create a copy with custom values.
327 */
328 val Default =
329 PaneScaffoldDirective(
330 maxHorizontalPartitions = 1,
331 horizontalPartitionSpacerSize = 0.dp,
332 maxVerticalPartitions = 1,
333 verticalPartitionSpacerSize = 0.dp,
334 defaultPanePreferredWidth = DefaultPreferredWidth,
335 defaultPanePreferredHeight = DefaultPreferredHeight,
336 excludedBounds = emptyList()
337 )
338 }
339 }
340
341 /** Policies that indicate how hinges are supposed to be addressed in an adaptive layout. */
342 @Immutable
343 @JvmInline
344 value class HingePolicy private constructor(private val value: Int) {
toStringnull345 override fun toString(): String {
346 return "HingePolicy." +
347 when (this) {
348 AlwaysAvoid -> "AlwaysAvoid"
349 AvoidSeparating -> "AvoidOccludingAndSeparating"
350 AvoidOccluding -> "AvoidOccludingOnly"
351 NeverAvoid -> "NeverAvoid"
352 else -> ""
353 }
354 }
355
356 companion object {
357 /** When rendering content in a layout, always avoid where hinges are. */
358 val AlwaysAvoid = HingePolicy(0)
359 /**
360 * When rendering content in a layout, avoid hinges that are separating. Note that an
361 * occluding hinge is supposed to be separating as well but not vice versa.
362 */
363 val AvoidSeparating = HingePolicy(1)
364 /**
365 * When rendering content in a layout, avoid hinges that are occluding. Note that an
366 * occluding hinge is supposed to be separating as well but not vice versa.
367 */
368 val AvoidOccluding = HingePolicy(2)
369 /** When rendering content in a layout, never avoid any hinges, separating or not. */
370 val NeverAvoid = HingePolicy(3)
371 }
372 }
373