-
Notifications
You must be signed in to change notification settings - Fork 155
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move DeviceUtils closer to call-site. Copy code from Material3-Adapti…
…ve library(to avoid experimental annotation propagation everywhere). Implement windowIsLarge to take care of device type handling(sort-of). Add some funky debug methods to make sure windowIsLarge works.
- Loading branch information
1 parent
a8efc9d
commit 2fb1cbe
Showing
6 changed files
with
380 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
amethyst/src/main/java/androidx/compose/material3/adaptive/Posture.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
/** | ||
* Copyright (c) 2024 Vitor Pamplona | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
* this software and associated documentation files (the "Software"), to deal in | ||
* the Software without restriction, including without limitation the rights to use, | ||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the | ||
* Software, and to permit persons to whom the Software is furnished to do so, | ||
* subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all | ||
* copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN | ||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
package androidx.compose.material3.adaptive | ||
|
||
import androidx.compose.runtime.Immutable | ||
import androidx.compose.ui.geometry.Rect | ||
import androidx.compose.ui.graphics.toComposeRect | ||
import androidx.window.layout.FoldingFeature | ||
|
||
/** | ||
* Calculates the [Posture] for a given list of [FoldingFeature]s. This methods converts framework | ||
* folding info into the Material-opinionated posture info. | ||
*/ | ||
fun calculatePosture(foldingFeatures: List<FoldingFeature>): Posture { | ||
var isTableTop = false | ||
val hingeList = mutableListOf<HingeInfo>() | ||
@Suppress("ListIterator") | ||
foldingFeatures.forEach { | ||
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL && | ||
it.state == FoldingFeature.State.HALF_OPENED | ||
) { | ||
isTableTop = true | ||
} | ||
hingeList.add( | ||
HingeInfo( | ||
bounds = it.bounds.toComposeRect(), | ||
isFlat = it.state == FoldingFeature.State.FLAT, | ||
isVertical = it.orientation == FoldingFeature.Orientation.VERTICAL, | ||
isSeparating = it.isSeparating, | ||
isOccluding = it.occlusionType == FoldingFeature.OcclusionType.FULL, | ||
), | ||
) | ||
} | ||
return Posture(isTableTop, hingeList) | ||
} | ||
|
||
/** | ||
* Posture info that can help make layout adaptation decisions. For example when | ||
* [Posture.separatingVerticalHingeBounds] is not empty, the layout may want to avoid putting any | ||
* content over those hinge area. We suggest to use [calculatePosture] to retrieve instances of this | ||
* class in applications, unless you have a strong need of customization that cannot be fulfilled by | ||
* the default implementation. | ||
* | ||
* Note that the hinge bounds will be represent as [Rect] with window coordinates, instead of layout | ||
* coordinate. | ||
* | ||
* @constructor create an instance of [Posture] | ||
* @property isTabletop `true` if the current window is considered as in the table top mode, i.e. | ||
* there is one half-opened horizontal hinge in the middle of the current window. When | ||
* this is `true` it usually means it's hard for users to interact with the window area | ||
* around the hinge and developers may consider separating the layout along the hinge and | ||
* show software keyboard or other controls in the bottom half of the window. | ||
* @property hingeList a list of all hinges that are relevant to the posture. | ||
*/ | ||
@Immutable | ||
class Posture( | ||
val isTabletop: Boolean = false, | ||
val hingeList: List<HingeInfo> = emptyList(), | ||
) { | ||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is Posture) return false | ||
if (isTabletop != other.isTabletop) return false | ||
if (hingeList != other.hingeList) return false | ||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = isTabletop.hashCode() | ||
result = 31 * result + hingeList.hashCode() | ||
return result | ||
} | ||
|
||
override fun toString(): String { | ||
@Suppress("ListIterator") | ||
return "Posture(isTabletop=$isTabletop, " + | ||
"hinges=[${hingeList.joinToString(", ")}])" | ||
} | ||
} | ||
|
||
/** | ||
* Returns the list of vertical hinge bounds that are separating. | ||
*/ | ||
val Posture.separatingVerticalHingeBounds get() = hingeList.getBounds { isVertical && isSeparating } | ||
|
||
/** | ||
* Returns the list of vertical hinge bounds that are occluding. | ||
*/ | ||
val Posture.occludingVerticalHingeBounds get() = hingeList.getBounds { isVertical && isOccluding } | ||
|
||
/** | ||
* Returns the list of all vertical hinge bounds. | ||
*/ | ||
val Posture.allVerticalHingeBounds get() = hingeList.getBounds { isVertical } | ||
|
||
/** | ||
* Returns the list of horizontal hinge bounds that are separating. | ||
*/ | ||
val Posture.separatingHorizontalHingeBounds | ||
get() = hingeList.getBounds { !isVertical && isSeparating } | ||
|
||
/** | ||
* Returns the list of horizontal hinge bounds that are occluding. | ||
*/ | ||
val Posture.occludingHorizontalHingeBounds | ||
get() = hingeList.getBounds { !isVertical && isOccluding } | ||
|
||
/** | ||
* Returns the list of all horizontal hinge bounds. | ||
*/ | ||
val Posture.allHorizontalHingeBounds | ||
get() = hingeList.getBounds { !isVertical } | ||
|
||
/** | ||
* A class that contains the info of a hinge relevant to a [Posture]. | ||
* | ||
* @param bounds the bounds of the hinge in the relevant viewport. | ||
* @param isFlat `true` if the hinge is fully open and the relevant window space presented to the | ||
* user is flat. | ||
* @param isVertical `true` if the hinge is a vertical one, i.e., it separates the viewport into | ||
* left and right; `false` if the hinge is horizontal, i.e., it separates the viewport | ||
* into top and bottom. | ||
* @param isSeparating `true` if the hinge creates two logical display areas. | ||
* @param isOccluding `true` if the hinge conceals part of the display. | ||
*/ | ||
@Immutable | ||
class HingeInfo( | ||
val bounds: Rect, | ||
val isFlat: Boolean, | ||
val isVertical: Boolean, | ||
val isSeparating: Boolean, | ||
val isOccluding: Boolean, | ||
) { | ||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is HingeInfo) return false | ||
if (bounds != other.bounds) return false | ||
if (isFlat != other.isFlat) return false | ||
if (isVertical != other.isVertical) return false | ||
if (isSeparating != other.isSeparating) return false | ||
if (isOccluding != other.isOccluding) return false | ||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = bounds.hashCode() | ||
result = 31 * result + isFlat.hashCode() | ||
result = 31 * result + isVertical.hashCode() | ||
result = 31 * result + isSeparating.hashCode() | ||
result = 31 * result + isOccluding.hashCode() | ||
return result | ||
} | ||
|
||
override fun toString(): String = | ||
"HingeInfo(bounds=$bounds, " + | ||
"isFlat=$isFlat, " + | ||
"isVertical=$isVertical, " + | ||
"isSeparating=$isSeparating, " + | ||
"isOccluding=$isOccluding)" | ||
} | ||
|
||
private inline fun List<HingeInfo>.getBounds(predicate: HingeInfo.() -> Boolean): List<Rect> = | ||
@Suppress("ListIterator") | ||
mapNotNull { if (it.predicate()) it.bounds else null } |
122 changes: 122 additions & 0 deletions
122
amethyst/src/main/java/androidx/compose/material3/adaptive/WindowAdaptiveInfo.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/** | ||
* Copyright (c) 2024 Vitor Pamplona | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
* this software and associated documentation files (the "Software"), to deal in | ||
* the Software without restriction, including without limitation the rights to use, | ||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the | ||
* Software, and to permit persons to whom the Software is furnished to do so, | ||
* subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in all | ||
* copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN | ||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
*/ | ||
package androidx.compose.material3.adaptive | ||
|
||
import android.app.Activity | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.Immutable | ||
import androidx.compose.runtime.State | ||
import androidx.compose.runtime.collectAsState | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.platform.LocalConfiguration | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.platform.LocalDensity | ||
import androidx.compose.ui.unit.IntSize | ||
import androidx.compose.ui.unit.toSize | ||
import androidx.window.core.layout.WindowSizeClass | ||
import androidx.window.layout.FoldingFeature | ||
import androidx.window.layout.WindowInfoTracker | ||
import androidx.window.layout.WindowMetricsCalculator | ||
import kotlinx.coroutines.flow.map | ||
|
||
@Composable | ||
fun currentWindowAdaptiveInfo(): WindowAdaptiveInfo { | ||
val windowSize = | ||
with(LocalDensity.current) { | ||
currentWindowSize().toSize().toDpSize() | ||
} | ||
return WindowAdaptiveInfo( | ||
WindowSizeClass.compute(windowSize.width.value, windowSize.height.value), | ||
calculatePosture(collectFoldingFeaturesAsState().value), | ||
) | ||
} | ||
|
||
/** | ||
* Returns and automatically update the current window size from [WindowMetricsCalculator]. | ||
* | ||
* @return an [IntSize] that represents the current window size. | ||
*/ | ||
@Composable | ||
fun currentWindowSize(): IntSize { | ||
// Observe view configuration changes and recalculate the size class on each change. We can't | ||
// use Activity#onConfigurationChanged as this will sometimes fail to be called on different | ||
// API levels, hence why this function needs to be @Composable so we can observe the | ||
// ComposeView's configuration changes. | ||
LocalConfiguration.current | ||
val windowBounds = | ||
WindowMetricsCalculator | ||
.getOrCreate() | ||
.computeCurrentWindowMetrics(LocalContext.current) | ||
.bounds | ||
return IntSize(windowBounds.width(), windowBounds.height()) | ||
} | ||
|
||
/** | ||
* Collects the current window folding features from [WindowInfoTracker] in to a [State]. | ||
* | ||
* @return a [State] of a [FoldingFeature] list. | ||
*/ | ||
@Composable | ||
fun collectFoldingFeaturesAsState(): State<List<FoldingFeature>> { | ||
val context = LocalContext.current | ||
return remember(context) { | ||
if (context is Activity) { | ||
// TODO(b/284347941) remove the instance check after the test bug is fixed. | ||
WindowInfoTracker | ||
.getOrCreate(context) | ||
.windowLayoutInfo(context) | ||
} else { | ||
WindowInfoTracker | ||
.getOrCreate(context) | ||
.windowLayoutInfo(context) | ||
}.map { it.displayFeatures.filterIsInstance<FoldingFeature>() } | ||
}.collectAsState(emptyList()) | ||
} | ||
|
||
/** | ||
* This class collects window info that affects adaptation decisions. An adaptive layout is supposed | ||
* to use the info from this class to decide how the layout is supposed to be adapted. | ||
* | ||
* @constructor create an instance of [WindowAdaptiveInfo] | ||
* @param windowSizeClass [WindowSizeClass] of the current window. | ||
* @param windowPosture [Posture] of the current window. | ||
*/ | ||
@Immutable | ||
class WindowAdaptiveInfo( | ||
val windowSizeClass: WindowSizeClass, | ||
val windowPosture: Posture, | ||
) { | ||
override fun equals(other: Any?): Boolean { | ||
if (this === other) return true | ||
if (other !is WindowAdaptiveInfo) return false | ||
if (windowSizeClass != other.windowSizeClass) return false | ||
if (windowPosture != other.windowPosture) return false | ||
return true | ||
} | ||
|
||
override fun hashCode(): Int { | ||
var result = windowSizeClass.hashCode() | ||
result = 31 * result + windowPosture.hashCode() | ||
return result | ||
} | ||
|
||
override fun toString(): String = "WindowAdaptiveInfo(windowSizeClass=$windowSizeClass, windowPosture=$windowPosture)" | ||
} |
Oops, something went wrong.