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  *      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.windowsizeclass
18 
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.ui.unit.Density
21 import androidx.compose.ui.unit.Dp
22 import androidx.compose.ui.unit.DpSize
23 import androidx.compose.ui.unit.dp
24 import androidx.compose.ui.util.fastForEach
25 
26 /**
27  * Window size classes are a set of opinionated viewport breakpoints to design, develop, and test
28  * responsive application layouts against. For more details check <a
29  * href="https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes"
30  * class="external" target="_blank">Support different screen sizes</a> documentation.
31  *
32  * WindowSizeClass contains a [WindowWidthSizeClass] and [WindowHeightSizeClass], representing the
33  * window size classes for this window's width and height respectively.
34  *
35  * See [calculateWindowSizeClass] to calculate the WindowSizeClass for an Activity's current window
36  *
37  * @property widthSizeClass width-based window size class ([WindowWidthSizeClass])
38  * @property heightSizeClass height-based window size class ([WindowHeightSizeClass])
39  */
40 @Immutable
41 class WindowSizeClass
42 private constructor(
43     val widthSizeClass: WindowWidthSizeClass,
44     val heightSizeClass: WindowHeightSizeClass
45 ) {
46     companion object {
47         /**
48          * Calculates the best matched [WindowSizeClass] for a given [size] according to the
49          * provided [supportedWidthSizeClasses] and [supportedHeightSizeClasses].
50          *
51          * @param size of the window
52          * @param supportedWidthSizeClasses the set of width size classes that are supported
53          * @param supportedHeightSizeClasses the set of height size classes that are supported
54          * @return [WindowSizeClass] corresponding to the given width and height
55          */
56         @ExperimentalMaterial3WindowSizeClassApi
calculateFromSizenull57         fun calculateFromSize(
58             size: DpSize,
59             supportedWidthSizeClasses: Set<WindowWidthSizeClass> =
60                 WindowWidthSizeClass.DefaultSizeClasses,
61             supportedHeightSizeClasses: Set<WindowHeightSizeClass> =
62                 WindowHeightSizeClass.DefaultSizeClasses
63         ): WindowSizeClass {
64             val windowWidthSizeClass =
65                 WindowWidthSizeClass.fromWidth(size.width, supportedWidthSizeClasses)
66             val windowHeightSizeClass =
67                 WindowHeightSizeClass.fromHeight(size.height, supportedHeightSizeClasses)
68             return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass)
69         }
70     }
71 
equalsnull72     override fun equals(other: Any?): Boolean {
73         if (this === other) return true
74         if (other == null || this::class != other::class) return false
75 
76         other as WindowSizeClass
77 
78         if (widthSizeClass != other.widthSizeClass) return false
79         if (heightSizeClass != other.heightSizeClass) return false
80 
81         return true
82     }
83 
hashCodenull84     override fun hashCode(): Int {
85         var result = widthSizeClass.hashCode()
86         result = 31 * result + heightSizeClass.hashCode()
87         return result
88     }
89 
toStringnull90     override fun toString() = "WindowSizeClass($widthSizeClass, $heightSizeClass)"
91 }
92 
93 /**
94  * Width-based window size class.
95  *
96  * A window size class represents a breakpoint that can be used to build responsive layouts. Each
97  * window size class breakpoint represents a majority case for typical device scenarios so your
98  * layouts will work well on most devices and configurations.
99  *
100  * For more details see <a
101  * href="https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#window_size_classes"
102  * class="external" target="_blank">Window size classes documentation</a>.
103  */
104 @Immutable
105 @kotlin.jvm.JvmInline
106 value class WindowWidthSizeClass private constructor(private val value: Int) :
107     Comparable<WindowWidthSizeClass> {
108 
109     override operator fun compareTo(other: WindowWidthSizeClass) =
110         breakpoint().compareTo(other.breakpoint())
111 
112     override fun toString(): String {
113         return "WindowWidthSizeClass." +
114             when (this) {
115                 Compact -> "Compact"
116                 Medium -> "Medium"
117                 Expanded -> "Expanded"
118                 else -> ""
119             }
120     }
121 
122     companion object {
123         /** Represents the majority of phones in portrait. */
124         val Compact = WindowWidthSizeClass(0)
125 
126         /**
127          * Represents the majority of tablets in portrait and large unfolded inner displays in
128          * portrait.
129          */
130         val Medium = WindowWidthSizeClass(1)
131 
132         /**
133          * Represents the majority of tablets in landscape and large unfolded inner displays in
134          * landscape.
135          */
136         val Expanded = WindowWidthSizeClass(2)
137 
138         /**
139          * The default set of size classes that includes [Compact], [Medium], and [Expanded] size
140          * classes. Should never expand to ensure behavioral consistency.
141          */
142         @Suppress("PrimitiveInCollection") val DefaultSizeClasses = setOf(Compact, Medium, Expanded)
143 
144         @Suppress("PrimitiveInCollection")
145         private val AllSizeClassList = listOf(Expanded, Medium, Compact)
146 
147         /**
148          * The set of all size classes. It's supposed to be expanded whenever a new size class is
149          * defined. By default [WindowSizeClass.calculateFromSize] will only return size classes in
150          * [DefaultSizeClasses] in order to avoid behavioral changes when new size classes are
151          * added. You can opt in to support all available size classes by doing:
152          * ```
153          * WindowSizeClass.calculateFromSize(
154          *     size = size,
155          *     density = density,
156          *     supportedWidthSizeClasses = WindowWidthSizeClass.AllSizeClasses,
157          *     supportedHeightSizeClasses = WindowHeightSizeClass.AllSizeClasses
158          * )
159          * ```
160          */
161         @Suppress("ListIterator", "PrimitiveInCollection")
162         val AllSizeClasses = AllSizeClassList.toSet()
163 
164         private fun WindowWidthSizeClass.breakpoint(): Dp {
165             return when {
166                 this == Expanded -> 840.dp
167                 this == Medium -> 600.dp
168                 else -> 0.dp
169             }
170         }
171 
172         /**
173          * Calculates the best matched [WindowWidthSizeClass] for a given [width] in Pixels and a
174          * given [Density] from [supportedSizeClasses].
175          */
176         internal fun fromWidth(
177             width: Dp,
178             supportedSizeClasses: Set<WindowWidthSizeClass>
179         ): WindowWidthSizeClass {
180             require(width >= 0.dp) { "Width must not be negative" }
181             require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" }
182             var smallestSupportedSizeClass = Compact
183             AllSizeClassList.fastForEach {
184                 if (it in supportedSizeClasses) {
185                     if (width >= it.breakpoint()) {
186                         return it
187                     }
188                     smallestSupportedSizeClass = it
189                 }
190             }
191 
192             // If none of the size classes matches, return the largest one.
193             return smallestSupportedSizeClass
194         }
195     }
196 }
197 
198 /**
199  * Height-based window size class.
200  *
201  * A window size class represents a breakpoint that can be used to build responsive layouts. Each
202  * window size class breakpoint represents a majority case for typical device scenarios so your
203  * layouts will work well on most devices and configurations.
204  *
205  * For more details see <a
206  * href="https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes#window_size_classes"
207  * class="external" target="_blank">Window size classes documentation</a>.
208  */
209 @Immutable
210 @kotlin.jvm.JvmInline
211 value class WindowHeightSizeClass private constructor(private val value: Int) :
212     Comparable<WindowHeightSizeClass> {
213 
compareTonull214     override operator fun compareTo(other: WindowHeightSizeClass) =
215         breakpoint().compareTo(other.breakpoint())
216 
217     override fun toString(): String {
218         return "WindowHeightSizeClass." +
219             when (this) {
220                 Compact -> "Compact"
221                 Medium -> "Medium"
222                 Expanded -> "Expanded"
223                 else -> ""
224             }
225     }
226 
227     companion object {
228         /** Represents the majority of phones in landscape */
229         val Compact = WindowHeightSizeClass(0)
230 
231         /** Represents the majority of tablets in landscape and majority of phones in portrait */
232         val Medium = WindowHeightSizeClass(1)
233 
234         /** Represents the majority of tablets in portrait */
235         val Expanded = WindowHeightSizeClass(2)
236 
237         /**
238          * The default set of size classes that includes [Compact], [Medium], and [Expanded] size
239          * classes. Should never expand to ensure behavioral consistency.
240          */
241         @Suppress("PrimitiveInCollection") val DefaultSizeClasses = setOf(Compact, Medium, Expanded)
242 
243         @Suppress("PrimitiveInCollection")
244         private val AllSizeClassList = listOf(Expanded, Medium, Compact)
245 
246         /**
247          * The set of all size classes. It's supposed to be expanded whenever a new size class is
248          * defined. By default [WindowSizeClass.calculateFromSize] will only return size classes in
249          * [DefaultSizeClasses] in order to avoid behavioral changes when new size classes are
250          * added. You can opt in to support all available size classes by doing:
251          * ```
252          * WindowSizeClass.calculateFromSize(
253          *     size = size,
254          *     density = density,
255          *     supportedWidthSizeClasses = WindowWidthSizeClass.AllSizeClasses,
256          *     supportedHeightSizeClasses = WindowHeightSizeClass.AllSizeClasses
257          * )
258          * ```
259          */
260         @Suppress("ListIterator", "PrimitiveInCollection")
261         val AllSizeClasses = AllSizeClassList.toSet()
262 
WindowHeightSizeClassnull263         private fun WindowHeightSizeClass.breakpoint(): Dp {
264             return when {
265                 this == Expanded -> 900.dp
266                 this == Medium -> 480.dp
267                 else -> 0.dp
268             }
269         }
270 
271         /**
272          * Calculates the best matched [WindowHeightSizeClass] for a given [height] in Pixels and a
273          * given [Density] from [supportedSizeClasses].
274          */
fromHeightnull275         internal fun fromHeight(
276             height: Dp,
277             supportedSizeClasses: Set<WindowHeightSizeClass>
278         ): WindowHeightSizeClass {
279             require(height >= 0.dp) { "Width must not be negative" }
280             require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" }
281             var smallestSupportedSizeClass = Expanded
282             AllSizeClassList.fastForEach {
283                 if (it in supportedSizeClasses) {
284                     if (height >= it.breakpoint()) {
285                         return it
286                     }
287                     smallestSupportedSizeClass = it
288                 }
289             }
290 
291             // If none of the size classes matches, return the largest one.
292             return smallestSupportedSizeClass
293         }
294     }
295 }
296