1 /*
<lambda>null2  * 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.window.core.layout
18 
19 import kotlin.jvm.JvmField
20 import kotlin.jvm.JvmStatic
21 
22 /**
23  * [WindowSizeClass] represents breakpoints for a viewport. Designers should design around the
24  * different combinations of width and height buckets. Developers should use the different buckets
25  * to specify the layouts. Ideally apps will work well in each bucket and by extension work well
26  * across multiple devices. If two devices are in similar buckets they should behave similarly.
27  *
28  * This class is meant to be a common definition that can be shared across different device types.
29  * Application developers can use [WindowSizeClass] to have standard window buckets and design the
30  * UI around those buckets. Library developers can use these buckets to create different UI with
31  * respect to each bucket. This will help with consistency across multiple device types.
32  *
33  * A library developer use-case can be creating some navigation UI library. For a size class with
34  * the [WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND] width it might be more reasonable to have a
35  * side navigation.
36  *
37  * An application use-case can be applied for apps that use a list-detail pattern. The app can use
38  * the [WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND] to determine if there is enough space to show
39  * the list and the detail side by side. If all apps follow this guidance then it will present a
40  * very consistent user experience.
41  *
42  * In some cases developers or UI systems may decide to create their own break points. A developer
43  * might optimize for a window that is smaller than the supported break points or larger. A UI
44  * system might find that some break points are better suited than the recommended break points. In
45  * these cases developers may wish to specify their own custom break points and match using a `when`
46  * statement.
47  *
48  * To process a [WindowSizeClass] use the methods [isAtLeastBreakpoint], [isWidthAtLeastBreakpoint],
49  * [isHeightAtLeastBreakpoint] methods. Note these methods are order dependent as the smaller
50  * [minWidthDp] and [minHeightDp] would match all the breakpoints that are larger. Therefore when
51  * processing the selection should normally be ordered from larger to smaller breakpoints.
52  *
53  * @see WindowWidthSizeClass
54  * @see WindowHeightSizeClass
55  */
56 class WindowSizeClass(
57     /** Returns the lower bound for the width of the size class in dp. */
58     val minWidthDp: Int,
59     /** Returns the lower bound for the height of the size class in dp. */
60     val minHeightDp: Int
61 ) {
62 
63     /** A convenience constructor that will truncate to ints. */
64     constructor(widthDp: Float, heightDp: Float) : this(widthDp.toInt(), heightDp.toInt())
65 
66     init {
67         require(minWidthDp >= 0) {
68             "Expected minWidthDp to be at least 0, minWidthDp: $minWidthDp."
69         }
70         require(minHeightDp >= 0) {
71             "Expected minHeightDp to be at least 0, minHeightDp: $minHeightDp."
72         }
73     }
74 
75     @Suppress("DEPRECATION")
76     @Deprecated(
77         "Use either isWidthAtLeastBreakpoint or isAtLeastBreakpoint to check matching bounds."
78     )
79     /** Returns the [WindowWidthSizeClass] that corresponds to the widthDp of the window. */
80     val windowWidthSizeClass: WindowWidthSizeClass
81         get() = WindowWidthSizeClass.compute(minWidthDp.toFloat())
82 
83     @Suppress("DEPRECATION")
84     @Deprecated(
85         "Use either isHeightAtLeastBreakpoint or isAtLeastBreakpoint to check matching bounds."
86     )
87     /** Returns the [WindowHeightSizeClass] that corresponds to the heightDp of the window. */
88     val windowHeightSizeClass: WindowHeightSizeClass
89         get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
90 
91     /**
92      * Returns `true` when [minWidthDp] is greater than or equal to [widthDpBreakpoint], `false`
93      * otherwise. When processing a [WindowSizeClass] note that this method is order dependent.
94      * Selection should go from largest to smallest breakpoints.
95      *
96      * @sample androidx.window.core.samples.layout.processWindowSizeClassWidthOnly
97      */
98     fun isWidthAtLeastBreakpoint(widthDpBreakpoint: Int): Boolean {
99         return minWidthDp >= widthDpBreakpoint
100     }
101 
102     /**
103      * Returns `true` when [minHeightDp] is greater than or equal to [heightDpBreakpoint], `false`
104      * otherwise. When processing a [WindowSizeClass] note that this method is order dependent.
105      * Selection should go from largest to smallest breakpoints.
106      */
107     fun isHeightAtLeastBreakpoint(heightDpBreakpoint: Int): Boolean {
108         return minHeightDp >= heightDpBreakpoint
109     }
110 
111     /**
112      * Returns `true` when [minWidthDp] is greater than or equal to [widthDpBreakpoint] and
113      * [minHeightDp] is greater than or equal to [heightDpBreakpoint], `false` otherwise. When
114      * processing a [WindowSizeClass] note that this method is order dependent. Selection should go
115      * from largest to smallest breakpoints.
116      */
117     fun isAtLeastBreakpoint(widthDpBreakpoint: Int, heightDpBreakpoint: Int): Boolean {
118         return isWidthAtLeastBreakpoint(widthDpBreakpoint) &&
119             isHeightAtLeastBreakpoint(heightDpBreakpoint)
120     }
121 
122     override fun equals(other: Any?): Boolean {
123         if (this === other) return true
124         if (other == null || this::class != other::class) return false
125 
126         other as WindowSizeClass
127 
128         if (minWidthDp != other.minWidthDp) return false
129         if (minHeightDp != other.minHeightDp) return false
130 
131         return true
132     }
133 
134     override fun hashCode(): Int {
135         var result = minWidthDp
136         result = 31 * result + minHeightDp
137         return result
138     }
139 
140     override fun toString(): String {
141         return "WindowSizeClass(minWidthDp=$minWidthDp, minHeightDp=$minHeightDp)"
142     }
143 
144     companion object {
145         /** A lower bound for a size class with Medium width in dp. */
146         const val WIDTH_DP_MEDIUM_LOWER_BOUND = 600
147 
148         /** A lower bound for a size class with Expanded width in dp. */
149         const val WIDTH_DP_EXPANDED_LOWER_BOUND = 840
150 
151         /** A lower bound for a size class with Large width in dp. */
152         const val WIDTH_DP_LARGE_LOWER_BOUND = 1200
153 
154         /** A lower bound for a size class width Extra Large width in dp. */
155         const val WIDTH_DP_EXTRA_LARGE_LOWER_BOUND = 1600
156 
157         /** A lower bound for a size class with Medium height in dp. */
158         const val HEIGHT_DP_MEDIUM_LOWER_BOUND = 480
159 
160         /** A lower bound for a size class with Expanded height in dp. */
161         const val HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
162 
163         private val WIDTH_DP_BREAKPOINTS_V1 =
164             listOf(0, WIDTH_DP_MEDIUM_LOWER_BOUND, WIDTH_DP_EXPANDED_LOWER_BOUND)
165 
166         private val WIDTH_DP_BREAKPOINTS_V2 =
167             WIDTH_DP_BREAKPOINTS_V1 +
168                 listOf(WIDTH_DP_LARGE_LOWER_BOUND, WIDTH_DP_EXTRA_LARGE_LOWER_BOUND)
169 
170         private val HEIGHT_DP_BREAKPOINTS_V1 =
171             listOf(0, HEIGHT_DP_MEDIUM_LOWER_BOUND, HEIGHT_DP_EXPANDED_LOWER_BOUND)
172 
173         private val HEIGHT_DP_BREAKPOINTS_V2 = HEIGHT_DP_BREAKPOINTS_V1
174 
175         private fun createBreakpointSet(
176             widthBreakpoints: List<Int>,
177             heightBreakpoints: List<Int>
178         ): Set<WindowSizeClass> {
179             return widthBreakpoints
180                 .flatMap { widthBp ->
181                     heightBreakpoints.map { heightBp ->
182                         WindowSizeClass(minWidthDp = widthBp, minHeightDp = heightBp)
183                     }
184                 }
185                 .toSet()
186         }
187 
188         /**
189          * The recommended breakpoints for window size classes.
190          *
191          * @sample androidx.window.core.samples.layout.calculateWindowSizeClass
192          */
193         @JvmField
194         val BREAKPOINTS_V1 = createBreakpointSet(WIDTH_DP_BREAKPOINTS_V1, HEIGHT_DP_BREAKPOINTS_V1)
195 
196         /**
197          * The recommended breakpoints for window size classes. This includes all the breakpoints
198          * from [BREAKPOINTS_V1] plus new breakpoints to account for the Large and Extra Large width
199          * breakpoints.
200          *
201          * @sample androidx.window.core.samples.layout.calculateWindowSizeClass
202          */
203         @JvmField
204         val BREAKPOINTS_V2 = createBreakpointSet(WIDTH_DP_BREAKPOINTS_V2, HEIGHT_DP_BREAKPOINTS_V2)
205 
206         /**
207          * Computes the recommended [WindowSizeClass] for the given width and height in DP.
208          *
209          * @param dpWidth width of a window in DP.
210          * @param dpHeight height of a window in DP.
211          * @return [WindowSizeClass] that is recommended for the given dimensions.
212          * @throws IllegalArgumentException if [dpWidth] or [dpHeight] is negative.
213          */
214         @JvmStatic
215         @Deprecated(
216             "Use computeWindowSizeClass instead.",
217             ReplaceWith(
218                 "BREAKPOINTS_V1.computeWindowSizeClass(widthDp = dpWidth, heightDp = dpHeight)",
219                 "androidx.window.core.layout.computeWindowSizeClass"
220             )
221         )
222         fun compute(dpWidth: Float, dpHeight: Float): WindowSizeClass {
223             val widthDp =
224                 when {
225                     dpWidth >= WIDTH_DP_EXPANDED_LOWER_BOUND -> WIDTH_DP_EXPANDED_LOWER_BOUND
226                     dpWidth >= WIDTH_DP_MEDIUM_LOWER_BOUND -> WIDTH_DP_MEDIUM_LOWER_BOUND
227                     else -> 0
228                 }
229             val heightDp =
230                 when {
231                     dpHeight >= HEIGHT_DP_EXPANDED_LOWER_BOUND -> HEIGHT_DP_EXPANDED_LOWER_BOUND
232                     dpHeight >= HEIGHT_DP_MEDIUM_LOWER_BOUND -> HEIGHT_DP_MEDIUM_LOWER_BOUND
233                     else -> 0
234                 }
235             return WindowSizeClass(widthDp, heightDp)
236         }
237     }
238 }
239