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