Update TwoPaneLayout to use WindowState (#13)

* Update gradle version

* Update dependencies

* Update TwoPaneLayout to use WindowState

* Update TwoPaneLayout tests

* Update sample and sample tests

* Make sure all pixel values are converted the same way
This commit is contained in:
Kristen Halper 2022-02-09 16:43:02 -05:00 коммит произвёл GitHub
Родитель a2b4061478
Коммит b8969f4b37
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 304 добавлений и 351 удалений

Просмотреть файл

@ -22,8 +22,8 @@ ext {
// ----------------------------------
gradlePluginVersion = '7.0.3'
kotlinVersion = "1.5.31"
gradlePluginVersion = '7.1.0'
kotlinVersion = "1.6.10"
compileSdkVersion = 31
targetSdkVersion = compileSdkVersion
minSdkVersion = 23
@ -44,10 +44,9 @@ ext {
]
// AndroidX dependencies
appCompatVersion = "1.3.0"
ktxCoreVersion = "1.5.0"
windowVersion = "1.0.0-beta04"
appCompatVersion = '1.4.1'
ktxCoreVersion = '1.7.0'
windowVersion = "1.0.0"
androidxDependencies = [
appCompat : "androidx.appcompat:appcompat:$appCompatVersion",
ktxCore : "androidx.core:core-ktx:$ktxCoreVersion",
@ -55,10 +54,9 @@ ext {
]
// Compose dependencies
composeVersion = "1.0.5"
composeVersion = "1.1.0-rc03"
activityComposeVersion = "1.4.0"
navigationComposeVersion = "2.4.0-beta02"
navigationComposeVersion = "2.4.0"
composeDependencies = [
composeMaterial : "androidx.compose.material:material:$composeVersion",
composeUI : "androidx.compose.ui:ui:$composeVersion",
@ -79,9 +77,16 @@ ext {
]
// Google dependencies
materialVersion = "1.5.0-alpha01"
materialVersion = "1.5.0"
googleDependencies = [
material: "com.google.android.material:material:$materialVersion"
]
// Microsoft dependencies
windowStateVersion = '1.0.0-alpha02'
composeTestingVersion = '1.0.0-alpha01'
microsoftDependencies = [
windowState : "com.microsoft.device.dualscreen:windowstate:$windowStateVersion",
composeTesting : "com.microsoft.device.dualscreen.testing:testing-compose:$composeTestingVersion",
]
}

Просмотреть файл

@ -19,3 +19,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.disableAutomaticComponentCreation=true

Просмотреть файл

@ -1,6 +1,6 @@
#Thu Apr 15 16:13:21 PDT 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

Просмотреть файл

@ -18,7 +18,7 @@ dependencies {
task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
mainClass.set("com.pinterest.ktlint.Main")
args "src/**/*.kt"
// to generate report in checkstyle format prepend following args:
// "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml"
@ -28,6 +28,6 @@ task ktlint(type: JavaExec, group: "verification") {
task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
classpath = configurations.ktlint
main = "com.pinterest.ktlint.Main"
mainClass.set("com.pinterest.ktlint.Main")
args "-F", "src/**/*.kt"
}

Просмотреть файл

@ -23,6 +23,8 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.twoPaneLayoutVersionCode
versionName rootProject.ext.twoPaneLayoutVersionName
buildConfigField("int", "VERSION_CODE", "${defaultConfig.versionCode}")
buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -53,24 +55,28 @@ android {
composeOptions {
kotlinCompilerExtensionVersion composeVersion
}
packagingOptions {
exclude 'META-INF/LGPL2.1'
exclude 'META-INF/AL2.0'
resources {
excludes += ['META-INF/LGPL2.1', 'META-INF/AL2.0']
}
}
}
dependencies {
implementation kotlinDependencies.kotlinStdlib
implementation androidxDependencies.ktxCore
implementation androidxDependencies.appCompat
implementation androidxDependencies.window
implementation composeDependencies.composeUI
implementation composeDependencies.composeMaterial
implementation composeDependencies.composeUITooling
implementation composeDependencies.activityCompose
implementation composeDependencies.navigationCompose
implementation microsoftDependencies.windowState
androidTestImplementation(testDependencies.androidxTestCore)
androidTestImplementation(testDependencies.androidxTestRules)
androidTestImplementation(testDependencies.androidxTestRunner)

Просмотреть файл

@ -26,12 +26,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import com.microsoft.device.dualscreen.twopanelayout.screenState.ScreenState
import androidx.compose.ui.unit.dp
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
@ -98,14 +101,23 @@ open class LayoutTest {
@Composable
internal fun MockTwoPaneLayout(
screenState: ScreenState,
windowState: WindowState,
constraints: Constraints,
firstPane: @Composable TwoPaneScope.() -> Unit,
secondPane: @Composable TwoPaneScope.() -> Unit
) {
val pane1SizePx: Size
val pane2SizePx: Size
with(LocalDensity.current) {
val pane1SizeDp = windowState.pane1SizeDp()
val pane2SizeDp = windowState.pane2SizeDp()
pane1SizePx = Size(pane1SizeDp.width.dp.toPx(), pane1SizeDp.height.dp.toPx())
pane2SizePx = Size(pane2SizeDp.width.dp.toPx(), pane2SizeDp.height.dp.toPx())
}
val measurePolicy = twoPaneMeasurePolicy(
orientation = screenState.orientation,
paneSize = screenState.paneSize,
windowMode = windowState.windowMode,
paneSizes = arrayOf(pane1SizePx, pane2SizePx),
mockConstraints = constraints
)
Layout(

Просмотреть файл

@ -6,18 +6,18 @@
package com.microsoft.device.dualscreen.twopanelayout
import android.graphics.Rect
import android.graphics.RectF
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.microsoft.device.dualscreen.twopanelayout.screenState.DeviceType
import com.microsoft.device.dualscreen.twopanelayout.screenState.LayoutOrientation
import com.microsoft.device.dualscreen.twopanelayout.screenState.LayoutState
import com.microsoft.device.dualscreen.twopanelayout.screenState.ScreenState
import com.microsoft.device.dualscreen.windowstate.WindowMode
import com.microsoft.device.dualscreen.windowstate.WindowState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
@ -29,32 +29,55 @@ import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
class TwoPaneTest : LayoutTest() {
/**
* Check that isSinglePane returns the correct value for all postures when in TwoPane mode
*/
@Test
fun isSinglePaneCheck_withSinglePane() {
val layoutState = LayoutState.Fold
fun isSinglePaneCheck_withTwoPaneMode() {
val paneMode = TwoPaneMode.TwoPane
val orientation = LayoutOrientation.Horizontal
val isSinglePane = isSinglePaneLayout(layoutState, paneMode, orientation)
assertTrue(isSinglePane)
for (windowMode in WindowMode.values()) {
val isSinglePane = isSinglePaneLayout(windowMode, paneMode)
when {
windowMode.isDualScreen -> assert(!isSinglePane)
else -> assert(isSinglePane)
}
}
}
/**
* Check that isSinglePane returns the correct value for all postures when in HorizontalSingle mode
*/
@Test
fun isSinglePaneCheck_withHorizontalSingleMode() {
val layoutState = LayoutState.Open
val paneMode = TwoPaneMode.HorizontalSingle
val orientation = LayoutOrientation.Horizontal
val isSinglePane = isSinglePaneLayout(layoutState, paneMode, orientation)
assertTrue(isSinglePane)
for (windowMode in WindowMode.values()) {
val isSinglePane = isSinglePaneLayout(windowMode, paneMode)
when (windowMode) {
WindowMode.DUAL_PORTRAIT -> assert(!isSinglePane)
else -> assert(isSinglePane)
}
}
}
/**
* Check that isSinglePane returns the correct value for all postures when in VerticalSingle mode
*/
@Test
fun isSinglePaneCheck_withDualPane() {
val layoutState = LayoutState.Open
fun isSinglePaneCheck_withVerticalSingleMode() {
val paneMode = TwoPaneMode.VerticalSingle
val orientation = LayoutOrientation.Horizontal
val isSinglePane = isSinglePaneLayout(layoutState, paneMode, orientation)
assertTrue(!isSinglePane)
for (windowMode in WindowMode.values()) {
val isSinglePane = isSinglePaneLayout(windowMode, paneMode)
when (windowMode) {
WindowMode.DUAL_LANDSCAPE -> assert(!isSinglePane)
else -> assert(isSinglePane)
}
}
}
@Test
@ -74,7 +97,7 @@ class TwoPaneTest : LayoutTest() {
Modifier
.onGloballyPositioned { coordinates ->
childSize[0] = coordinates.size
childPosition[0] = coordinates.positionInRoot()
childPosition[0] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}
@ -85,7 +108,7 @@ class TwoPaneTest : LayoutTest() {
Modifier
.onGloballyPositioned { coordinates ->
childSize[1] = coordinates.size
childPosition[1] = coordinates.positionInRoot()
childPosition[1] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}
@ -110,28 +133,43 @@ class TwoPaneTest : LayoutTest() {
val height = 600
val hingeBounds = Rect(390, 0, 410, 600)
val constraints = Constraints(width, width, height, height)
val screenState = ScreenState(
deviceType = DeviceType.Dual,
screenSize = Size(width.toFloat(), height.toFloat()),
hingeBounds = hingeBounds,
orientation = LayoutOrientation.Vertical,
layoutState = LayoutState.Open
)
var widthDp: Dp
var heightDp: Dp
var hingeBoundsDp: RectF
val drawLatch = CountDownLatch(2)
val childSize = arrayOfNulls<IntSize>(2)
val childPosition = arrayOfNulls<Offset>(2)
activityTestRule.setContent {
with(LocalDensity.current) {
widthDp = width.toDp()
heightDp = height.toDp()
val left = hingeBounds.left.toDp().value
val top = hingeBounds.top.toDp().value
val right = hingeBounds.right.toDp().value
val bottom = hingeBounds.bottom.toDp().value
hingeBoundsDp = RectF(left, top, right, bottom)
}
Container(width = width, height = height) {
MockTwoPaneLayout(
screenState = screenState,
windowState = WindowState(
hasFold = true,
foldIsHorizontal = false,
foldBoundsDp = hingeBoundsDp,
foldIsSeparating = true,
windowWidthDp = widthDp,
windowHeightDp = heightDp,
),
constraints = constraints,
firstPane = {
Container(
Modifier
.onGloballyPositioned { coordinates ->
childSize[0] = coordinates.size
childPosition[0] = coordinates.positionInRoot()
childPosition[0] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}
@ -141,7 +179,7 @@ class TwoPaneTest : LayoutTest() {
Modifier
.onGloballyPositioned { coordinates ->
childSize[1] = coordinates.size
childPosition[1] = coordinates.positionInRoot()
childPosition[1] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}
@ -163,25 +201,28 @@ class TwoPaneTest : LayoutTest() {
@Test
fun tablet_withWeight() {
val width = 800
val height = 1200
val hingeBounds = Rect()
val width = 3300
val height = 4000
val constraints = Constraints(width, width, height, height)
val screenState = ScreenState(
deviceType = DeviceType.Big,
screenSize = Size(width.toFloat(), height.toFloat()),
hingeBounds = hingeBounds,
orientation = LayoutOrientation.Horizontal,
layoutState = LayoutState.Open
)
var widthDp: Dp
var heightDp: Dp
val drawLatch = CountDownLatch(2)
val childSize = arrayOfNulls<IntSize>(2)
val childPosition = arrayOfNulls<Offset>(2)
activityTestRule.setContent {
with(LocalDensity.current) {
widthDp = width.toDp()
heightDp = height.toDp()
}
Container(width = width, height = height) {
MockTwoPaneLayout(
screenState = screenState,
windowState = WindowState(
windowWidthDp = widthDp,
windowHeightDp = heightDp,
),
constraints = constraints,
firstPane = {
Container(
@ -189,7 +230,7 @@ class TwoPaneTest : LayoutTest() {
.weight(.4f)
.onGloballyPositioned { coordinates ->
childSize[0] = coordinates.size
childPosition[0] = coordinates.positionInRoot()
childPosition[0] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}
@ -200,7 +241,7 @@ class TwoPaneTest : LayoutTest() {
.weight(.6f)
.onGloballyPositioned { coordinates ->
childSize[1] = coordinates.size
childPosition[1] = coordinates.positionInRoot()
childPosition[1] = coordinates.positionInParent()
drawLatch.countDown()
}
) {}

Просмотреть файл

@ -5,27 +5,24 @@
package com.microsoft.device.dualscreen.twopanelayout
import android.graphics.Rect
import android.app.Activity
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.microsoft.device.dualscreen.twopanelayout.screenState.ConfigScreenState
import com.microsoft.device.dualscreen.twopanelayout.screenState.DeviceType
import com.microsoft.device.dualscreen.twopanelayout.screenState.LayoutOrientation
import com.microsoft.device.dualscreen.twopanelayout.screenState.LayoutState
import com.microsoft.device.dualscreen.twopanelayout.screenState.ScreenState
import com.microsoft.device.dualscreen.windowstate.WindowMode
import com.microsoft.device.dualscreen.windowstate.WindowState
import com.microsoft.device.dualscreen.windowstate.rememberWindowState
/**
* TwoPaneMode
@ -61,24 +58,10 @@ fun TwoPaneLayout(
pane1: @Composable TwoPaneScope.() -> Unit,
pane2: @Composable TwoPaneScope.() -> Unit
) {
var screenState by remember {
mutableStateOf(
ScreenState(
deviceType = DeviceType.Single,
screenSize = Size.Zero,
hingeBounds = Rect(),
orientation = LayoutOrientation.Horizontal,
layoutState = LayoutState.Fold
)
)
}
ConfigScreenState(onStateChange = { screenState = it })
// REVISIT: not sure if this cast is safe
val windowState = (LocalContext.current as Activity).rememberWindowState()
val isSinglePane = isSinglePaneLayout(
layoutState = screenState.layoutState,
paneMode = paneMode,
orientation = screenState.orientation
)
val isSinglePane = isSinglePaneLayout(windowState.windowMode, paneMode)
if (isSinglePane) {
SinglePaneContainer(
@ -87,7 +70,7 @@ fun TwoPaneLayout(
)
} else {
TwoPaneContainer(
screenState = screenState,
windowState = windowState,
modifier = modifier,
pane1 = pane1,
pane2 = pane2
@ -156,14 +139,23 @@ internal fun SinglePaneContainer(
*/
@Composable
private fun TwoPaneContainer(
screenState: ScreenState,
windowState: WindowState,
modifier: Modifier,
pane1: @Composable TwoPaneScope.() -> Unit,
pane2: @Composable TwoPaneScope.() -> Unit
) {
val pane1SizePx: Size
val pane2SizePx: Size
with(LocalDensity.current) {
val pane1SizeDp = windowState.pane1SizeDp()
val pane2SizeDp = windowState.pane2SizeDp()
pane1SizePx = Size(pane1SizeDp.width.dp.toPx(), pane1SizeDp.height.dp.toPx())
pane2SizePx = Size(pane2SizeDp.width.dp.toPx(), pane2SizeDp.height.dp.toPx())
}
val measurePolicy = twoPaneMeasurePolicy(
orientation = screenState.orientation,
paneSize = screenState.paneSize,
windowMode = windowState.windowMode,
paneSizes = arrayOf(pane1SizePx, pane2SizePx),
)
Layout(
content = {
@ -176,13 +168,12 @@ private fun TwoPaneContainer(
}
internal fun isSinglePaneLayout(
layoutState: LayoutState,
windowMode: WindowMode,
paneMode: TwoPaneMode,
orientation: LayoutOrientation
): Boolean {
return layoutState == LayoutState.Fold ||
paneMode == TwoPaneMode.VerticalSingle && orientation == LayoutOrientation.Vertical ||
paneMode == TwoPaneMode.HorizontalSingle && orientation == LayoutOrientation.Horizontal
return !windowMode.isDualScreen ||
paneMode == TwoPaneMode.VerticalSingle && windowMode == WindowMode.DUAL_PORTRAIT ||
paneMode == TwoPaneMode.HorizontalSingle && windowMode == WindowMode.DUAL_LANDSCAPE
}
@LayoutScopeMarker

Просмотреть файл

@ -16,13 +16,13 @@ import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import com.microsoft.device.dualscreen.twopanelayout.screenState.LayoutOrientation
import com.microsoft.device.dualscreen.windowstate.WindowMode
import kotlin.math.roundToInt
@Composable
internal fun twoPaneMeasurePolicy(
orientation: LayoutOrientation,
paneSize: Size,
windowMode: WindowMode,
paneSizes: Array<Size>,
mockConstraints: Constraints = Constraints(0, 0, 0, 0)
): MeasurePolicy {
return MeasurePolicy { measurables, constraints ->
@ -53,7 +53,7 @@ internal fun twoPaneMeasurePolicy(
placeables = if (maxWeight == 0f || maxWeight * 2 == totalWeight) {
measureTwoPaneEqually(
constraints = childrenConstraints,
paneSize = paneSize,
paneSizes = paneSizes,
measurables = measurables
)
} else {
@ -61,7 +61,7 @@ internal fun twoPaneMeasurePolicy(
constraints = childrenConstraints,
measurables = measurables,
totalWeight = totalWeight,
orientation = orientation,
windowMode = windowMode,
twoPaneParentData = twoPaneParentData
)
}
@ -70,10 +70,10 @@ internal fun twoPaneMeasurePolicy(
layout(childrenConstraints.maxWidth, childrenConstraints.maxHeight) {
placeables.forEachIndexed { index, placeable ->
placeTwoPaneEqually(
orientation = orientation,
windowMode = windowMode,
placeable = placeable,
index = index,
paneSize = paneSize,
lastPaneSize = paneSizes[1],
constraints = childrenConstraints
)
}
@ -82,7 +82,7 @@ internal fun twoPaneMeasurePolicy(
layout(childrenConstraints.maxWidth, childrenConstraints.maxHeight) {
placeables.forEachIndexed { index, placeable ->
placeTwoPaneProportionally(
orientation = orientation,
windowMode = windowMode,
placeable = placeable,
index = index,
twoPaneParentData = twoPaneParentData,
@ -95,34 +95,42 @@ internal fun twoPaneMeasurePolicy(
}
}
/*
/**
* to measure the two panes for dual-screen/foldable/large-screen without weight,
* or with two equal weight
*/
private fun measureTwoPaneEqually(
constraints: Constraints,
paneSize: Size,
paneSizes: Array<Size>,
measurables: List<Measurable>
): List<Placeable> {
val paneWidth = paneSize.width.toInt()
val paneHeight = paneSize.height.toInt()
val placeables = emptyList<Placeable>().toMutableList()
for (i in measurables.indices) {
val paneWidth = paneSizes[i].width.roundToInt()
val paneHeight = paneSizes[i].height.roundToInt()
val childConstraints = Constraints(
minWidth = constraints.minWidth.coerceAtMost(paneWidth),
minHeight = constraints.minHeight.coerceAtMost(paneHeight),
maxWidth = constraints.maxWidth.coerceAtMost(paneWidth),
maxHeight = constraints.maxHeight.coerceAtMost(paneHeight)
)
return measurables.map { it.measure(childConstraints) }
placeables.add(measurables[i].measure(childConstraints))
}
return placeables
}
/*
/**
* to measure the pane for dual-screen with two non-equal weight
*/
private fun measureTwoPaneProportionally(
constraints: Constraints,
measurables: List<Measurable>,
totalWeight: Float,
orientation: LayoutOrientation,
windowMode: WindowMode,
twoPaneParentData: Array<TwoPaneParentData?>
): List<Placeable> {
val minWidth = constraints.minWidth
@ -136,14 +144,16 @@ private fun measureTwoPaneProportionally(
val weight = parentData.weight
var childConstraints: Constraints
val ratio = weight / totalWeight
childConstraints = if (orientation == LayoutOrientation.Vertical) {
childConstraints = when (windowMode) {
WindowMode.DUAL_PORTRAIT -> {
Constraints(
minWidth = (minWidth * ratio).roundToInt(),
minHeight = minHeight,
maxWidth = (maxWidth * ratio).roundToInt(),
maxHeight = maxHeight
)
} else {
}
WindowMode.DUAL_LANDSCAPE -> {
Constraints(
minWidth = minWidth,
minHeight = (minHeight * ratio).roundToInt(),
@ -151,48 +161,56 @@ private fun measureTwoPaneProportionally(
maxHeight = (maxHeight * ratio).roundToInt()
)
}
else -> throw IllegalStateException("[measureTwoPaneProportionally] Error: single pane window mode ($windowMode) found inside TwoPaneContainer")
}
val placeable = measurables[i].measure(childConstraints)
placeables.add(placeable)
}
return placeables
}
private fun Placeable.PlacementScope.placeTwoPaneEqually(
orientation: LayoutOrientation,
windowMode: WindowMode,
placeable: Placeable,
index: Int,
paneSize: Size,
lastPaneSize: Size,
constraints: Constraints
) {
if (orientation == LayoutOrientation.Vertical) {
when (windowMode) {
WindowMode.DUAL_PORTRAIT -> {
var xPosition = 0 // for the first pane
if (index != 0) { // for the second pane
val lastPaneWidth = paneSize.width.toInt()
val lastPaneWidth = lastPaneSize.width.roundToInt()
val firstPaneWidth = constraints.maxWidth - lastPaneWidth
xPosition += firstPaneWidth
}
placeable.place(x = xPosition, y = 0)
} else {
}
WindowMode.DUAL_LANDSCAPE -> {
var yPosition = 0
if (index != 0) {
val lastPaneHeight = paneSize.height.toInt()
val lastPaneHeight = lastPaneSize.height.roundToInt()
val firstPaneHeight = constraints.maxHeight - lastPaneHeight
yPosition += firstPaneHeight
}
placeable.place(x = 0, y = yPosition)
}
else -> throw IllegalStateException("[placeTwoPaneEqually] Error: single pane window mode ($windowMode) found inside TwoPaneContainer")
}
}
private fun Placeable.PlacementScope.placeTwoPaneProportionally(
orientation: LayoutOrientation,
windowMode: WindowMode,
placeable: Placeable,
index: Int,
twoPaneParentData: Array<TwoPaneParentData?>,
constraints: Constraints,
totalWeight: Float
) {
if (orientation == LayoutOrientation.Vertical) {
when (windowMode) {
WindowMode.DUAL_PORTRAIT -> {
var xPosition = 0 // for the first pane
if (index != 0) { // for the second pane
val parentData = twoPaneParentData[index]
@ -202,7 +220,8 @@ private fun Placeable.PlacementScope.placeTwoPaneProportionally(
xPosition += firstPaneWidth
}
placeable.place(x = xPosition, y = 0)
} else {
}
WindowMode.DUAL_LANDSCAPE -> {
var yPosition = 0
if (index != 0) {
val parentData = twoPaneParentData[index]
@ -213,6 +232,8 @@ private fun Placeable.PlacementScope.placeTwoPaneProportionally(
}
placeable.place(x = 0, y = yPosition)
}
else -> throw IllegalStateException("[placeTwoPaneProportionally] Error: single pane window mode ($windowMode) found inside TwoPaneContainer")
}
}
private val IntrinsicMeasurable.data: TwoPaneParentData?

Просмотреть файл

@ -1,73 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.twopanelayout.screenState
import android.graphics.Rect
import androidx.compose.ui.geometry.Size
/**
* LayoutOrientation
* Horizontal, the width of hinge/folding line is bigger than the height, top/bottom
* Vertical the height of hinge/folding line is bigger than the width, left/right
*/
enum class LayoutOrientation {
Horizontal,
Vertical
}
/**
* LayoutState
* Open, two-pane layout display, it is always "Open" for big-screen device
* Fold single layout display, including single-screen phone, foldable device in folding mode and app in un-spanned mode
*/
enum class LayoutState {
Open,
Fold
}
/**
* DeviceType
* Single, // regular single-screen device, such as single-screen phone
* Dual, // dual-screen/foldable device, such as Surface Duo device, Samsung Galaxy Fold 2
* Big // large-screen device, such as tablet
*/
enum class DeviceType {
Single,
Dual,
Big
}
class ScreenState(
val deviceType: DeviceType,
val screenSize: Size,
var hingeBounds: Rect,
var orientation: LayoutOrientation,
var layoutState: LayoutState
) {
val paneSize: Size
get() {
if (deviceType == DeviceType.Big) {
return if (orientation == LayoutOrientation.Vertical) {
Size(width = screenSize.width / 2, height = screenSize.height)
} else {
Size(width = screenSize.width, height = screenSize.height / 2)
}
} else if (deviceType == DeviceType.Dual) {
return if (orientation == LayoutOrientation.Vertical) {
Size(
width = hingeBounds.left.toFloat(),
height = hingeBounds.height().toFloat()
)
} else {
Size(
width = hingeBounds.width().toFloat(),
height = hingeBounds.top.toFloat()
)
}
}
return screenSize
}
}

Просмотреть файл

@ -1,80 +0,0 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
package com.microsoft.device.dualscreen.twopanelayout.screenState
import android.app.Activity
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import kotlinx.coroutines.flow.collect
const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585
@Composable
fun ConfigScreenState(onStateChange: (ScreenState) -> Unit) {
val context = LocalContext.current
val activity = context as Activity
val windowInfoRep = WindowInfoTracker.getOrCreate(context)
val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp
val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP
var deviceType = if (isTablet) DeviceType.Big else DeviceType.Single
var layoutState = if (isTablet) LayoutState.Open else LayoutState.Fold
val screenHeight = LocalConfiguration.current.screenHeightDp * LocalDensity.current.density
val screenWidth = LocalConfiguration.current.screenWidthDp * LocalDensity.current.density
var orientation = orientationMappingFromScreen(LocalConfiguration.current.orientation)
LaunchedEffect(windowInfoRep) {
windowInfoRep.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
var featureBounds = Rect()
if (newLayoutInfo.displayFeatures.isNotEmpty()) {
val foldingFeature = newLayoutInfo.displayFeatures.first() as FoldingFeature
featureBounds = foldingFeature.bounds
layoutState = if (foldingFeature.isSeparating) LayoutState.Open else LayoutState.Fold
orientation = orientationMappingFromFoldingFeature(foldingFeature.orientation)
deviceType = DeviceType.Dual
}
val screenState = ScreenState(
deviceType = deviceType,
screenSize = Size(width = screenWidth, height = screenHeight),
hingeBounds = featureBounds,
orientation = orientation,
layoutState = layoutState
)
onStateChange(screenState)
}
}
}
private fun orientationMappingFromFoldingFeature(original: FoldingFeature.Orientation): LayoutOrientation {
return if (original == FoldingFeature.Orientation.VERTICAL) {
LayoutOrientation.Vertical
} else {
LayoutOrientation.Horizontal
}
}
/**
* For tablet or single-screen device
* Portrait orientation will dual-landscape mode, which is treated as using horizontal hinge
* Landscape orientation will be dual-portrait mode, which is treated as using vertical hinge
*/
private fun orientationMappingFromScreen(original: Int): LayoutOrientation {
return if (original == ORIENTATION_PORTRAIT) {
LayoutOrientation.Horizontal
} else {
LayoutOrientation.Vertical
}
}

Просмотреть файл

@ -36,11 +36,12 @@ android {
composeOptions {
kotlinCompilerExtensionVersion composeVersion
}
packagingOptions {
exclude 'META-INF/LGPL2.1'
exclude 'META-INF/AL2.0'
resources {
excludes += ['META-INF/LGPL2.1', 'META-INF/AL2.0']
}
}
}
dependencies {
@ -62,6 +63,7 @@ dependencies {
androidTestImplementation(testDependencies.androidxTestRunner)
androidTestImplementation(testDependencies.composeUITest)
androidTestImplementation(testDependencies.composeJunit)
androidTestImplementation(microsoftDependencies.composeTesting)
debugImplementation(testDependencies.composeUITestManifest)
}

Просмотреть файл

@ -9,14 +9,27 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.microsoft.device.dualscreen.testing.createWindowLayoutInfoPublisherRule
import com.microsoft.device.dualscreen.testing.getString
import com.microsoft.device.dualscreen.testing.simulateHorizontalFoldingFeature
import com.microsoft.device.dualscreen.testing.simulateVerticalFoldingFeature
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
class SampleTest {
private val publisherRule = createWindowLayoutInfoPublisherRule()
private val composeTestRule = createAndroidComposeRule<MainActivity>()
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@get: Rule
val testRule: TestRule
init {
testRule = RuleChain.outerRule(publisherRule).around(composeTestRule)
RuleChain.outerRule(composeTestRule)
}
@Before
fun setUp() {
@ -25,25 +38,37 @@ class SampleTest {
}
}
private val appName = "TwoPaneLayoutSample"
private val firstPaneText = "First pane: TwoPaneLayout is a UI component for Jetpack Compose, which contains the layouts that help you create UI for dual-screen, foldable, and large-screen devices. TwoPaneLayout provides a two-pane layout for use at the top level of a UI. The component will place two panes side-by-side on dual-screen, foldable, and large-screen devices and one pane only on regular single-screen devices. The two panes can be horizontal or vertical, based on the orientation of the device, unless paneMode is configured."
private val secondPaneText = "Second pane: The element layout is based on the order, which means the first element will be placed in the first pane and the second element will be placed in the second pane. The TwoPaneLayout is able to assign children widths according to their weights provided using the TwoPaneScope.weight modifier. When none of its children have weights, the first child element will be prioritized when the app is running on a regular single-screen device, or when foldable and dual-screen devices become folded or unspanned. If the app is spanned or unfolded on foldable and dual-screen, or large-screen devices, the two elements will be laid out equally to take all the display area."
@Test
fun app_launches() {
composeTestRule.onNodeWithText(appName).assertIsDisplayed()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.app_name)).assertIsDisplayed()
}
@Test
fun app_canNavigateToSecondPane() {
composeTestRule.onNodeWithText(firstPaneText).performClick()
composeTestRule.onNodeWithText(secondPaneText).assertIsDisplayed()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.first_pane_text)).performClick()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.second_pane_text)).assertIsDisplayed()
}
@Test
fun app_canNavigateToFirstPane() {
composeTestRule.onNodeWithText(firstPaneText).performClick()
composeTestRule.onNodeWithText(secondPaneText).performClick()
composeTestRule.onNodeWithText(firstPaneText).assertIsDisplayed()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.first_pane_text)).performClick()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.second_pane_text)).performClick()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.first_pane_text)).assertIsDisplayed()
}
@Test
fun app_dualPortrait_showsTwoPanes() {
publisherRule.simulateVerticalFoldingFeature(composeTestRule)
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.first_pane_text)).assertIsDisplayed()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.second_pane_text)).assertIsDisplayed()
}
@Test
fun app_dualLandscape_showsOnePane() {
publisherRule.simulateHorizontalFoldingFeature(composeTestRule)
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.first_pane_text)).assertIsDisplayed()
composeTestRule.onNodeWithText(composeTestRule.getString(R.string.second_pane_text)).assertDoesNotExist()
}
}

Просмотреть файл

@ -58,19 +58,21 @@ fun MainPage() {
pane1 = {
Text(
text = stringResource(R.string.first_pane_text),
modifier = Modifier.fillMaxSize().background(color = Color.Cyan)
.clickable {
navigateToPane2()
}
modifier = Modifier
.fillMaxSize()
.background(color = Color.Cyan)
.clickable { navigateToPane2() },
color = Color.Black
)
},
pane2 = {
Text(
text = stringResource(R.string.second_pane_text),
modifier = Modifier.fillMaxSize().background(color = Color.Magenta)
.clickable {
navigateToPane1()
}
modifier = Modifier
.fillMaxSize()
.background(color = Color.Magenta)
.clickable { navigateToPane1() },
color = Color.Black
)
}
)