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