/* * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.compose.material3.windowsizeclass import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach /** * Window size classes are a set of opinionated viewport breakpoints to design, develop, and test * responsive application layouts against. * For more details check Support different screen sizes documentation. * * WindowSizeClass contains a [WindowWidthSizeClass] and [WindowHeightSizeClass], representing the * window size classes for this window's width and height respectively. * * See [calculateWindowSizeClass] to calculate the WindowSizeClass for an Activity's current window * * @property widthSizeClass width-based window size class ([WindowWidthSizeClass]) * @property heightSizeClass height-based window size class ([WindowHeightSizeClass]) */ @Immutable class WindowSizeClass private constructor( val widthSizeClass: WindowWidthSizeClass, val heightSizeClass: WindowHeightSizeClass ) { companion object { /** * Calculates the best matched [WindowSizeClass] for a given [size] according to * the provided [supportedWidthSizeClasses] and [supportedHeightSizeClasses]. * * @param size of the window * @param supportedWidthSizeClasses the set of width size classes that are supported * @param supportedHeightSizeClasses the set of height size classes that are supported * @return [WindowSizeClass] corresponding to the given width and height */ @ExperimentalMaterial3WindowSizeClassApi fun calculateFromSize( size: DpSize, supportedWidthSizeClasses: Set = WindowWidthSizeClass.DefaultSizeClasses, supportedHeightSizeClasses: Set = WindowHeightSizeClass.DefaultSizeClasses ): WindowSizeClass { val windowWidthSizeClass = WindowWidthSizeClass.fromWidth( size.width, supportedWidthSizeClasses ) val windowHeightSizeClass = WindowHeightSizeClass.fromHeight( size.height, supportedHeightSizeClasses ) return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass) } } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false other as WindowSizeClass if (widthSizeClass != other.widthSizeClass) return false if (heightSizeClass != other.heightSizeClass) return false return true } override fun hashCode(): Int { var result = widthSizeClass.hashCode() result = 31 * result + heightSizeClass.hashCode() return result } override fun toString() = "WindowSizeClass($widthSizeClass, $heightSizeClass)" } /** * Width-based window size class. * * A window size class represents a breakpoint that can be used to build responsive layouts. Each * window size class breakpoint represents a majority case for typical device scenarios so your * layouts will work well on most devices and configurations. * * For more details see Window size classes documentation. */ @Immutable @kotlin.jvm.JvmInline value class WindowWidthSizeClass private constructor(private val value: Int) : Comparable { override operator fun compareTo(other: WindowWidthSizeClass) = breakpoint().compareTo(other.breakpoint()) override fun toString(): String { return "WindowWidthSizeClass." + when (this) { Compact -> "Compact" Medium -> "Medium" Expanded -> "Expanded" else -> "" } } companion object { /** Represents the majority of phones in portrait. */ val Compact = WindowWidthSizeClass(0) /** * Represents the majority of tablets in portrait and large unfolded inner displays in * portrait. */ val Medium = WindowWidthSizeClass(1) /** * Represents the majority of tablets in landscape and large unfolded inner displays in * landscape. */ val Expanded = WindowWidthSizeClass(2) /** * The default set of size classes that includes [Compact], [Medium], and [Expanded] size * classes. Should never expand to ensure behavioral consistency. */ @Suppress("PrimitiveInCollection") val DefaultSizeClasses = setOf(Compact, Medium, Expanded) @Suppress("PrimitiveInCollection") private val AllSizeClassList = listOf(Expanded, Medium, Compact) /** * The set of all size classes. It's supposed to be expanded whenever a new size class is * defined. By default [WindowSizeClass.calculateFromSize] will only return size classes in * [DefaultSizeClasses] in order to avoid behavioral changes when new size classes are * added. You can opt in to support all available size classes by doing: * ``` * WindowSizeClass.calculateFromSize( * size = size, * density = density, * supportedWidthSizeClasses = WindowWidthSizeClass.AllSizeClasses, * supportedHeightSizeClasses = WindowHeightSizeClass.AllSizeClasses * ) * ``` */ @Suppress("ListIterator", "PrimitiveInCollection") val AllSizeClasses = AllSizeClassList.toSet() private fun WindowWidthSizeClass.breakpoint(): Dp { return when { this == Expanded -> 840.dp this == Medium -> 600.dp else -> 0.dp } } /** * Calculates the best matched [WindowWidthSizeClass] for a given [width] in Pixels and * a given [Density] from [supportedSizeClasses]. */ internal fun fromWidth( width: Dp, supportedSizeClasses: Set ): WindowWidthSizeClass { require(width >= 0.dp) { "Width must not be negative" } require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" } var smallestSupportedSizeClass = Compact AllSizeClassList.fastForEach { if (it in supportedSizeClasses) { if (width >= it.breakpoint()) { return it } smallestSupportedSizeClass = it } } // If none of the size classes matches, return the largest one. return smallestSupportedSizeClass } } } /** * Height-based window size class. * * A window size class represents a breakpoint that can be used to build responsive layouts. Each * window size class breakpoint represents a majority case for typical device scenarios so your * layouts will work well on most devices and configurations. * * For more details see Window size classes documentation. */ @Immutable @kotlin.jvm.JvmInline value class WindowHeightSizeClass private constructor(private val value: Int) : Comparable { override operator fun compareTo(other: WindowHeightSizeClass) = breakpoint().compareTo(other.breakpoint()) override fun toString(): String { return "WindowHeightSizeClass." + when (this) { Compact -> "Compact" Medium -> "Medium" Expanded -> "Expanded" else -> "" } } companion object { /** Represents the majority of phones in landscape */ val Compact = WindowHeightSizeClass(0) /** Represents the majority of tablets in landscape and majority of phones in portrait */ val Medium = WindowHeightSizeClass(1) /** Represents the majority of tablets in portrait */ val Expanded = WindowHeightSizeClass(2) /** * The default set of size classes that includes [Compact], [Medium], and [Expanded] size * classes. Should never expand to ensure behavioral consistency. */ @Suppress("PrimitiveInCollection") val DefaultSizeClasses = setOf(Compact, Medium, Expanded) @Suppress("PrimitiveInCollection") private val AllSizeClassList = listOf(Expanded, Medium, Compact) /** * The set of all size classes. It's supposed to be expanded whenever a new size class is * defined. By default [WindowSizeClass.calculateFromSize] will only return size classes in * [DefaultSizeClasses] in order to avoid behavioral changes when new size classes are * added. You can opt in to support all available size classes by doing: * ``` * WindowSizeClass.calculateFromSize( * size = size, * density = density, * supportedWidthSizeClasses = WindowWidthSizeClass.AllSizeClasses, * supportedHeightSizeClasses = WindowHeightSizeClass.AllSizeClasses * ) * ``` */ @Suppress("ListIterator", "PrimitiveInCollection") val AllSizeClasses = AllSizeClassList.toSet() private fun WindowHeightSizeClass.breakpoint(): Dp { return when { this == Expanded -> 900.dp this == Medium -> 480.dp else -> 0.dp } } /** * Calculates the best matched [WindowHeightSizeClass] for a given [height] in Pixels and * a given [Density] from [supportedSizeClasses]. */ internal fun fromHeight( height: Dp, supportedSizeClasses: Set ): WindowHeightSizeClass { require(height >= 0.dp) { "Width must not be negative" } require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" } var smallestSupportedSizeClass = Expanded AllSizeClassList.fastForEach { if (it in supportedSizeClasses) { if (height >= it.breakpoint()) { return it } smallestSupportedSizeClass = it } } // If none of the size classes matches, return the largest one. return smallestSupportedSizeClass } } }