From 27da5f57513b02918b1a3468f8dca9c599c87c3f Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 2 Aug 2022 15:37:20 -0700 Subject: [PATCH 01/10] Add adaptive project shell --- README.md | 3 + adaptive/README.md | 21 ++++ adaptive/build.gradle | 114 ++++++++++++++++++ adaptive/gradle.properties | 3 + adaptive/src/main/AndroidManifest.xml | 18 +++ .../src/test/resources/robolectric.properties | 3 + settings.gradle | 1 + 7 files changed, 163 insertions(+) create mode 100644 adaptive/README.md create mode 100644 adaptive/build.gradle create mode 100644 adaptive/gradle.properties create mode 100644 adaptive/src/main/AndroidManifest.xml create mode 100644 adaptive/src/test/resources/robolectric.properties diff --git a/README.md b/README.md index d4404bfd7..45ee6fe42 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ A library that provides a layout implementing the swipe-to-refresh UX pattern, s ### 🌏 [Web](./web/) A wrapper around WebView for basic WebView support in Jetpack Compose. +### 📜 [Adaptive](./adaptive/) +A library providing a collection of utilities for adaptive layouts. + ### 📐 [Insets](./insets/) (Deprecated) See our [Migration Guide](https://google.github.io/accompanist/insets/) for migrating to Insets in Compose. diff --git a/adaptive/README.md b/adaptive/README.md new file mode 100644 index 000000000..60647889d --- /dev/null +++ b/adaptive/README.md @@ -0,0 +1,21 @@ +# Adaptive utilities for Jetpack Compose + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-adaptive)](https://search.maven.org/search?q=g:com.google.accompanist) + +For more information, visit the documentation: https://google.github.io/accompanist/adaptive + +## Download + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "com.google.accompanist:accompanist-adaptive:" +} +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit. + + [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-adaptive/ \ No newline at end of file diff --git a/adaptive/build.gradle b/adaptive/build.gradle new file mode 100644 index 000000000..eb01d7676 --- /dev/null +++ b/adaptive/build.gradle @@ -0,0 +1,114 @@ +/* + * 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 + * + * https://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. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'org.jetbrains.dokka' +} + +kotlin { + explicitApi() +} + +android { + compileSdkVersion 32 + + defaultConfig { + minSdkVersion 21 + // targetSdkVersion has no effect for libraries. This is only used for the test APK + targetSdkVersion 32 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + buildConfig false + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } + + lintOptions { + textReport true + textOutput 'stdout' + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks + checkReleaseBuilds false + } + + packagingOptions { + // Some of the META-INF files conflict with coroutines-test. Exclude them to enable + // our test APK to build (has no effect on our AARs) + excludes += "/META-INF/AL2.0" + excludes += "/META-INF/LGPL2.1" + } + + testOptions { + unitTests { + includeAndroidResources = true + } + animationsDisabled true + } + + sourceSets { + test { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + androidTest { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + } +} + +dependencies { + api libs.compose.foundation.foundation + + implementation libs.napier + + // ====================== + // Test dependencies + // ====================== + + androidTestImplementation project(':internal-testutils') + testImplementation project(':internal-testutils') + + androidTestImplementation libs.junit + testImplementation libs.junit + + androidTestImplementation libs.truth + testImplementation libs.truth + + androidTestImplementation libs.compose.ui.test.junit4 + testImplementation libs.compose.ui.test.junit4 + + androidTestImplementation libs.compose.ui.test.manifest + testImplementation libs.compose.ui.test.manifest + + androidTestImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.runner + + testImplementation libs.robolectric +} + +apply plugin: "com.vanniktech.maven.publish" diff --git a/adaptive/gradle.properties b/adaptive/gradle.properties new file mode 100644 index 000000000..51234ce9a --- /dev/null +++ b/adaptive/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=accompanist-adaptive +POM_NAME=Accompanist Adaptive library +POM_PACKAGING=aar \ No newline at end of file diff --git a/adaptive/src/main/AndroidManifest.xml b/adaptive/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e66addd9e --- /dev/null +++ b/adaptive/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/adaptive/src/test/resources/robolectric.properties b/adaptive/src/test/resources/robolectric.properties new file mode 100644 index 000000000..2806eaffa --- /dev/null +++ b/adaptive/src/test/resources/robolectric.properties @@ -0,0 +1,3 @@ +# Pin SDK to 30 since Robolectric does not currently support API 31: +# https://github.com/robolectric/robolectric/issues/6635 +sdk=30 diff --git a/settings.gradle b/settings.gradle index 017fefa3b..8968085a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ gradleEnterprise { } } +include ':adaptive' include ':internal-testutils' include ':insets' include ':insets-ui' From f9a96199b00a4143773ced82b2909ebdbbd75e9c Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 4 Aug 2022 17:50:39 -0700 Subject: [PATCH 02/10] Add WindowGeometry and TwoPane --- adaptive/api/current.api | 40 + adaptive/build.gradle | 8 +- .../google/accompanist/adaptive/TwoPane.kt | 411 ++++ .../accompanist/adaptive/WindowGeometry.kt | 72 + .../accompanist/adaptive/TwoPaneTest.kt | 1799 +++++++++++++++++ .../adaptive/WindowGeometryTest.kt | 155 ++ appcompat-theme/build.gradle | 2 +- drawablepainter/build.gradle | 2 +- flowlayout/build.gradle | 2 +- gradle/libs.versions.toml | 10 +- insets-ui/build.gradle | 2 +- insets/build.gradle | 2 +- internal-testutils/build.gradle | 2 +- navigation-animation/build.gradle | 2 +- navigation-material/build.gradle | 2 +- pager-indicators/build.gradle | 2 +- pager/build.gradle | 2 +- permissions/build.gradle | 2 +- placeholder-material/build.gradle | 2 +- placeholder/build.gradle | 2 +- sample/build.gradle | 3 +- swiperefresh/build.gradle | 2 +- systemuicontroller/build.gradle | 2 +- web/build.gradle | 2 +- 24 files changed, 2510 insertions(+), 20 deletions(-) create mode 100644 adaptive/api/current.api create mode 100644 adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt create mode 100644 adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt create mode 100644 adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt create mode 100644 adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt diff --git a/adaptive/api/current.api b/adaptive/api/current.api new file mode 100644 index 000000000..b7594ecbc --- /dev/null +++ b/adaptive/api/current.api @@ -0,0 +1,40 @@ +// Signature format: 4.0 +package com.google.accompanist.adaptive { + + public final class SplitResult { + ctor public SplitResult(boolean isHorizontalSplit, androidx.compose.ui.geometry.Rect splitArea); + method public androidx.compose.ui.geometry.Rect getSplitArea(); + method public boolean isHorizontalSplit(); + property public final boolean isHorizontalSplit; + property public final androidx.compose.ui.geometry.Rect splitArea; + } + + public final class TwoPaneKt { + method public static com.google.accompanist.adaptive.TwoPaneStrategy DelegateTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy firstStrategy, com.google.accompanist.adaptive.TwoPaneStrategy secondStrategy, kotlin.jvm.functions.Function3 useFirstStrategy); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetHorizontalTwoPaneStrategy(float splitOffset, boolean offsetFromStart, optional float splitWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetVerticalTwoPaneStrategy(float splitOffset, boolean offsetFromTop, optional float splitHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionHorizontalTwoPaneStrategy(float splitFraction, optional float splitWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionVerticalTwoPaneStrategy(float splitFraction, optional float splitHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); + method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, optional androidx.compose.ui.Modifier modifier); + method public static com.google.accompanist.adaptive.TwoPaneStrategy TwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); + } + + @androidx.compose.runtime.Stable public fun interface TwoPaneStrategy { + method public com.google.accompanist.adaptive.SplitResult calculateSplitResult(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates); + } + + public interface WindowGeometry { + method public java.util.List getDisplayFeatures(); + method public androidx.compose.material3.windowsizeclass.WindowSizeClass getWindowSizeClass(); + property public abstract java.util.List displayFeatures; + property public abstract androidx.compose.material3.windowsizeclass.WindowSizeClass windowSizeClass; + } + + public final class WindowGeometryKt { + method @androidx.compose.runtime.Composable public static com.google.accompanist.adaptive.WindowGeometry calculateWindowGeometry(android.app.Activity activity); + } + +} + diff --git a/adaptive/build.gradle b/adaptive/build.gradle index eb01d7676..d4689258e 100644 --- a/adaptive/build.gradle +++ b/adaptive/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 @@ -83,6 +83,9 @@ android { dependencies { api libs.compose.foundation.foundation + api libs.compose.ui.ui + api libs.compose.material3.windowSizeClass + api libs.androidx.window implementation libs.napier @@ -108,6 +111,9 @@ dependencies { androidTestImplementation libs.androidx.test.runner testImplementation libs.androidx.test.runner + androidTestImplementation libs.androidx.window.testing + testImplementation libs.androidx.window.testing + testImplementation libs.robolectric } diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt new file mode 100644 index 000000000..5ea5eb452 --- /dev/null +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -0,0 +1,411 @@ +/* + * 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 + * + * https://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 com.google.accompanist.adaptive + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.constrain +import androidx.compose.ui.unit.constrainHeight +import androidx.compose.ui.unit.constrainWidth +import androidx.compose.ui.unit.dp +import androidx.window.layout.FoldingFeature +import kotlin.math.roundToInt + +/** + * A layout that places two different pieces of content defined by the [first] and [second] + * slots where the arrangement, sizes and separation behaviour is controlled by [TwoPaneStrategy]. + * + * The default implementations of [TwoPaneStrategy] are fold and hinges aware, meaning that the two + * pane will adopt its layout to properly separate [first] and [second] panes so they don't + * interfere with hardware hinges (vertical or horizontal), or respect folds when needed + * (for example, when foldable is half-folded (90-degree fold AKA tabletop) the split will become + * on the bend). + * + * The [TwoPane] layout will always place both [first] and [second], based on the provided + * [strategy] and window environment. If you instead only want to place one or the other, + * that should be controlled at a higher level and not calling [TwoPane] if placing both is not + * desired. + * + * @param first the first content of the layout, a left-most in LTR, a right-most in RTL and + * top-most in a vertical split based on the [SplitResult] of [TwoPaneStrategy.calculateSplitResult] + * @param second the second content of the layout, a right-most in the LTR, a left-most in the RTL + * and the bottom-most in a vertical split based on the [SplitResult] of + * [TwoPaneStrategy.calculateSplitResult] + * @param strategy strategy of the two pane that controls the arrangement of the layout + * @param modifier an optional modifier for the layout + */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TwoPane( + first: @Composable () -> Unit, + second: @Composable () -> Unit, + strategy: TwoPaneStrategy, + modifier: Modifier = Modifier +) { + val density = LocalDensity.current + Layout( + modifier = modifier.wrapContentSize(), + content = { + Box(Modifier.layoutId("first")) { + first() + } + Box(Modifier.layoutId("second")) { + second() + } + } + ) { measurable, constraints -> + val firstMeasurable = measurable.find { it.layoutId == "first" }!! + val secondMeasurable = measurable.find { it.layoutId == "second" }!! + + layout(constraints.maxWidth, constraints.maxHeight) { + val splitResult = strategy.calculateSplitResult( + density = density, + layoutDirection = layoutDirection, + layoutCoordinates = checkNotNull(coordinates) { + "TwoPane does not support the use of alignment lines!" + } + ) + + val isHorizontalSplit = splitResult.isHorizontalSplit + val splitArea = splitResult.splitArea + + val splitLeft = constraints.constrainWidth(splitArea.left.roundToInt()) + val splitRight = constraints.constrainWidth(splitArea.right.roundToInt()) + val splitTop = constraints.constrainHeight(splitArea.top.roundToInt()) + val splitBottom = constraints.constrainHeight(splitArea.bottom.roundToInt()) + val firstConstraints = + if (isHorizontalSplit) { + val width = when (layoutDirection) { + LayoutDirection.Ltr -> splitLeft + LayoutDirection.Rtl -> constraints.maxWidth - splitRight + } + + constraints.copy(minWidth = width, maxWidth = width) + } else { + constraints.copy(minHeight = splitTop, maxHeight = splitTop) + } + val secondConstraints = + if (isHorizontalSplit) { + val width = when (layoutDirection) { + LayoutDirection.Ltr -> constraints.maxWidth - splitRight + LayoutDirection.Rtl -> splitLeft + } + constraints.copy(minWidth = width, maxWidth = width) + } else { + val height = constraints.maxHeight - splitBottom + constraints.copy(minHeight = height, maxHeight = height) + } + val firstPlaceable = firstMeasurable.measure(constraints.constrain(firstConstraints)) + val secondPlaceable = secondMeasurable.measure(constraints.constrain(secondConstraints)) + + firstPlaceable.placeRelative(0, 0) + val detailOffsetX = + if (isHorizontalSplit) { + constraints.maxWidth - secondPlaceable.width + } else { + 0 + } + val detailOffsetY = + if (isHorizontalSplit) { + 0 + } else { + constraints.maxHeight - secondPlaceable.height + } + secondPlaceable.placeRelative(detailOffsetX, detailOffsetY) + } + } +} + +/** + * A strategy for configuring the [TwoPane] component, that is responsible for the meta-data + * corresponding to the arrangement of the two panes of the layout. + */ +@Stable +fun interface TwoPaneStrategy { + /** + * Calculates the split result in local bounds. + */ + fun calculateSplitResult( + density: Density, + layoutDirection: LayoutDirection, + layoutCoordinates: LayoutCoordinates + ): SplitResult +} + +/** + * Returns the specification for where to place a split in [TwoPane] as a result of + * [TwoPaneStrategy.calculateSplitResult] + */ +class SplitResult( + + /** + * Whether the split is vertical or horizontal + */ + val isHorizontalSplit: Boolean, + + /** + * The area that is nether a `start` pane or an `end` pane, but a separation between those two. + * In case width or height is 0 - it means that the area itself is a 0 width/height, but the + * place within the layout is still defined. + */ + val splitArea: Rect, +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a + * horizontal or vertical fold respectively. + * + * If there is no fold, then the [fallbackStrategy] will be used instead. + */ +public fun TwoPaneStrategy( + fallbackStrategy: TwoPaneStrategy, + windowGeometry: WindowGeometry, +): TwoPaneStrategy = HorizontalTwoPaneStrategy( + fallbackStrategy = VerticalTwoPaneStrategy( + fallbackStrategy = fallbackStrategy, + windowGeometry = windowGeometry, + ), + windowGeometry = windowGeometry +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold. + * + * If there is no horizontal fold, then the [fallbackStrategy] will be used instead. + */ +public fun HorizontalTwoPaneStrategy( + fallbackStrategy: TwoPaneStrategy, + windowGeometry: WindowGeometry, +): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + val verticalFold = windowGeometry.displayFeatures.find { + it is FoldingFeature && it.orientation == FoldingFeature.Orientation.VERTICAL + } as FoldingFeature? + + if (verticalFold != null && + ( + verticalFold.isSeparating || + verticalFold.occlusionType == FoldingFeature.OcclusionType.FULL + ) && + verticalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow()) + ) { + val foldBounds = verticalFold.bounds.toComposeRect() + SplitResult( + isHorizontalSplit = true, + splitArea = Rect( + layoutCoordinates.windowToLocal(foldBounds.topLeft), + layoutCoordinates.windowToLocal(foldBounds.bottomRight) + ) + ) + } else { + fallbackStrategy.calculateSplitResult(density, layoutDirection, layoutCoordinates) + } +} + +/** + * Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold. + * + * If there is no vertical fold, then the [fallbackStrategy] will be used instead. + */ +public fun VerticalTwoPaneStrategy( + fallbackStrategy: TwoPaneStrategy, + windowGeometry: WindowGeometry, +): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + val horizontalFold = windowGeometry.displayFeatures.find { + it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL + } as FoldingFeature? + + if (horizontalFold != null && + ( + horizontalFold.isSeparating || + horizontalFold.occlusionType == FoldingFeature.OcclusionType.FULL + ) && + horizontalFold.bounds.toComposeRect().overlaps(layoutCoordinates.boundsInWindow()) + ) { + val foldBounds = horizontalFold.bounds.toComposeRect() + SplitResult( + isHorizontalSplit = false, + splitArea = Rect( + layoutCoordinates.windowToLocal(foldBounds.topLeft), + layoutCoordinates.windowToLocal(foldBounds.bottomRight) + ) + ) + } else { + fallbackStrategy.calculateSplitResult(density, layoutDirection, layoutCoordinates) + } +} + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * The split will be placed at the given [splitFraction] from start, with the given [splitWidth]. + * + * This strategy is _not_ fold aware. + */ +public fun FractionHorizontalTwoPaneStrategy( + splitFraction: Float, + splitWidth: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + val splitX = layoutCoordinates.size.width * when (layoutDirection) { + LayoutDirection.Ltr -> splitFraction + LayoutDirection.Rtl -> 1 - splitFraction + } + val splitWidthPixel = with(density) { splitWidth.toPx() } + + SplitResult( + isHorizontalSplit = true, + splitArea = Rect( + left = splitX - splitWidthPixel / 2f, + top = 0f, + right = splitX + splitWidthPixel / 2f, + bottom = layoutCoordinates.size.height.toFloat(), + ) + ) +} + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * The split will be placed at [splitOffset] either from the start or end based on + * [offsetFromStart], with the given [splitWidth]. + * + * This strategy is _not_ fold aware. + */ +public fun FixedOffsetHorizontalTwoPaneStrategy( + splitOffset: Dp, + offsetFromStart: Boolean, + splitWidth: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + val splitOffsetPixel = with(density) { splitOffset.toPx() } + val splitX = when (layoutDirection) { + LayoutDirection.Ltr -> + if (offsetFromStart) { + splitOffsetPixel + } else { + layoutCoordinates.size.width - splitOffsetPixel + } + LayoutDirection.Rtl -> + if (offsetFromStart) { + layoutCoordinates.size.width - splitOffsetPixel + } else { + splitOffsetPixel + } + } + val splitWidthPixel = with(density) { splitWidth.toPx() } + + SplitResult( + isHorizontalSplit = true, + splitArea = Rect( + left = splitX - splitWidthPixel / 2f, + top = 0f, + right = splitX + splitWidthPixel / 2f, + bottom = layoutCoordinates.size.height.toFloat(), + ) + ) +} + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * The split will be placed at the given [splitFraction] from start, with the given [splitHeight]. + * + * This strategy is _not_ fold aware. + */ +public fun FractionVerticalTwoPaneStrategy( + splitFraction: Float, + splitHeight: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates -> + val splitY = layoutCoordinates.size.height * splitFraction + val splitHeightPixel = with(density) { splitHeight.toPx() } + + SplitResult( + isHorizontalSplit = false, + splitArea = Rect( + left = 0f, + top = splitY - splitHeightPixel / 2f, + right = layoutCoordinates.size.width.toFloat(), + bottom = splitY + splitHeightPixel / 2f, + ) + ) +} + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * The split will be placed at [splitOffset] either from the top or bottom based on + * [offsetFromTop], with the given [splitHeight]. + * + * This strategy is _not_ fold aware. + */ +public fun FixedOffsetVerticalTwoPaneStrategy( + splitOffset: Dp, + offsetFromTop: Boolean, + splitHeight: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates -> + val splitOffsetPixel = with(density) { splitOffset.toPx() } + val splitY = + if (offsetFromTop) { + splitOffsetPixel + } else { + layoutCoordinates.size.height - splitOffsetPixel + } + val splitHeightPixel = with(density) { splitHeight.toPx() } + + SplitResult( + isHorizontalSplit = false, + splitArea = Rect( + left = 0f, + top = splitY - splitHeightPixel / 2f, + right = layoutCoordinates.size.width.toFloat(), + bottom = splitY + splitHeightPixel / 2f, + ) + ) +} + +/** + * Returns a [DelegateTwoPaneStrategy] that will delegate [TwoPaneStrategy.calculateSplitResult] to + * the [firstStrategy] when [useFirstStrategy] returns `true`. Otherwise, it will delegate to + * the [secondStrategy]. + */ +public fun DelegateTwoPaneStrategy( + firstStrategy: TwoPaneStrategy, + secondStrategy: TwoPaneStrategy, + useFirstStrategy: ( + density: Density, + layoutDirection: LayoutDirection, + layoutCoordinates: LayoutCoordinates + ) -> Boolean, +): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + if (useFirstStrategy(density, layoutDirection, layoutCoordinates)) { + firstStrategy + } else { + secondStrategy + }.calculateSplitResult(density, layoutDirection, layoutCoordinates) +} diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt new file mode 100644 index 000000000..2d71d6819 --- /dev/null +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt @@ -0,0 +1,72 @@ +/* + * 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 + * + * https://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 com.google.accompanist.adaptive + +import android.app.Activity +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.window.layout.DisplayFeature +import androidx.window.layout.WindowInfoTracker + +/** + * A description of the current window geometry. + */ +public interface WindowGeometry { + + /** + * The current [WindowSizeClass]. + */ + val windowSizeClass: WindowSizeClass + + /** + * The current list of known [DisplayFeature]s. + */ + val displayFeatures: List +} + +/** + * Calculates the [WindowGeometry] for the given [activity]. + */ +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +public fun calculateWindowGeometry(activity: Activity): WindowGeometry { + val windowSizeClass = calculateWindowSizeClass(activity) + + val windowInfoTracker = remember(activity) { WindowInfoTracker.getOrCreate(activity) } + val windowLayoutInfo = remember(windowInfoTracker, activity) { + windowInfoTracker.windowLayoutInfo(activity) + } + + val displayFeatures by produceState(initialValue = emptyList()) { + windowLayoutInfo.collect { info -> + value = info.displayFeatures + } + } + + return object : WindowGeometry { + override val windowSizeClass: WindowSizeClass + get() = windowSizeClass + + override val displayFeatures: List + get() = displayFeatures + } +} diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt new file mode 100644 index 000000000..c09789ca8 --- /dev/null +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -0,0 +1,1799 @@ +/* + * 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 + * + * https://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 com.google.accompanist.adaptive + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toAndroidRect +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toIntRect +import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.unit.toSize +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.core.ExperimentalWindowApi +import androidx.window.layout.DisplayFeature +import androidx.window.layout.FoldingFeature +import androidx.window.testing.layout.FoldingFeature +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@RunWith(AndroidJUnit4::class) +class TwoPaneTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun fraction_horizontal_renders_correctly_ltr() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(300.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(300.dp, 0.dp), + DpSize(600.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fraction_horizontal_renders_correctly_rtl() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(600.dp, 0.dp), + DpSize(300.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(600.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fraction_horizontal_renders_correctly_with_split_width_ltr() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f, + splitWidth = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(268.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(332.dp, 0.dp), + DpSize(568.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fraction_horizontal_renders_correctly_with_split_width_rtl() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f, + splitWidth = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(632.dp, 0.dp), + DpSize(268.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(568.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fraction_vertical_renders_correctly() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 400.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 400.dp), + DpSize(900.dp, 800.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fraction_vertical_renders_correctly_with_split_height() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f, + splitHeight = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 368.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 432.dp), + DpSize(900.dp, 768.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_ltr() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = 200.dp, + offsetFromStart = true, + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(200.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(200.dp, 0.dp), + DpSize(700.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_horizontal_from_start_horizontal_renders_correctly_rtl() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = 200.dp, + offsetFromStart = true, + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(700.dp, 0.dp), + DpSize(200.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(700.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_ltr() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = 200.dp, + offsetFromStart = true, + splitWidth = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(168.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(232.dp, 0.dp), + DpSize(668.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_horizontal_from_start_renders_correctly_with_split_width_rtl() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = 200.dp, + offsetFromStart = true, + splitWidth = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(732.dp, 0.dp), + DpSize(168.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(668.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_vertical_from_top_renders_correctly() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = true + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 300.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 300.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_vertical_from_top_renders_correctly_with_split_height() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = true, + splitHeight = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 268.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 332.dp), + DpSize(900.dp, 868.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_vertical_from_bottom_renders_correctly() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = false + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 900.dp), + DpSize(900.dp, 300.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun fixed_offset_vertical_from_bottom_renders_correctly_with_split_height() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = 300.dp, + offsetFromTop = false, + splitHeight = 64.dp + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 868.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 932.dp), + DpSize(900.dp, 268.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_fallback_when_no_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + override val windowSizeClass: WindowSizeClass = + WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)) + override val displayFeatures: List = + emptyList() + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 400.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 400.dp), + DpSize(900.dp, 800.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_vertical_placing_when_occluding_horizontal_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 600.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 600.dp), + DpSize(900.dp, 600.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_vertical_placing_when_separating_horizontal_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 60.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 570.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 630.dp), + DpSize(900.dp, 570.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_fallback_when_non_occluding_horizontal_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 400.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 400.dp), + DpSize(900.dp, 800.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_horizontal_placing_when_occluding_vertical_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(450.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(450.dp, 0.dp), + DpSize(450.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_horizontal_placing_when_separating_vertical_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 64.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(418.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(482.dp, 0.dp), + DpSize(418.dp, 1200.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun two_pane_strategy_uses_fallback_when_non_occluding_vertical_fold_present() { + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + strategy = TwoPaneStrategy( + fallbackStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = 1f / 3f + ), + windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + ), + modifier = Modifier + .requiredSize(900.dp, 1200.dp) + .onPlaced { twoPaneCoordinates = it } + ) + } + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 400.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 400.dp), + DpSize(900.dp, 800.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun delegate_renders_correctly_when_changing_size_ltr() { + var width by mutableStateOf(900.dp) + var height by mutableStateOf(1200.dp) + + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + // Strategy: If we are taller than we are wide, use a vertical strategy with the + // second slot in the bottom 1/4. Otherwise, use a horizontal strategy with the + // second slot at the end 1/4. + strategy = DelegateTwoPaneStrategy( + firstStrategy = FractionVerticalTwoPaneStrategy(0.75f), + secondStrategy = FractionHorizontalTwoPaneStrategy(0.75f), + useFirstStrategy = { _, _, layoutCoordinates -> + layoutCoordinates.size.height >= layoutCoordinates.size.width + } + ), + modifier = Modifier + .requiredSize(width, height) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + // Initial state: we are taller than we are wide, so second should in the bottom 1/4 of the + // screen + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 900.dp), + DpSize(900.dp, 300.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + + // Swap width and height + width = 1200.dp + height = 900.dp + + composeTestRule.waitForIdle() + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(900.dp, 0.dp), + DpSize(300.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } + + @Test + fun delegate_renders_correctly_when_changing_size_rtl() { + var width by mutableStateOf(900.dp) + var height by mutableStateOf(1200.dp) + + lateinit var density: Density + lateinit var twoPaneCoordinates: LayoutCoordinates + lateinit var firstCoordinates: LayoutCoordinates + lateinit var secondCoordinates: LayoutCoordinates + + composeTestRule.setContent { + density = LocalDensity.current + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + TwoPane( + first = { + Spacer( + Modifier + .background(Color.Red) + .fillMaxSize() + .onPlaced { firstCoordinates = it } + ) + }, + second = { + Spacer( + Modifier + .background(Color.Blue) + .fillMaxSize() + .onPlaced { secondCoordinates = it } + ) + }, + // Strategy: If we are taller than we are wide, use a vertical strategy with the + // second slot in the bottom 1/4. Otherwise, use a horizontal strategy with the + // second slot at the end 1/4. + strategy = DelegateTwoPaneStrategy( + firstStrategy = FractionVerticalTwoPaneStrategy(0.75f), + secondStrategy = FractionHorizontalTwoPaneStrategy(0.75f), + useFirstStrategy = { _, _, layoutCoordinates -> + layoutCoordinates.size.height >= layoutCoordinates.size.width + } + ), + modifier = Modifier + .requiredSize(width, height) + .onPlaced { twoPaneCoordinates = it } + ) + } + } + + // Initial state: we are taller than we are wide, so second should in the bottom 1/4 of the + // screen + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 900.dp), + DpSize(900.dp, 300.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + + // Swap width and height + width = 1200.dp + height = 900.dp + + composeTestRule.waitForIdle() + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(300.dp, 0.dp), + DpSize(900.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), + 0.001f + ) + + compareRectWithTolerance( + with(density) { + DpRect( + DpOffset(0.dp, 0.dp), + DpSize(300.dp, 900.dp) + ).toRect().round().toRect() + }, + twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), + 0.001f + ) + } +} + +private fun compareRectWithTolerance( + expected: Rect, + actual: Rect, + tolerance: Float, +) { + assertThat(actual.left).isWithin(tolerance).of(expected.left) + assertThat(actual.right).isWithin(tolerance).of(expected.right) + assertThat(actual.top).isWithin(tolerance).of(expected.top) + assertThat(actual.bottom).isWithin(tolerance).of(expected.bottom) +} + +/** + * A descriptor of a [FoldingFeature] but with the [center] and [size] specified relative to the + * to the coordinates of the [TwoPane] layout. + */ +private data class LocalFoldingFeature( + val center: Dp, + val size: Dp, + val state: FoldingFeature.State, + val orientation: FoldingFeature.Orientation +) + +/** + * Folding features are always expressed in window coordinates. + * + * For the sake of testing, however, we want to specify them relative to the [TwoPane] under test. + * + * Therefore, this method takes in a list of [LocalFoldingFeature]s and the [TwoPane] layout info + * in order to map the [LocalFoldingFeature]s into real [FoldingFeature] with the proper window + * coordinates. + * + * In other words, this allows specifying [LocalFoldingFeature]s as if the [TwoPane] layout matches + * the window bounds. + */ +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalWindowApi::class) +private fun fakeWindowGeometry( + density: Density, + twoPaneCoordinates: LayoutCoordinates, + localFoldingFeatures: List +): WindowGeometry = + object : WindowGeometry { + override val windowSizeClass: WindowSizeClass = + WindowSizeClass.calculateFromSize(twoPaneCoordinates.size.toSize().toDpSize(density)) + + override val displayFeatures: List get() { + val boundsTopLeftOffset = twoPaneCoordinates.localToWindow( + twoPaneCoordinates.size.toIntRect().topLeft.toOffset() + ) + val boundsBottomRightOffset = twoPaneCoordinates.localToWindow( + twoPaneCoordinates.size.toIntRect().bottomRight.toOffset() + ) + val bounds = Rect( + boundsTopLeftOffset, + boundsBottomRightOffset + ) + + return localFoldingFeatures.map { localFoldingFeature -> + val foldLeft: Float + val foldTop: Float + val foldRight: Float + val foldBottom: Float + + with(density) { + if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { + foldLeft = 0f + foldTop = + (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() + foldRight = twoPaneCoordinates.size.width.toFloat() + foldBottom = + (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() + } else { + foldLeft = + (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() + foldTop = 0f + foldRight = + (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() + foldBottom = twoPaneCoordinates.size.height.toFloat() + } + } + + val foldTopLeftOffset = twoPaneCoordinates.localToWindow( + Offset(foldLeft, foldTop) + ) + val foldBottomRightOffset = twoPaneCoordinates.localToWindow( + Offset(foldRight, foldBottom) + ) + val foldBounds = Rect( + foldTopLeftOffset, + foldBottomRightOffset, + ) + + val center: Int + val size: Int + + if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { + center = foldBounds.center.y.roundToInt() + size = foldBounds.height.roundToInt() + } else { + center = foldBounds.center.x.roundToInt() + size = foldBounds.width.roundToInt() + } + + FoldingFeature( + windowBounds = bounds.toAndroidRect(), + center = center, + size = size, + state = localFoldingFeature.state, + orientation = localFoldingFeature.orientation, + ) + } + } + } + +private fun Size.toDpSize(density: Density) = + with(density) { + DpSize( + width = width.toDp(), + height = height.toDp() + ) + } + +private fun Rect.round(): IntRect = + IntRect( + left = left.roundToInt(), + top = top.roundToInt(), + right = right.roundToInt(), + bottom = bottom.roundToInt() + ) + +private fun IntRect.toRect(): Rect = + Rect( + left = left.toFloat(), + top = top.toFloat(), + right = right.toFloat(), + bottom = bottom.toFloat() + ) diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt new file mode 100644 index 000000000..4d056b199 --- /dev/null +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt @@ -0,0 +1,155 @@ +/* + * 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 + * + * https://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 com.google.accompanist.adaptive + +import androidx.activity.ComponentActivity +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.layout.FoldingFeature +import androidx.window.layout.WindowLayoutInfo +import androidx.window.layout.WindowMetricsCalculator +import androidx.window.testing.layout.FoldingFeature +import androidx.window.testing.layout.WindowLayoutInfoPublisherRule +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@RunWith(AndroidJUnit4::class) +class WindowGeometryTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() + + @Test + fun empty_folding_features_is_correct() { + lateinit var expectedWindowSizeClass: WindowSizeClass + lateinit var windowGeometry: WindowGeometry + + composeTestRule.setContent { + expectedWindowSizeClass = WindowSizeClass.calculateFromSize( + with(LocalDensity.current) { + WindowMetricsCalculator + .getOrCreate() + .computeCurrentWindowMetrics(composeTestRule.activity) + .bounds + .toComposeRect() + .size + .toDpSize() + } + ) + windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) + } + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) + + assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) + assertThat(windowGeometry.displayFeatures).isEmpty() + } + + @Test + fun single_folding_features_is_correct() { + lateinit var expectedWindowSizeClass: WindowSizeClass + lateinit var windowGeometry: WindowGeometry + + composeTestRule.setContent { + expectedWindowSizeClass = WindowSizeClass.calculateFromSize( + with(LocalDensity.current) { + WindowMetricsCalculator + .getOrCreate() + .computeCurrentWindowMetrics(composeTestRule.activity) + .bounds + .toComposeRect() + .size + .toDpSize() + } + ) + windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) + } + + val fakeFoldingFeature = FoldingFeature( + activity = composeTestRule.activity, + center = 200, + size = 40, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL, + ) + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( + WindowLayoutInfo( + listOf( + fakeFoldingFeature + ) + ) + ) + + assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) + assertThat(windowGeometry.displayFeatures).hasSize(1) + assertThat(windowGeometry.displayFeatures[0]).isEqualTo(fakeFoldingFeature) + } + + @Test + fun updating_folding_features_is_correct() { + lateinit var expectedWindowSizeClass: WindowSizeClass + lateinit var windowGeometry: WindowGeometry + + composeTestRule.setContent { + expectedWindowSizeClass = WindowSizeClass.calculateFromSize( + with(LocalDensity.current) { + WindowMetricsCalculator + .getOrCreate() + .computeCurrentWindowMetrics(composeTestRule.activity) + .bounds + .toComposeRect() + .size + .toDpSize() + } + ) + windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) + } + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) + + val fakeFoldingFeature = FoldingFeature( + activity = composeTestRule.activity, + center = 200, + size = 40, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL, + ) + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( + WindowLayoutInfo( + listOf( + fakeFoldingFeature + ) + ) + ) + + assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) + assertThat(windowGeometry.displayFeatures).hasSize(1) + assertThat(windowGeometry.displayFeatures[0]).isEqualTo(fakeFoldingFeature) + } +} diff --git a/appcompat-theme/build.gradle b/appcompat-theme/build.gradle index 661ef97d0..e2115189e 100644 --- a/appcompat-theme/build.gradle +++ b/appcompat-theme/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/drawablepainter/build.gradle b/drawablepainter/build.gradle index a43beea9c..d25508904 100644 --- a/drawablepainter/build.gradle +++ b/drawablepainter/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/flowlayout/build.gradle b/flowlayout/build.gradle index 7360224bf..3d8ed7952 100644 --- a/flowlayout/build.gradle +++ b/flowlayout/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c0fe71f5..1c454cc72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -compose = "1.3.0-alpha02" +compose = "1.3.0-SNAPSHOT" composeCompiler = "1.3.0-rc01" -composesnapshot = "-" # a single character = no snapshot +composesnapshot = "8898423" # a single character = no snapshot # gradlePlugin and lint need to be updated together gradlePlugin = "7.3.0-beta05" @@ -14,8 +14,11 @@ coroutines = "1.6.0" okhttp = "3.12.13" coil = "1.3.2" +composeMaterial3 = "1.0.0-alpha15" + androidxtest = "1.4.0" androidxnavigation = "2.5.0-rc02" +androidxWindow = "1.0.0" [libraries] compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } @@ -27,6 +30,7 @@ compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", ve compose-foundation-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose" } compose-material-material = { module = "androidx.compose.material:material", version.ref = "compose" } +compose-material3-windowSizeClass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "composeMaterial3" } compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } compose-animation-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } @@ -63,6 +67,8 @@ androidx-fragment = "androidx.fragment:fragment-ktx:1.5.1" androidx-dynamicanimation = "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03" androidx-lifecycle-runtime = "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" androidx-lifecycle-viewmodel-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" +androidx-window = { module = "androidx.window:window", version.ref = "androidxWindow" } +androidx-window-testing = { module = "androidx.window:window-testing", version.ref = "androidxWindow" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxnavigation" } androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "androidxnavigation" } diff --git a/insets-ui/build.gradle b/insets-ui/build.gradle index e763c06ff..15118a628 100644 --- a/insets-ui/build.gradle +++ b/insets-ui/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/insets/build.gradle b/insets/build.gradle index 92cb49bde..7bda54db8 100644 --- a/insets/build.gradle +++ b/insets/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/internal-testutils/build.gradle b/internal-testutils/build.gradle index efdca5672..3f49bc20e 100644 --- a/internal-testutils/build.gradle +++ b/internal-testutils/build.gradle @@ -20,7 +20,7 @@ plugins { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/navigation-animation/build.gradle b/navigation-animation/build.gradle index 9a6ac4bbf..8fc421fc7 100644 --- a/navigation-animation/build.gradle +++ b/navigation-animation/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/navigation-material/build.gradle b/navigation-material/build.gradle index 3be323492..d8ed5779d 100644 --- a/navigation-material/build.gradle +++ b/navigation-material/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/pager-indicators/build.gradle b/pager-indicators/build.gradle index 26f66b885..b33ae828b 100644 --- a/pager-indicators/build.gradle +++ b/pager-indicators/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/pager/build.gradle b/pager/build.gradle index 0f6d18861..62ac8a9da 100644 --- a/pager/build.gradle +++ b/pager/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/permissions/build.gradle b/permissions/build.gradle index 72b56deb0..01ff93844 100644 --- a/permissions/build.gradle +++ b/permissions/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/placeholder-material/build.gradle b/placeholder-material/build.gradle index d21f2e5aa..041f394bf 100644 --- a/placeholder-material/build.gradle +++ b/placeholder-material/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/placeholder/build.gradle b/placeholder/build.gradle index 37eb28b57..b7c89df91 100644 --- a/placeholder/build.gradle +++ b/placeholder/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/sample/build.gradle b/sample/build.gradle index 1cb85325f..317406871 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { applicationId "com.google.accompanist.sample" @@ -47,6 +47,7 @@ android { } dependencies { + implementation project(':adaptive') implementation project(':drawablepainter') implementation project(':insets') implementation project(':insets-ui') diff --git a/swiperefresh/build.gradle b/swiperefresh/build.gradle index 4529e1f8d..7cde37914 100644 --- a/swiperefresh/build.gradle +++ b/swiperefresh/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/systemuicontroller/build.gradle b/systemuicontroller/build.gradle index 63dd27ddf..b5d78f5f1 100644 --- a/systemuicontroller/build.gradle +++ b/systemuicontroller/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 diff --git a/web/build.gradle b/web/build.gradle index de21612d4..c40f9db4b 100644 --- a/web/build.gradle +++ b/web/build.gradle @@ -25,7 +25,7 @@ kotlin { } android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { minSdkVersion 21 From 1cb1c31ca0ff5db63575b7febaca9bc757f9c712 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Thu, 4 Aug 2022 18:17:00 -0700 Subject: [PATCH 03/10] Add basic TwoPane sample --- sample/src/main/AndroidManifest.xml | 10 +++ .../sample/adaptive/BasicTwoPaneSample.kt | 90 +++++++++++++++++++ sample/src/main/res/values/strings.xml | 2 + 3 files changed, 102 insertions(+) create mode 100644 sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 9d44a7069..c25b9381f 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -413,6 +413,16 @@ + + + + + + + diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt new file mode 100644 index 000000000..a9dff03a8 --- /dev/null +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt @@ -0,0 +1,90 @@ +/* + * 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 + * + * https://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 com.google.accompanist.sample.adaptive + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.accompanist.adaptive.DelegateTwoPaneStrategy +import com.google.accompanist.adaptive.FractionHorizontalTwoPaneStrategy +import com.google.accompanist.adaptive.FractionVerticalTwoPaneStrategy +import com.google.accompanist.adaptive.TwoPane +import com.google.accompanist.adaptive.TwoPaneStrategy +import com.google.accompanist.adaptive.calculateWindowGeometry +import com.google.accompanist.sample.AccompanistSampleTheme + +class BasicTwoPaneSample : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AccompanistSampleTheme { + val windowGeometry = calculateWindowGeometry(this) + + TwoPane( + first = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("First") + } + } + }, + second = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("Second") + } + } + }, + strategy = TwoPaneStrategy( + fallbackStrategy = DelegateTwoPaneStrategy( + firstStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = 0.75f, + ), + secondStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = 0.75f, + ), + useFirstStrategy = { _, _, layoutCoordinates -> + // Split vertically if the height is larger than the width + layoutCoordinates.size.height >= layoutCoordinates.size.width + } + ), + windowGeometry = windowGeometry + ), + modifier = Modifier.padding(8.dp) + ) + } + } + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index 27d3b6d69..d656dbb5e 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -63,4 +63,6 @@ Placeholder: Shimmer WebView: Basic + + Adaptive: TwoPane Basic From a7ce62a583b012eadd116174df8ec1f647359d16 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 5 Aug 2022 14:31:51 -0700 Subject: [PATCH 04/10] Add more documentation to SplitResult --- .../main/java/com/google/accompanist/adaptive/TwoPane.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt index 5ea5eb452..b5aed06d0 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -151,7 +151,11 @@ fun TwoPane( @Stable fun interface TwoPaneStrategy { /** - * Calculates the split result in local bounds. + * Calculates the split result in local bounds of the [TwoPane]. + * + * @param density the [Density] for measuring and laying out + * @param layoutDirection the [LayoutDirection] for measuring and laying out + * @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane] */ fun calculateSplitResult( density: Density, @@ -175,6 +179,8 @@ class SplitResult( * The area that is nether a `start` pane or an `end` pane, but a separation between those two. * In case width or height is 0 - it means that the area itself is a 0 width/height, but the * place within the layout is still defined. + * + * The [splitArea] should be defined in local bounds to the [TwoPane]. */ val splitArea: Rect, ) From a2f952c756408f0e9155d867ecf52e7a61202ccf Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 5 Aug 2022 14:34:39 -0700 Subject: [PATCH 05/10] Remove DelegateTwoPaneStrategy --- adaptive/api/current.api | 1 - .../google/accompanist/adaptive/TwoPane.kt | 21 -- .../accompanist/adaptive/TwoPaneTest.kt | 200 ------------------ .../sample/adaptive/BasicTwoPaneSample.kt | 25 ++- 4 files changed, 12 insertions(+), 235 deletions(-) diff --git a/adaptive/api/current.api b/adaptive/api/current.api index b7594ecbc..d83739a52 100644 --- a/adaptive/api/current.api +++ b/adaptive/api/current.api @@ -10,7 +10,6 @@ package com.google.accompanist.adaptive { } public final class TwoPaneKt { - method public static com.google.accompanist.adaptive.TwoPaneStrategy DelegateTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy firstStrategy, com.google.accompanist.adaptive.TwoPaneStrategy secondStrategy, kotlin.jvm.functions.Function3 useFirstStrategy); method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetHorizontalTwoPaneStrategy(float splitOffset, boolean offsetFromStart, optional float splitWidth); method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetVerticalTwoPaneStrategy(float splitOffset, boolean offsetFromTop, optional float splitHeight); method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionHorizontalTwoPaneStrategy(float splitFraction, optional float splitWidth); diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt index b5aed06d0..639b90621 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -394,24 +394,3 @@ public fun FixedOffsetVerticalTwoPaneStrategy( ) ) } - -/** - * Returns a [DelegateTwoPaneStrategy] that will delegate [TwoPaneStrategy.calculateSplitResult] to - * the [firstStrategy] when [useFirstStrategy] returns `true`. Otherwise, it will delegate to - * the [secondStrategy]. - */ -public fun DelegateTwoPaneStrategy( - firstStrategy: TwoPaneStrategy, - secondStrategy: TwoPaneStrategy, - useFirstStrategy: ( - density: Density, - layoutDirection: LayoutDirection, - layoutCoordinates: LayoutCoordinates - ) -> Boolean, -): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> - if (useFirstStrategy(density, layoutDirection, layoutCoordinates)) { - firstStrategy - } else { - secondStrategy - }.calculateSplitResult(density, layoutDirection, layoutCoordinates) -} diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index c09789ca8..3ad20b5dd 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -1459,206 +1459,6 @@ class TwoPaneTest { 0.001f ) } - - @Test - fun delegate_renders_correctly_when_changing_size_ltr() { - var width by mutableStateOf(900.dp) - var height by mutableStateOf(1200.dp) - - lateinit var density: Density - lateinit var twoPaneCoordinates: LayoutCoordinates - lateinit var firstCoordinates: LayoutCoordinates - lateinit var secondCoordinates: LayoutCoordinates - - composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - // Strategy: If we are taller than we are wide, use a vertical strategy with the - // second slot in the bottom 1/4. Otherwise, use a horizontal strategy with the - // second slot at the end 1/4. - strategy = DelegateTwoPaneStrategy( - firstStrategy = FractionVerticalTwoPaneStrategy(0.75f), - secondStrategy = FractionHorizontalTwoPaneStrategy(0.75f), - useFirstStrategy = { _, _, layoutCoordinates -> - layoutCoordinates.size.height >= layoutCoordinates.size.width - } - ), - modifier = Modifier - .requiredSize(width, height) - .onPlaced { twoPaneCoordinates = it } - ) - } - } - - // Initial state: we are taller than we are wide, so second should in the bottom 1/4 of the - // screen - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 0.dp), - DpSize(900.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f - ) - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 900.dp), - DpSize(900.dp, 300.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f - ) - - // Swap width and height - width = 1200.dp - height = 900.dp - - composeTestRule.waitForIdle() - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 0.dp), - DpSize(900.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f - ) - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(900.dp, 0.dp), - DpSize(300.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f - ) - } - - @Test - fun delegate_renders_correctly_when_changing_size_rtl() { - var width by mutableStateOf(900.dp) - var height by mutableStateOf(1200.dp) - - lateinit var density: Density - lateinit var twoPaneCoordinates: LayoutCoordinates - lateinit var firstCoordinates: LayoutCoordinates - lateinit var secondCoordinates: LayoutCoordinates - - composeTestRule.setContent { - density = LocalDensity.current - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { - TwoPane( - first = { - Spacer( - Modifier - .background(Color.Red) - .fillMaxSize() - .onPlaced { firstCoordinates = it } - ) - }, - second = { - Spacer( - Modifier - .background(Color.Blue) - .fillMaxSize() - .onPlaced { secondCoordinates = it } - ) - }, - // Strategy: If we are taller than we are wide, use a vertical strategy with the - // second slot in the bottom 1/4. Otherwise, use a horizontal strategy with the - // second slot at the end 1/4. - strategy = DelegateTwoPaneStrategy( - firstStrategy = FractionVerticalTwoPaneStrategy(0.75f), - secondStrategy = FractionHorizontalTwoPaneStrategy(0.75f), - useFirstStrategy = { _, _, layoutCoordinates -> - layoutCoordinates.size.height >= layoutCoordinates.size.width - } - ), - modifier = Modifier - .requiredSize(width, height) - .onPlaced { twoPaneCoordinates = it } - ) - } - } - - // Initial state: we are taller than we are wide, so second should in the bottom 1/4 of the - // screen - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 0.dp), - DpSize(900.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f - ) - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 900.dp), - DpSize(900.dp, 300.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f - ) - - // Swap width and height - width = 1200.dp - height = 900.dp - - composeTestRule.waitForIdle() - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(300.dp, 0.dp), - DpSize(900.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(firstCoordinates), - 0.001f - ) - - compareRectWithTolerance( - with(density) { - DpRect( - DpOffset(0.dp, 0.dp), - DpSize(300.dp, 900.dp) - ).toRect().round().toRect() - }, - twoPaneCoordinates.localBoundingBoxOf(secondCoordinates), - 0.001f - ) - } } private fun compareRectWithTolerance( diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt index a9dff03a8..baefb4261 100644 --- a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt @@ -27,7 +27,6 @@ import androidx.compose.material.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.google.accompanist.adaptive.DelegateTwoPaneStrategy import com.google.accompanist.adaptive.FractionHorizontalTwoPaneStrategy import com.google.accompanist.adaptive.FractionVerticalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane @@ -68,18 +67,18 @@ class BasicTwoPaneSample : ComponentActivity() { } }, strategy = TwoPaneStrategy( - fallbackStrategy = DelegateTwoPaneStrategy( - firstStrategy = FractionVerticalTwoPaneStrategy( - splitFraction = 0.75f, - ), - secondStrategy = FractionHorizontalTwoPaneStrategy( - splitFraction = 0.75f, - ), - useFirstStrategy = { _, _, layoutCoordinates -> - // Split vertically if the height is larger than the width - layoutCoordinates.size.height >= layoutCoordinates.size.width - } - ), + fallbackStrategy = { density, layoutDirection, layoutCoordinates -> + // Split vertically if the height is larger than the width + if (layoutCoordinates.size.height >= layoutCoordinates.size.width) { + FractionVerticalTwoPaneStrategy( + splitFraction = 0.75f, + ) + } else { + FractionHorizontalTwoPaneStrategy( + splitFraction = 0.75f, + ) + }.calculateSplitResult(density, layoutDirection, layoutCoordinates) + }, windowGeometry = windowGeometry ), modifier = Modifier.padding(8.dp) From 26d1a55b350c5bf2480389b7c7866701f027487e Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 5 Aug 2022 14:43:25 -0700 Subject: [PATCH 06/10] split = verb, gap = noun (splitting creates a gap) --- adaptive/api/current.api | 14 ++-- .../google/accompanist/adaptive/TwoPane.kt | 68 +++++++++---------- .../accompanist/adaptive/TwoPaneTest.kt | 17 ++--- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/adaptive/api/current.api b/adaptive/api/current.api index d83739a52..8bfbffcb0 100644 --- a/adaptive/api/current.api +++ b/adaptive/api/current.api @@ -2,18 +2,18 @@ package com.google.accompanist.adaptive { public final class SplitResult { - ctor public SplitResult(boolean isHorizontalSplit, androidx.compose.ui.geometry.Rect splitArea); - method public androidx.compose.ui.geometry.Rect getSplitArea(); + ctor public SplitResult(boolean isHorizontalSplit, androidx.compose.ui.geometry.Rect gapBounds); + method public androidx.compose.ui.geometry.Rect getGapBounds(); method public boolean isHorizontalSplit(); + property public final androidx.compose.ui.geometry.Rect gapBounds; property public final boolean isHorizontalSplit; - property public final androidx.compose.ui.geometry.Rect splitArea; } public final class TwoPaneKt { - method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetHorizontalTwoPaneStrategy(float splitOffset, boolean offsetFromStart, optional float splitWidth); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetVerticalTwoPaneStrategy(float splitOffset, boolean offsetFromTop, optional float splitHeight); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionHorizontalTwoPaneStrategy(float splitFraction, optional float splitWidth); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionVerticalTwoPaneStrategy(float splitFraction, optional float splitHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetHorizontalTwoPaneStrategy(float splitOffset, boolean offsetFromStart, optional float gapWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetVerticalTwoPaneStrategy(float splitOffset, boolean offsetFromTop, optional float gapHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionHorizontalTwoPaneStrategy(float splitFraction, optional float gapWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionVerticalTwoPaneStrategy(float splitFraction, optional float gapHeight); method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, optional androidx.compose.ui.Modifier modifier); method public static com.google.accompanist.adaptive.TwoPaneStrategy TwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt index 639b90621..c80abd505 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -95,32 +95,32 @@ fun TwoPane( ) val isHorizontalSplit = splitResult.isHorizontalSplit - val splitArea = splitResult.splitArea + val gapBounds = splitResult.gapBounds - val splitLeft = constraints.constrainWidth(splitArea.left.roundToInt()) - val splitRight = constraints.constrainWidth(splitArea.right.roundToInt()) - val splitTop = constraints.constrainHeight(splitArea.top.roundToInt()) - val splitBottom = constraints.constrainHeight(splitArea.bottom.roundToInt()) + val gapLeft = constraints.constrainWidth(gapBounds.left.roundToInt()) + val gapRight = constraints.constrainWidth(gapBounds.right.roundToInt()) + val gapTop = constraints.constrainHeight(gapBounds.top.roundToInt()) + val gapBottom = constraints.constrainHeight(gapBounds.bottom.roundToInt()) val firstConstraints = if (isHorizontalSplit) { val width = when (layoutDirection) { - LayoutDirection.Ltr -> splitLeft - LayoutDirection.Rtl -> constraints.maxWidth - splitRight + LayoutDirection.Ltr -> gapLeft + LayoutDirection.Rtl -> constraints.maxWidth - gapRight } constraints.copy(minWidth = width, maxWidth = width) } else { - constraints.copy(minHeight = splitTop, maxHeight = splitTop) + constraints.copy(minHeight = gapTop, maxHeight = gapTop) } val secondConstraints = if (isHorizontalSplit) { val width = when (layoutDirection) { - LayoutDirection.Ltr -> constraints.maxWidth - splitRight - LayoutDirection.Rtl -> splitLeft + LayoutDirection.Ltr -> constraints.maxWidth - gapRight + LayoutDirection.Rtl -> gapLeft } constraints.copy(minWidth = width, maxWidth = width) } else { - val height = constraints.maxHeight - splitBottom + val height = constraints.maxHeight - gapBottom constraints.copy(minHeight = height, maxHeight = height) } val firstPlaceable = firstMeasurable.measure(constraints.constrain(firstConstraints)) @@ -176,13 +176,13 @@ class SplitResult( val isHorizontalSplit: Boolean, /** - * The area that is nether a `start` pane or an `end` pane, but a separation between those two. - * In case width or height is 0 - it means that the area itself is a 0 width/height, but the + * The bounds that are nether a `start` pane or an `end` pane, but a separation between those + * two. In case width or height is 0 - it means that the gap itself is a 0 width/height, but the * place within the layout is still defined. * - * The [splitArea] should be defined in local bounds to the [TwoPane]. + * The [gapBounds] should be defined in local bounds to the [TwoPane]. */ - val splitArea: Rect, + val gapBounds: Rect, ) /** @@ -225,7 +225,7 @@ public fun HorizontalTwoPaneStrategy( val foldBounds = verticalFold.bounds.toComposeRect() SplitResult( isHorizontalSplit = true, - splitArea = Rect( + gapBounds = Rect( layoutCoordinates.windowToLocal(foldBounds.topLeft), layoutCoordinates.windowToLocal(foldBounds.bottomRight) ) @@ -258,7 +258,7 @@ public fun VerticalTwoPaneStrategy( val foldBounds = horizontalFold.bounds.toComposeRect() SplitResult( isHorizontalSplit = false, - splitArea = Rect( + gapBounds = Rect( layoutCoordinates.windowToLocal(foldBounds.topLeft), layoutCoordinates.windowToLocal(foldBounds.bottomRight) ) @@ -271,23 +271,23 @@ public fun VerticalTwoPaneStrategy( /** * Returns a [TwoPaneStrategy] that will place the slots horizontally. * - * The split will be placed at the given [splitFraction] from start, with the given [splitWidth]. + * The gap will be placed at the given [splitFraction] from start, with the given [gapWidth]. * * This strategy is _not_ fold aware. */ public fun FractionHorizontalTwoPaneStrategy( splitFraction: Float, - splitWidth: Dp = 0.dp, + gapWidth: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> val splitX = layoutCoordinates.size.width * when (layoutDirection) { LayoutDirection.Ltr -> splitFraction LayoutDirection.Rtl -> 1 - splitFraction } - val splitWidthPixel = with(density) { splitWidth.toPx() } + val splitWidthPixel = with(density) { gapWidth.toPx() } SplitResult( isHorizontalSplit = true, - splitArea = Rect( + gapBounds = Rect( left = splitX - splitWidthPixel / 2f, top = 0f, right = splitX + splitWidthPixel / 2f, @@ -299,15 +299,15 @@ public fun FractionHorizontalTwoPaneStrategy( /** * Returns a [TwoPaneStrategy] that will place the slots horizontally. * - * The split will be placed at [splitOffset] either from the start or end based on - * [offsetFromStart], with the given [splitWidth]. + * The gap will be placed at [splitOffset] either from the start or end based on + * [offsetFromStart], with the given [gapWidth]. * * This strategy is _not_ fold aware. */ public fun FixedOffsetHorizontalTwoPaneStrategy( splitOffset: Dp, offsetFromStart: Boolean, - splitWidth: Dp = 0.dp, + gapWidth: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> val splitOffsetPixel = with(density) { splitOffset.toPx() } val splitX = when (layoutDirection) { @@ -324,11 +324,11 @@ public fun FixedOffsetHorizontalTwoPaneStrategy( splitOffsetPixel } } - val splitWidthPixel = with(density) { splitWidth.toPx() } + val splitWidthPixel = with(density) { gapWidth.toPx() } SplitResult( isHorizontalSplit = true, - splitArea = Rect( + gapBounds = Rect( left = splitX - splitWidthPixel / 2f, top = 0f, right = splitX + splitWidthPixel / 2f, @@ -340,20 +340,20 @@ public fun FixedOffsetHorizontalTwoPaneStrategy( /** * Returns a [TwoPaneStrategy] that will place the slots horizontally. * - * The split will be placed at the given [splitFraction] from start, with the given [splitHeight]. + * The split will be placed at the given [splitFraction] from start, with the given [gapHeight]. * * This strategy is _not_ fold aware. */ public fun FractionVerticalTwoPaneStrategy( splitFraction: Float, - splitHeight: Dp = 0.dp, + gapHeight: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates -> val splitY = layoutCoordinates.size.height * splitFraction - val splitHeightPixel = with(density) { splitHeight.toPx() } + val splitHeightPixel = with(density) { gapHeight.toPx() } SplitResult( isHorizontalSplit = false, - splitArea = Rect( + gapBounds = Rect( left = 0f, top = splitY - splitHeightPixel / 2f, right = layoutCoordinates.size.width.toFloat(), @@ -366,14 +366,14 @@ public fun FractionVerticalTwoPaneStrategy( * Returns a [TwoPaneStrategy] that will place the slots horizontally. * * The split will be placed at [splitOffset] either from the top or bottom based on - * [offsetFromTop], with the given [splitHeight]. + * [offsetFromTop], with the given [gapHeight]. * * This strategy is _not_ fold aware. */ public fun FixedOffsetVerticalTwoPaneStrategy( splitOffset: Dp, offsetFromTop: Boolean, - splitHeight: Dp = 0.dp, + gapHeight: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates -> val splitOffsetPixel = with(density) { splitOffset.toPx() } val splitY = @@ -382,11 +382,11 @@ public fun FixedOffsetVerticalTwoPaneStrategy( } else { layoutCoordinates.size.height - splitOffsetPixel } - val splitHeightPixel = with(density) { splitHeight.toPx() } + val splitHeightPixel = with(density) { gapHeight.toPx() } SplitResult( isHorizontalSplit = false, - splitArea = Rect( + gapBounds = Rect( left = 0f, top = splitY - splitHeightPixel / 2f, right = layoutCoordinates.size.width.toFloat(), diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index 3ad20b5dd..d460bf002 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -23,9 +23,6 @@ import androidx.compose.foundation.layout.requiredSize import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -214,7 +211,7 @@ class TwoPaneTest { }, strategy = FractionHorizontalTwoPaneStrategy( splitFraction = 1f / 3f, - splitWidth = 64.dp + gapWidth = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -275,7 +272,7 @@ class TwoPaneTest { }, strategy = FractionHorizontalTwoPaneStrategy( splitFraction = 1f / 3f, - splitWidth = 64.dp + gapWidth = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -393,7 +390,7 @@ class TwoPaneTest { }, strategy = FractionVerticalTwoPaneStrategy( splitFraction = 1f / 3f, - splitHeight = 64.dp + gapHeight = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -576,7 +573,7 @@ class TwoPaneTest { strategy = FixedOffsetHorizontalTwoPaneStrategy( splitOffset = 200.dp, offsetFromStart = true, - splitWidth = 64.dp + gapWidth = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -638,7 +635,7 @@ class TwoPaneTest { strategy = FixedOffsetHorizontalTwoPaneStrategy( splitOffset = 200.dp, offsetFromStart = true, - splitWidth = 64.dp + gapWidth = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -758,7 +755,7 @@ class TwoPaneTest { strategy = FixedOffsetVerticalTwoPaneStrategy( splitOffset = 300.dp, offsetFromTop = true, - splitHeight = 64.dp + gapHeight = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -877,7 +874,7 @@ class TwoPaneTest { strategy = FixedOffsetVerticalTwoPaneStrategy( splitOffset = 300.dp, offsetFromTop = false, - splitHeight = 64.dp + gapHeight = 64.dp ), modifier = Modifier .requiredSize(900.dp, 1200.dp) From 98139d1250ba6a8def7a37b122cb8b3c0411fe90 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Fri, 5 Aug 2022 16:05:24 -0700 Subject: [PATCH 07/10] Expose fold-aware entry points only --- adaptive/api/current.api | 16 +- .../google/accompanist/adaptive/TwoPane.kt | 233 ++++++++++--- .../accompanist/adaptive/TwoPaneTest.kt | 309 ++++++++++-------- .../sample/adaptive/BasicTwoPaneSample.kt | 14 +- 4 files changed, 373 insertions(+), 199 deletions(-) diff --git a/adaptive/api/current.api b/adaptive/api/current.api index 8bfbffcb0..9a14c8afd 100644 --- a/adaptive/api/current.api +++ b/adaptive/api/current.api @@ -10,17 +10,17 @@ package com.google.accompanist.adaptive { } public final class TwoPaneKt { - method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetHorizontalTwoPaneStrategy(float splitOffset, boolean offsetFromStart, optional float gapWidth); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FixedOffsetVerticalTwoPaneStrategy(float splitOffset, boolean offsetFromTop, optional float gapHeight); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionHorizontalTwoPaneStrategy(float splitFraction, optional float gapWidth); - method public static com.google.accompanist.adaptive.TwoPaneStrategy FractionVerticalTwoPaneStrategy(float splitFraction, optional float gapHeight); - method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitFraction, optional float gapWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitOffset, boolean offsetFromStart, optional float gapWidth); method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, optional androidx.compose.ui.Modifier modifier); - method public static com.google.accompanist.adaptive.TwoPaneStrategy TwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); - method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.TwoPaneStrategy fallbackStrategy, com.google.accompanist.adaptive.WindowGeometry windowGeometry); + method public static com.google.accompanist.adaptive.TwoPaneStrategy TwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitFraction, optional float gapHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitOffset, boolean offsetFromTop, optional float gapHeight); } - @androidx.compose.runtime.Stable public fun interface TwoPaneStrategy { + public fun interface TwoPaneStrategy { method public com.google.accompanist.adaptive.SplitResult calculateSplitResult(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates); } diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt index c80abd505..c049a0100 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -19,7 +19,6 @@ package com.google.accompanist.adaptive import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect @@ -64,7 +63,7 @@ import kotlin.math.roundToInt */ @OptIn(ExperimentalComposeUiApi::class) @Composable -fun TwoPane( +public fun TwoPane( first: @Composable () -> Unit, second: @Composable () -> Unit, strategy: TwoPaneStrategy, @@ -144,12 +143,32 @@ fun TwoPane( } } +/** + * Returns the specification for where to place a split in [TwoPane] as a result of + * [TwoPaneStrategy.calculateSplitResult] + */ +public class SplitResult( + + /** + * Whether the split is vertical or horizontal + */ + public val isHorizontalSplit: Boolean, + + /** + * The bounds that are nether a `start` pane or an `end` pane, but a separation between those + * two. In case width or height is 0 - it means that the gap itself is a 0 width/height, but the + * place within the layout is still defined. + * + * The [gapBounds] should be defined in local bounds to the [TwoPane]. + */ + public val gapBounds: Rect, +) + /** * A strategy for configuring the [TwoPane] component, that is responsible for the meta-data * corresponding to the arrangement of the two panes of the layout. */ -@Stable -fun interface TwoPaneStrategy { +public fun interface TwoPaneStrategy { /** * Calculates the split result in local bounds of the [TwoPane]. * @@ -157,7 +176,7 @@ fun interface TwoPaneStrategy { * @param layoutDirection the [LayoutDirection] for measuring and laying out * @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane] */ - fun calculateSplitResult( + public fun calculateSplitResult( density: Density, layoutDirection: LayoutDirection, layoutCoordinates: LayoutCoordinates @@ -165,52 +184,184 @@ fun interface TwoPaneStrategy { } /** - * Returns the specification for where to place a split in [TwoPane] as a result of - * [TwoPaneStrategy.calculateSplitResult] + * A strategy for configuring the [TwoPane] component, that is responsible for the meta-data + * corresponding to the arrangement of the two panes of the layout. + * + * This strategy can be conditional: If `null` is returned from [calculateSplitResult], then this + * strategy did not produce a split result to use, and a different strategy should be used. */ -class SplitResult( - +private fun interface ConditionalTwoPaneStrategy { /** - * Whether the split is vertical or horizontal - */ - val isHorizontalSplit: Boolean, - - /** - * The bounds that are nether a `start` pane or an `end` pane, but a separation between those - * two. In case width or height is 0 - it means that the gap itself is a 0 width/height, but the - * place within the layout is still defined. + * Calculates the split result in local bounds of the [TwoPane], or `null` if this strategy + * does not apply. * - * The [gapBounds] should be defined in local bounds to the [TwoPane]. + * @param density the [Density] for measuring and laying out + * @param layoutDirection the [LayoutDirection] for measuring and laying out + * @param layoutCoordinates the [LayoutCoordinates] of the [TwoPane] */ - val gapBounds: Rect, -) + public fun calculateSplitResult( + density: Density, + layoutDirection: LayoutDirection, + layoutCoordinates: LayoutCoordinates + ): SplitResult? +} /** * Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a * horizontal or vertical fold respectively. * - * If there is no fold, then the [fallbackStrategy] will be used instead. + * If there is no fold, then the [defaultStrategy] will be used instead. */ public fun TwoPaneStrategy( - fallbackStrategy: TwoPaneStrategy, windowGeometry: WindowGeometry, -): TwoPaneStrategy = HorizontalTwoPaneStrategy( - fallbackStrategy = VerticalTwoPaneStrategy( - fallbackStrategy = fallbackStrategy, - windowGeometry = windowGeometry, - ), - windowGeometry = windowGeometry + defaultStrategy: TwoPaneStrategy, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + FoldAwareVerticalTwoPaneStrategy(windowGeometry), + defaultStrategy = defaultStrategy ) /** * Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold. * - * If there is no horizontal fold, then the [fallbackStrategy] will be used instead. + * If there is no fold, then the [defaultStrategy] will be used instead. + */ +public fun HorizontalTwoPaneStrategy( + windowGeometry: WindowGeometry, + defaultStrategy: TwoPaneStrategy, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + defaultStrategy = defaultStrategy +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * If there is a vertical fold, then the gap will be placed along the fold. + * + * Otherwise, the gap will be placed at the given [splitFraction] from start, with the given + * [gapWidth]. + */ +public fun HorizontalTwoPaneStrategy( + windowGeometry: WindowGeometry, + splitFraction: Float, + gapWidth: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + defaultStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = splitFraction, + gapWidth = gapWidth + ) +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * If there is a vertical fold, then the gap will be placed along the fold. + * + * Otherwise, the gap will be placed at [splitOffset] either from the start or end based on + * [offsetFromStart], with the given [gapWidth]. */ public fun HorizontalTwoPaneStrategy( - fallbackStrategy: TwoPaneStrategy, windowGeometry: WindowGeometry, + splitOffset: Dp, + offsetFromStart: Boolean, + gapWidth: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + defaultStrategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = splitOffset, + offsetFromStart = offsetFromStart, + gapWidth = gapWidth + ) +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold. + * + * If there is no fold, then the [defaultStrategy] will be used instead. + */ +public fun VerticalTwoPaneStrategy( + windowGeometry: WindowGeometry, + defaultStrategy: TwoPaneStrategy, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareVerticalTwoPaneStrategy(windowGeometry), + defaultStrategy = defaultStrategy +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * If there is a vertical fold, then the gap will be placed along the fold. + * + * Otherwise, the gap will be placed at the given [splitFraction] from top, with the given + * [gapHeight]. + */ +public fun VerticalTwoPaneStrategy( + windowGeometry: WindowGeometry, + splitFraction: Float, + gapHeight: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + defaultStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = splitFraction, + gapHeight = gapHeight + ) +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally. + * + * If there is a vertical fold, then the gap will be placed along the fold. + * + * Otherwise, the gap will be placed at [splitOffset] either from the top or bottom based on + * [offsetFromTop], with the given [gapHeight]. + */ +public fun VerticalTwoPaneStrategy( + windowGeometry: WindowGeometry, + splitOffset: Dp, + offsetFromTop: Boolean, + gapHeight: Dp = 0.dp, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(windowGeometry), + defaultStrategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = splitOffset, + offsetFromTop = offsetFromTop, + gapHeight = gapHeight + ) +) + +/** + * Returns a composite [TwoPaneStrategy]. + * + * The conditional strategies (if any) will be attempted in order, and their split result used + * if they return one. If none return a split result, then the [defaultStrategy] will be used, + * which guarantees returning a [SplitResult]. + */ +private fun TwoPaneStrategy( + vararg conditionalStrategies: ConditionalTwoPaneStrategy, + defaultStrategy: TwoPaneStrategy ): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + conditionalStrategies.firstNotNullOfOrNull { conditionalTwoPaneStrategy -> + conditionalTwoPaneStrategy.calculateSplitResult( + density = density, + layoutDirection = layoutDirection, + layoutCoordinates = layoutCoordinates + ) + } ?: defaultStrategy.calculateSplitResult( + density = density, + layoutDirection = layoutDirection, + layoutCoordinates = layoutCoordinates + ) +} + +/** + * Returns a [ConditionalTwoPaneStrategy] that will place the slots horizontally if there is a + * vertical fold, or `null` if there is no fold. + */ +private fun FoldAwareHorizontalTwoPaneStrategy( + windowGeometry: WindowGeometry, +): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates -> val verticalFold = windowGeometry.displayFeatures.find { it is FoldingFeature && it.orientation == FoldingFeature.Orientation.VERTICAL } as FoldingFeature? @@ -231,19 +382,17 @@ public fun HorizontalTwoPaneStrategy( ) ) } else { - fallbackStrategy.calculateSplitResult(density, layoutDirection, layoutCoordinates) + null } } /** - * Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold. - * - * If there is no vertical fold, then the [fallbackStrategy] will be used instead. + * Returns a [ConditionalTwoPaneStrategy] that will place the slots vertically if there is a + * horizontal fold, or `null` if there is no fold. */ -public fun VerticalTwoPaneStrategy( - fallbackStrategy: TwoPaneStrategy, +private fun FoldAwareVerticalTwoPaneStrategy( windowGeometry: WindowGeometry, -): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> +): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates -> val horizontalFold = windowGeometry.displayFeatures.find { it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL } as FoldingFeature? @@ -264,7 +413,7 @@ public fun VerticalTwoPaneStrategy( ) ) } else { - fallbackStrategy.calculateSplitResult(density, layoutDirection, layoutCoordinates) + null } } @@ -275,7 +424,7 @@ public fun VerticalTwoPaneStrategy( * * This strategy is _not_ fold aware. */ -public fun FractionHorizontalTwoPaneStrategy( +internal fun FractionHorizontalTwoPaneStrategy( splitFraction: Float, gapWidth: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> @@ -304,7 +453,7 @@ public fun FractionHorizontalTwoPaneStrategy( * * This strategy is _not_ fold aware. */ -public fun FixedOffsetHorizontalTwoPaneStrategy( +internal fun FixedOffsetHorizontalTwoPaneStrategy( splitOffset: Dp, offsetFromStart: Boolean, gapWidth: Dp = 0.dp, @@ -344,7 +493,7 @@ public fun FixedOffsetHorizontalTwoPaneStrategy( * * This strategy is _not_ fold aware. */ -public fun FractionVerticalTwoPaneStrategy( +internal fun FractionVerticalTwoPaneStrategy( splitFraction: Float, gapHeight: Dp = 0.dp, ): TwoPaneStrategy = TwoPaneStrategy { density, _, layoutCoordinates -> @@ -370,7 +519,7 @@ public fun FractionVerticalTwoPaneStrategy( * * This strategy is _not_ fold aware. */ -public fun FixedOffsetVerticalTwoPaneStrategy( +internal fun FixedOffsetVerticalTwoPaneStrategy( splitOffset: Dp, offsetFromTop: Boolean, gapHeight: Dp = 0.dp, diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index d460bf002..63aa5192b 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -911,6 +911,20 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = emptyList() + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -932,15 +946,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionVerticalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f - ), - windowGeometry = object : WindowGeometry { - override val windowSizeClass: WindowSizeClass = - WindowSizeClass.calculateFromSize(DpSize(900.dp, 1200.dp)) - override val displayFeatures: List = - emptyList() - } + ) ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -977,6 +987,27 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -998,30 +1029,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionHorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f - ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 0.dp, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } + ) ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -1058,6 +1070,27 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 60.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -1079,30 +1112,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionHorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f - ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 60.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } + ) ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -1139,6 +1153,27 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -1160,30 +1195,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionVerticalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 0.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -1220,6 +1236,27 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -1241,30 +1278,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionHorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 0.dp, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.VERTICAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -1301,6 +1319,27 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 64.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } composeTestRule.setContent { density = LocalDensity.current @@ -1322,30 +1361,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionHorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = HorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 64.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.VERTICAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } ), modifier = Modifier .requiredSize(900.dp, 1200.dp) @@ -1385,6 +1405,28 @@ class TwoPaneTest { composeTestRule.setContent { density = LocalDensity.current + val windowGeometry = object : WindowGeometry { + val fakeWindowGeometry by lazy { + fakeWindowGeometry( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL + ) + ) + ) + } + + override val windowSizeClass: WindowSizeClass get() = + fakeWindowGeometry.windowSizeClass + override val displayFeatures: List get() = + fakeWindowGeometry.displayFeatures + } + TwoPane( first = { Spacer( @@ -1403,30 +1445,11 @@ class TwoPaneTest { ) }, strategy = TwoPaneStrategy( - fallbackStrategy = FractionVerticalTwoPaneStrategy( + windowGeometry = windowGeometry, + defaultStrategy = VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 1f / 3f - ), - windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 0.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.VERTICAL - ) - ) - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures - } + ) ), modifier = Modifier .requiredSize(900.dp, 1200.dp) diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt index baefb4261..3d5564637 100644 --- a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt @@ -27,10 +27,10 @@ import androidx.compose.material.Text import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.google.accompanist.adaptive.FractionHorizontalTwoPaneStrategy -import com.google.accompanist.adaptive.FractionVerticalTwoPaneStrategy +import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.google.accompanist.adaptive.TwoPaneStrategy +import com.google.accompanist.adaptive.VerticalTwoPaneStrategy import com.google.accompanist.adaptive.calculateWindowGeometry import com.google.accompanist.sample.AccompanistSampleTheme @@ -67,19 +67,21 @@ class BasicTwoPaneSample : ComponentActivity() { } }, strategy = TwoPaneStrategy( - fallbackStrategy = { density, layoutDirection, layoutCoordinates -> + windowGeometry = windowGeometry, + defaultStrategy = { density, layoutDirection, layoutCoordinates -> // Split vertically if the height is larger than the width if (layoutCoordinates.size.height >= layoutCoordinates.size.width) { - FractionVerticalTwoPaneStrategy( + VerticalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 0.75f, ) } else { - FractionHorizontalTwoPaneStrategy( + HorizontalTwoPaneStrategy( + windowGeometry = windowGeometry, splitFraction = 0.75f, ) }.calculateSplitResult(density, layoutDirection, layoutCoordinates) }, - windowGeometry = windowGeometry ), modifier = Modifier.padding(8.dp) ) From 266226b15d0186955232ea5fe9e27604fa27de73 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 9 Aug 2022 14:03:45 -0700 Subject: [PATCH 08/10] Remove WindowGeometry and consolidate folding support --- adaptive/api/current.api | 47 +- .../{WindowGeometry.kt => DisplayFeatures.kt} | 34 +- .../google/accompanist/adaptive/TwoPane.kt | 243 ++++++---- .../adaptive/DisplayFeaturesTest.kt | 117 +++++ .../accompanist/adaptive/TwoPaneTest.kt | 422 ++++++++---------- .../adaptive/WindowGeometryTest.kt | 155 ------- sample/src/main/AndroidManifest.xml | 20 + .../sample/adaptive/BasicTwoPaneSample.kt | 34 +- .../adaptive/HorizontalTwoPaneSample.kt | 78 ++++ .../sample/adaptive/VerticalTwoPaneSample.kt | 78 ++++ sample/src/main/res/values/strings.xml | 3 + 11 files changed, 664 insertions(+), 567 deletions(-) rename adaptive/src/main/java/com/google/accompanist/adaptive/{WindowGeometry.kt => DisplayFeatures.kt} (57%) create mode 100644 adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt delete mode 100644 adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt create mode 100644 sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt create mode 100644 sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt diff --git a/adaptive/api/current.api b/adaptive/api/current.api index 9a14c8afd..1d9b3f34f 100644 --- a/adaptive/api/current.api +++ b/adaptive/api/current.api @@ -1,39 +1,42 @@ // Signature format: 4.0 package com.google.accompanist.adaptive { + public final class DisplayFeaturesKt { + method @androidx.compose.runtime.Composable public static java.util.List calculateDisplayFeatures(android.app.Activity activity); + } + + @kotlin.jvm.JvmInline public final class FoldAwareConfiguration { + field public static final com.google.accompanist.adaptive.FoldAwareConfiguration.Companion Companion; + } + + public static final class FoldAwareConfiguration.Companion { + method public int getAllFolds(); + method public int getHorizontalFoldsOnly(); + method public int getVerticalFoldsOnly(); + property public final int AllFolds; + property public final int HorizontalFoldsOnly; + property public final int VerticalFoldsOnly; + } + public final class SplitResult { - ctor public SplitResult(boolean isHorizontalSplit, androidx.compose.ui.geometry.Rect gapBounds); + ctor public SplitResult(androidx.compose.foundation.gestures.Orientation gapOrientation, androidx.compose.ui.geometry.Rect gapBounds); method public androidx.compose.ui.geometry.Rect getGapBounds(); - method public boolean isHorizontalSplit(); + method public androidx.compose.foundation.gestures.Orientation getGapOrientation(); property public final androidx.compose.ui.geometry.Rect gapBounds; - property public final boolean isHorizontalSplit; + property public final androidx.compose.foundation.gestures.Orientation gapOrientation; } public final class TwoPaneKt { - method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); - method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitFraction, optional float gapWidth); - method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitOffset, boolean offsetFromStart, optional float gapWidth); - method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, optional androidx.compose.ui.Modifier modifier); - method public static com.google.accompanist.adaptive.TwoPaneStrategy TwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); - method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, com.google.accompanist.adaptive.TwoPaneStrategy defaultStrategy); - method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitFraction, optional float gapHeight); - method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(com.google.accompanist.adaptive.WindowGeometry windowGeometry, float splitOffset, boolean offsetFromTop, optional float gapHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitFraction, optional float gapWidth); + method public static com.google.accompanist.adaptive.TwoPaneStrategy HorizontalTwoPaneStrategy(float splitOffset, optional boolean offsetFromStart, optional float gapWidth); + method @androidx.compose.runtime.Composable public static void TwoPane(kotlin.jvm.functions.Function0 first, kotlin.jvm.functions.Function0 second, com.google.accompanist.adaptive.TwoPaneStrategy strategy, java.util.List displayFeatures, optional androidx.compose.ui.Modifier modifier, optional int foldAwareConfiguration); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitFraction, optional float gapHeight); + method public static com.google.accompanist.adaptive.TwoPaneStrategy VerticalTwoPaneStrategy(float splitOffset, optional boolean offsetFromTop, optional float gapHeight); } public fun interface TwoPaneStrategy { method public com.google.accompanist.adaptive.SplitResult calculateSplitResult(androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.ui.layout.LayoutCoordinates layoutCoordinates); } - public interface WindowGeometry { - method public java.util.List getDisplayFeatures(); - method public androidx.compose.material3.windowsizeclass.WindowSizeClass getWindowSizeClass(); - property public abstract java.util.List displayFeatures; - property public abstract androidx.compose.material3.windowsizeclass.WindowSizeClass windowSizeClass; - } - - public final class WindowGeometryKt { - method @androidx.compose.runtime.Composable public static com.google.accompanist.adaptive.WindowGeometry calculateWindowGeometry(android.app.Activity activity); - } - } diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/DisplayFeatures.kt similarity index 57% rename from adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt rename to adaptive/src/main/java/com/google/accompanist/adaptive/DisplayFeatures.kt index 2d71d6819..f440f816c 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/WindowGeometry.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/DisplayFeatures.kt @@ -17,9 +17,6 @@ package com.google.accompanist.adaptive import android.app.Activity -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -28,29 +25,10 @@ import androidx.window.layout.DisplayFeature import androidx.window.layout.WindowInfoTracker /** - * A description of the current window geometry. + * Calculates the list of [DisplayFeature]s from the given [activity]. */ -public interface WindowGeometry { - - /** - * The current [WindowSizeClass]. - */ - val windowSizeClass: WindowSizeClass - - /** - * The current list of known [DisplayFeature]s. - */ - val displayFeatures: List -} - -/** - * Calculates the [WindowGeometry] for the given [activity]. - */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable -public fun calculateWindowGeometry(activity: Activity): WindowGeometry { - val windowSizeClass = calculateWindowSizeClass(activity) - +public fun calculateDisplayFeatures(activity: Activity): List { val windowInfoTracker = remember(activity) { WindowInfoTracker.getOrCreate(activity) } val windowLayoutInfo = remember(windowInfoTracker, activity) { windowInfoTracker.windowLayoutInfo(activity) @@ -62,11 +40,5 @@ public fun calculateWindowGeometry(activity: Activity): WindowGeometry { } } - return object : WindowGeometry { - override val windowSizeClass: WindowSizeClass - get() = windowSizeClass - - override val displayFeatures: List - get() = displayFeatures - } + return displayFeatures } diff --git a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt index c049a0100..9603566f0 100644 --- a/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt +++ b/adaptive/src/main/java/com/google/accompanist/adaptive/TwoPane.kt @@ -16,6 +16,7 @@ package com.google.accompanist.adaptive +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable @@ -35,6 +36,7 @@ import androidx.compose.ui.unit.constrain import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp +import androidx.window.layout.DisplayFeature import androidx.window.layout.FoldingFeature import kotlin.math.roundToInt @@ -42,11 +44,14 @@ import kotlin.math.roundToInt * A layout that places two different pieces of content defined by the [first] and [second] * slots where the arrangement, sizes and separation behaviour is controlled by [TwoPaneStrategy]. * - * The default implementations of [TwoPaneStrategy] are fold and hinges aware, meaning that the two - * pane will adopt its layout to properly separate [first] and [second] panes so they don't - * interfere with hardware hinges (vertical or horizontal), or respect folds when needed - * (for example, when foldable is half-folded (90-degree fold AKA tabletop) the split will become - * on the bend). + * [TwoPane] is fold and hinges aware using the provided the [displayFeatures] (which should + * normally be calculated via [calculateDisplayFeatures]). The layout will be adapted to properly + * separate [first] and [second] panes so they don't interfere with hardware hinges (vertical or + * horizontal) as specified in [displayFeatures], or respect folds when needed (for example, when + * foldable is half-folded (90-degree fold AKA tabletop) the split will become on the bend). + * + * To only be aware of folds with a specific orientation, pass in an alternate + * [foldAwareConfiguration] to only adjust for vertical or horizontal folds. * * The [TwoPane] layout will always place both [first] and [second], based on the provided * [strategy] and window environment. If you instead only want to place one or the other, @@ -61,9 +66,46 @@ import kotlin.math.roundToInt * @param strategy strategy of the two pane that controls the arrangement of the layout * @param modifier an optional modifier for the layout */ -@OptIn(ExperimentalComposeUiApi::class) @Composable public fun TwoPane( + first: @Composable () -> Unit, + second: @Composable () -> Unit, + strategy: TwoPaneStrategy, + displayFeatures: List, + modifier: Modifier = Modifier, + foldAwareConfiguration: FoldAwareConfiguration = FoldAwareConfiguration.AllFolds, +) { + TwoPane( + first = first, + second = second, + strategy = when (foldAwareConfiguration) { + FoldAwareConfiguration.HorizontalFoldsOnly -> { + VerticalTwoPaneStrategy( + displayFeatures = displayFeatures, + defaultStrategy = strategy, + ) + } + FoldAwareConfiguration.VerticalFoldsOnly -> { + HorizontalTwoPaneStrategy( + displayFeatures = displayFeatures, + defaultStrategy = strategy, + ) + } + FoldAwareConfiguration.AllFolds -> { + TwoPaneStrategy( + displayFeatures = displayFeatures, + defaultStrategy = strategy, + ) + } + else -> error("Unknown FoldAware value!") + }, + modifier = modifier, + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun TwoPane( first: @Composable () -> Unit, second: @Composable () -> Unit, strategy: TwoPaneStrategy, @@ -93,7 +135,7 @@ public fun TwoPane( } ) - val isHorizontalSplit = splitResult.isHorizontalSplit + val gapOrientation = splitResult.gapOrientation val gapBounds = splitResult.gapBounds val gapLeft = constraints.constrainWidth(gapBounds.left.roundToInt()) @@ -101,7 +143,7 @@ public fun TwoPane( val gapTop = constraints.constrainHeight(gapBounds.top.roundToInt()) val gapBottom = constraints.constrainHeight(gapBounds.bottom.roundToInt()) val firstConstraints = - if (isHorizontalSplit) { + if (gapOrientation == Orientation.Vertical) { val width = when (layoutDirection) { LayoutDirection.Ltr -> gapLeft LayoutDirection.Rtl -> constraints.maxWidth - gapRight @@ -112,7 +154,7 @@ public fun TwoPane( constraints.copy(minHeight = gapTop, maxHeight = gapTop) } val secondConstraints = - if (isHorizontalSplit) { + if (gapOrientation == Orientation.Vertical) { val width = when (layoutDirection) { LayoutDirection.Ltr -> constraints.maxWidth - gapRight LayoutDirection.Rtl -> gapLeft @@ -127,13 +169,13 @@ public fun TwoPane( firstPlaceable.placeRelative(0, 0) val detailOffsetX = - if (isHorizontalSplit) { + if (gapOrientation == Orientation.Vertical) { constraints.maxWidth - secondPlaceable.width } else { 0 } val detailOffsetY = - if (isHorizontalSplit) { + if (gapOrientation == Orientation.Vertical) { 0 } else { constraints.maxHeight - secondPlaceable.height @@ -143,6 +185,33 @@ public fun TwoPane( } } +/** + * The configuration for which type of folds for a [TwoPane] to automatically avoid. + */ +@JvmInline +public value class FoldAwareConfiguration private constructor(private val value: Int) { + + companion object { + /** + * The [TwoPane] will only be aware of horizontal folds only, splitting the content + * vertically. + */ + val HorizontalFoldsOnly = FoldAwareConfiguration(0) + + /** + * The [TwoPane] will only be aware of vertical folds only, splitting the content + * horizontally. + */ + val VerticalFoldsOnly = FoldAwareConfiguration(1) + + /** + * The [TwoPane] will be aware of both horizontal and vertical folds, splitting the content + * vertically and horizontally respectively. + */ + val AllFolds = FoldAwareConfiguration(2) + } +} + /** * Returns the specification for where to place a split in [TwoPane] as a result of * [TwoPaneStrategy.calculateSplitResult] @@ -150,9 +219,9 @@ public fun TwoPane( public class SplitResult( /** - * Whether the split is vertical or horizontal + * Whether the gap is vertical or horizontal */ - public val isHorizontalSplit: Boolean, + public val gapOrientation: Orientation, /** * The bounds that are nether a `start` pane or an `end` pane, but a separation between those @@ -206,34 +275,6 @@ private fun interface ConditionalTwoPaneStrategy { ): SplitResult? } -/** - * Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a - * horizontal or vertical fold respectively. - * - * If there is no fold, then the [defaultStrategy] will be used instead. - */ -public fun TwoPaneStrategy( - windowGeometry: WindowGeometry, - defaultStrategy: TwoPaneStrategy, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - FoldAwareVerticalTwoPaneStrategy(windowGeometry), - defaultStrategy = defaultStrategy -) - -/** - * Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold. - * - * If there is no fold, then the [defaultStrategy] will be used instead. - */ -public fun HorizontalTwoPaneStrategy( - windowGeometry: WindowGeometry, - defaultStrategy: TwoPaneStrategy, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - defaultStrategy = defaultStrategy -) - /** * Returns a [TwoPaneStrategy] that will place the slots horizontally. * @@ -243,15 +284,11 @@ public fun HorizontalTwoPaneStrategy( * [gapWidth]. */ public fun HorizontalTwoPaneStrategy( - windowGeometry: WindowGeometry, splitFraction: Float, gapWidth: Dp = 0.dp, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - defaultStrategy = FractionHorizontalTwoPaneStrategy( - splitFraction = splitFraction, - gapWidth = gapWidth - ) +): TwoPaneStrategy = FractionHorizontalTwoPaneStrategy( + splitFraction = splitFraction, + gapWidth = gapWidth ) /** @@ -263,30 +300,13 @@ public fun HorizontalTwoPaneStrategy( * [offsetFromStart], with the given [gapWidth]. */ public fun HorizontalTwoPaneStrategy( - windowGeometry: WindowGeometry, splitOffset: Dp, - offsetFromStart: Boolean, + offsetFromStart: Boolean = true, gapWidth: Dp = 0.dp, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - defaultStrategy = FixedOffsetHorizontalTwoPaneStrategy( - splitOffset = splitOffset, - offsetFromStart = offsetFromStart, - gapWidth = gapWidth - ) -) - -/** - * Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold. - * - * If there is no fold, then the [defaultStrategy] will be used instead. - */ -public fun VerticalTwoPaneStrategy( - windowGeometry: WindowGeometry, - defaultStrategy: TwoPaneStrategy, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareVerticalTwoPaneStrategy(windowGeometry), - defaultStrategy = defaultStrategy +): TwoPaneStrategy = FixedOffsetHorizontalTwoPaneStrategy( + splitOffset = splitOffset, + offsetFromStart = offsetFromStart, + gapWidth = gapWidth ) /** @@ -298,15 +318,11 @@ public fun VerticalTwoPaneStrategy( * [gapHeight]. */ public fun VerticalTwoPaneStrategy( - windowGeometry: WindowGeometry, splitFraction: Float, gapHeight: Dp = 0.dp, -): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - defaultStrategy = FractionVerticalTwoPaneStrategy( - splitFraction = splitFraction, - gapHeight = gapHeight - ) +): TwoPaneStrategy = FractionVerticalTwoPaneStrategy( + splitFraction = splitFraction, + gapHeight = gapHeight ) /** @@ -318,17 +334,54 @@ public fun VerticalTwoPaneStrategy( * [offsetFromTop], with the given [gapHeight]. */ public fun VerticalTwoPaneStrategy( - windowGeometry: WindowGeometry, splitOffset: Dp, - offsetFromTop: Boolean, + offsetFromTop: Boolean = true, gapHeight: Dp = 0.dp, +): TwoPaneStrategy = FixedOffsetVerticalTwoPaneStrategy( + splitOffset = splitOffset, + offsetFromTop = offsetFromTop, + gapHeight = gapHeight +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots vertically or horizontally if there is a + * horizontal or vertical fold respectively. + * + * If there is no fold, then the [defaultStrategy] will be used instead. + */ +private fun TwoPaneStrategy( + displayFeatures: List, + defaultStrategy: TwoPaneStrategy, ): TwoPaneStrategy = TwoPaneStrategy( - FoldAwareHorizontalTwoPaneStrategy(windowGeometry), - defaultStrategy = FixedOffsetVerticalTwoPaneStrategy( - splitOffset = splitOffset, - offsetFromTop = offsetFromTop, - gapHeight = gapHeight - ) + FoldAwareHorizontalTwoPaneStrategy(displayFeatures), + FoldAwareVerticalTwoPaneStrategy(displayFeatures), + defaultStrategy = defaultStrategy +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots horizontally if there is a vertical fold. + * + * If there is no fold, then the [defaultStrategy] will be used instead. + */ +private fun HorizontalTwoPaneStrategy( + displayFeatures: List, + defaultStrategy: TwoPaneStrategy, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareHorizontalTwoPaneStrategy(displayFeatures), + defaultStrategy = defaultStrategy +) + +/** + * Returns a [TwoPaneStrategy] that will place the slots vertically if there is a horizontal fold. + * + * If there is no fold, then the [defaultStrategy] will be used instead. + */ +private fun VerticalTwoPaneStrategy( + displayFeatures: List, + defaultStrategy: TwoPaneStrategy, +): TwoPaneStrategy = TwoPaneStrategy( + FoldAwareVerticalTwoPaneStrategy(displayFeatures), + defaultStrategy = defaultStrategy ) /** @@ -360,9 +413,9 @@ private fun TwoPaneStrategy( * vertical fold, or `null` if there is no fold. */ private fun FoldAwareHorizontalTwoPaneStrategy( - windowGeometry: WindowGeometry, + displayFeatures: List, ): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates -> - val verticalFold = windowGeometry.displayFeatures.find { + val verticalFold = displayFeatures.find { it is FoldingFeature && it.orientation == FoldingFeature.Orientation.VERTICAL } as FoldingFeature? @@ -375,7 +428,7 @@ private fun FoldAwareHorizontalTwoPaneStrategy( ) { val foldBounds = verticalFold.bounds.toComposeRect() SplitResult( - isHorizontalSplit = true, + gapOrientation = Orientation.Vertical, gapBounds = Rect( layoutCoordinates.windowToLocal(foldBounds.topLeft), layoutCoordinates.windowToLocal(foldBounds.bottomRight) @@ -391,9 +444,9 @@ private fun FoldAwareHorizontalTwoPaneStrategy( * horizontal fold, or `null` if there is no fold. */ private fun FoldAwareVerticalTwoPaneStrategy( - windowGeometry: WindowGeometry, + displayFeatures: List, ): ConditionalTwoPaneStrategy = ConditionalTwoPaneStrategy { _, _, layoutCoordinates -> - val horizontalFold = windowGeometry.displayFeatures.find { + val horizontalFold = displayFeatures.find { it is FoldingFeature && it.orientation == FoldingFeature.Orientation.HORIZONTAL } as FoldingFeature? @@ -406,7 +459,7 @@ private fun FoldAwareVerticalTwoPaneStrategy( ) { val foldBounds = horizontalFold.bounds.toComposeRect() SplitResult( - isHorizontalSplit = false, + gapOrientation = Orientation.Horizontal, gapBounds = Rect( layoutCoordinates.windowToLocal(foldBounds.topLeft), layoutCoordinates.windowToLocal(foldBounds.bottomRight) @@ -435,7 +488,7 @@ internal fun FractionHorizontalTwoPaneStrategy( val splitWidthPixel = with(density) { gapWidth.toPx() } SplitResult( - isHorizontalSplit = true, + gapOrientation = Orientation.Vertical, gapBounds = Rect( left = splitX - splitWidthPixel / 2f, top = 0f, @@ -476,7 +529,7 @@ internal fun FixedOffsetHorizontalTwoPaneStrategy( val splitWidthPixel = with(density) { gapWidth.toPx() } SplitResult( - isHorizontalSplit = true, + gapOrientation = Orientation.Vertical, gapBounds = Rect( left = splitX - splitWidthPixel / 2f, top = 0f, @@ -501,7 +554,7 @@ internal fun FractionVerticalTwoPaneStrategy( val splitHeightPixel = with(density) { gapHeight.toPx() } SplitResult( - isHorizontalSplit = false, + gapOrientation = Orientation.Horizontal, gapBounds = Rect( left = 0f, top = splitY - splitHeightPixel / 2f, @@ -534,7 +587,7 @@ internal fun FixedOffsetVerticalTwoPaneStrategy( val splitHeightPixel = with(density) { gapHeight.toPx() } SplitResult( - isHorizontalSplit = false, + gapOrientation = Orientation.Horizontal, gapBounds = Rect( left = 0f, top = splitY - splitHeightPixel / 2f, diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt new file mode 100644 index 000000000..25f4cab72 --- /dev/null +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/DisplayFeaturesTest.kt @@ -0,0 +1,117 @@ +/* + * 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 + * + * https://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 com.google.accompanist.adaptive + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.window.layout.DisplayFeature +import androidx.window.layout.FoldingFeature +import androidx.window.layout.WindowLayoutInfo +import androidx.window.testing.layout.FoldingFeature +import androidx.window.testing.layout.WindowLayoutInfoPublisherRule +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class DisplayFeaturesTest { + + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @get:Rule + val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() + + @Test + fun empty_folding_features_is_correct() { + lateinit var displayFeatures: List + + composeTestRule.setContent { + displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity) + } + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) + + composeTestRule.waitForIdle() + + assertThat(displayFeatures).isEmpty() + } + + @Test + fun single_folding_features_is_correct() { + lateinit var displayFeatures: List + + composeTestRule.setContent { + displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity) + } + + val fakeFoldingFeature = FoldingFeature( + activity = composeTestRule.activity, + center = 200, + size = 40, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL, + ) + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( + WindowLayoutInfo( + listOf( + fakeFoldingFeature + ) + ) + ) + + composeTestRule.waitForIdle() + + assertThat(displayFeatures).hasSize(1) + assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature) + } + + @Test + fun updating_folding_features_is_correct() { + lateinit var displayFeatures: List + + composeTestRule.setContent { + displayFeatures = calculateDisplayFeatures(activity = composeTestRule.activity) + } + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) + + val fakeFoldingFeature = FoldingFeature( + activity = composeTestRule.activity, + center = 200, + size = 40, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL, + ) + + windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( + WindowLayoutInfo( + listOf( + fakeFoldingFeature + ) + ) + ) + + composeTestRule.waitForIdle() + + assertThat(displayFeatures).hasSize(1) + assertThat(displayFeatures[0]).isEqualTo(fakeFoldingFeature) + } +} diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt index 63aa5192b..6419f47af 100644 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt +++ b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/TwoPaneTest.kt @@ -20,13 +20,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.requiredSize -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.layout.LayoutCoordinates @@ -44,7 +41,6 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toIntRect import androidx.compose.ui.unit.toOffset -import androidx.compose.ui.unit.toSize import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.window.core.ExperimentalWindowApi import androidx.window.layout.DisplayFeature @@ -56,7 +52,6 @@ import org.junit.Test import org.junit.runner.RunWith import kotlin.math.roundToInt -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @RunWith(AndroidJUnit4::class) class TwoPaneTest { @get:Rule @@ -911,19 +906,12 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = emptyList() - ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = emptyList() + ) } composeTestRule.setContent { @@ -945,13 +933,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ) + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -987,26 +972,19 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 0.dp, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.HORIZONTAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } composeTestRule.setContent { @@ -1028,13 +1006,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ) + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1070,26 +1045,19 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 60.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 60.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } composeTestRule.setContent { @@ -1111,13 +1079,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ) + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1153,26 +1118,19 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 600.dp, - size = 0.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.HORIZONTAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 600.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.HORIZONTAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } composeTestRule.setContent { @@ -1194,13 +1152,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ), + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1236,26 +1191,19 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 0.dp, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.VERTICAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.HALF_OPENED, + orientation = FoldingFeature.Orientation.VERTICAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } composeTestRule.setContent { @@ -1277,13 +1225,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ), + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1319,26 +1264,19 @@ class TwoPaneTest { lateinit var twoPaneCoordinates: LayoutCoordinates lateinit var firstCoordinates: LayoutCoordinates lateinit var secondCoordinates: LayoutCoordinates - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 64.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.VERTICAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 64.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } composeTestRule.setContent { @@ -1360,13 +1298,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = HorizontalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ), + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1405,26 +1340,19 @@ class TwoPaneTest { composeTestRule.setContent { density = LocalDensity.current - val windowGeometry = object : WindowGeometry { - val fakeWindowGeometry by lazy { - fakeWindowGeometry( - density = density, - twoPaneCoordinates = twoPaneCoordinates, - localFoldingFeatures = listOf( - LocalFoldingFeature( - center = 450.dp, - size = 0.dp, - state = FoldingFeature.State.FLAT, - orientation = FoldingFeature.Orientation.VERTICAL - ) + val displayFeatures = DelegateList { + fakeDisplayFeatures( + density = density, + twoPaneCoordinates = twoPaneCoordinates, + localFoldingFeatures = listOf( + LocalFoldingFeature( + center = 450.dp, + size = 0.dp, + state = FoldingFeature.State.FLAT, + orientation = FoldingFeature.Orientation.VERTICAL ) ) - } - - override val windowSizeClass: WindowSizeClass get() = - fakeWindowGeometry.windowSizeClass - override val displayFeatures: List get() = - fakeWindowGeometry.displayFeatures + ) } TwoPane( @@ -1444,13 +1372,10 @@ class TwoPaneTest { .onPlaced { secondCoordinates = it } ) }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 1f / 3f - ) + strategy = VerticalTwoPaneStrategy( + splitFraction = 1f / 3f ), + displayFeatures = displayFeatures, modifier = Modifier .requiredSize(900.dp, 1200.dp) .onPlaced { twoPaneCoordinates = it } @@ -1503,6 +1428,27 @@ private data class LocalFoldingFeature( val orientation: FoldingFeature.Orientation ) +/** + * A [List] that lazily constructs the backing delegate list by calling the provided lambda. + */ +private class DelegateList( + listFactory: () -> List +) : List { + val delegate by lazy(listFactory) + override val size: Int get() = delegate.size + override fun get(index: Int): T = delegate[index] + override fun isEmpty(): Boolean = delegate.isEmpty() + override fun iterator(): Iterator = delegate.iterator() + override fun listIterator(): ListIterator = delegate.listIterator() + override fun listIterator(index: Int): ListIterator = delegate.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int): List = + delegate.subList(fromIndex, toIndex) + override fun lastIndexOf(element: T): Int = delegate.lastIndexOf(element) + override fun indexOf(element: T): Int = delegate.indexOf(element) + override fun containsAll(elements: Collection): Boolean = delegate.containsAll(elements) + override fun contains(element: T): Boolean = delegate.contains(element) +} + /** * Folding features are always expressed in window coordinates. * @@ -1515,92 +1461,78 @@ private data class LocalFoldingFeature( * In other words, this allows specifying [LocalFoldingFeature]s as if the [TwoPane] layout matches * the window bounds. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalWindowApi::class) -private fun fakeWindowGeometry( +@OptIn(ExperimentalWindowApi::class) +private fun fakeDisplayFeatures( density: Density, twoPaneCoordinates: LayoutCoordinates, localFoldingFeatures: List -): WindowGeometry = - object : WindowGeometry { - override val windowSizeClass: WindowSizeClass = - WindowSizeClass.calculateFromSize(twoPaneCoordinates.size.toSize().toDpSize(density)) - - override val displayFeatures: List get() { - val boundsTopLeftOffset = twoPaneCoordinates.localToWindow( - twoPaneCoordinates.size.toIntRect().topLeft.toOffset() - ) - val boundsBottomRightOffset = twoPaneCoordinates.localToWindow( - twoPaneCoordinates.size.toIntRect().bottomRight.toOffset() - ) - val bounds = Rect( - boundsTopLeftOffset, - boundsBottomRightOffset - ) - - return localFoldingFeatures.map { localFoldingFeature -> - val foldLeft: Float - val foldTop: Float - val foldRight: Float - val foldBottom: Float - - with(density) { - if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { - foldLeft = 0f - foldTop = - (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() - foldRight = twoPaneCoordinates.size.width.toFloat() - foldBottom = - (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() - } else { - foldLeft = - (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() - foldTop = 0f - foldRight = - (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() - foldBottom = twoPaneCoordinates.size.height.toFloat() - } - } - - val foldTopLeftOffset = twoPaneCoordinates.localToWindow( - Offset(foldLeft, foldTop) - ) - val foldBottomRightOffset = twoPaneCoordinates.localToWindow( - Offset(foldRight, foldBottom) - ) - val foldBounds = Rect( - foldTopLeftOffset, - foldBottomRightOffset, - ) +): List { + val boundsTopLeftOffset = twoPaneCoordinates.localToWindow( + twoPaneCoordinates.size.toIntRect().topLeft.toOffset() + ) + val boundsBottomRightOffset = twoPaneCoordinates.localToWindow( + twoPaneCoordinates.size.toIntRect().bottomRight.toOffset() + ) + val bounds = Rect( + boundsTopLeftOffset, + boundsBottomRightOffset + ) - val center: Int - val size: Int - - if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { - center = foldBounds.center.y.roundToInt() - size = foldBounds.height.roundToInt() - } else { - center = foldBounds.center.x.roundToInt() - size = foldBounds.width.roundToInt() - } - - FoldingFeature( - windowBounds = bounds.toAndroidRect(), - center = center, - size = size, - state = localFoldingFeature.state, - orientation = localFoldingFeature.orientation, - ) + return localFoldingFeatures.map { localFoldingFeature -> + val foldLeft: Float + val foldTop: Float + val foldRight: Float + val foldBottom: Float + + with(density) { + if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { + foldLeft = 0f + foldTop = + (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() + foldRight = twoPaneCoordinates.size.width.toFloat() + foldBottom = + (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() + } else { + foldLeft = + (localFoldingFeature.center - localFoldingFeature.size / 2).toPx() + foldTop = 0f + foldRight = + (localFoldingFeature.center + localFoldingFeature.size / 2).toPx() + foldBottom = twoPaneCoordinates.size.height.toFloat() } } - } -private fun Size.toDpSize(density: Density) = - with(density) { - DpSize( - width = width.toDp(), - height = height.toDp() + val foldTopLeftOffset = twoPaneCoordinates.localToWindow( + Offset(foldLeft, foldTop) + ) + val foldBottomRightOffset = twoPaneCoordinates.localToWindow( + Offset(foldRight, foldBottom) + ) + val foldBounds = Rect( + foldTopLeftOffset, + foldBottomRightOffset, + ) + + val center: Int + val size: Int + + if (localFoldingFeature.orientation == FoldingFeature.Orientation.HORIZONTAL) { + center = foldBounds.center.y.roundToInt() + size = foldBounds.height.roundToInt() + } else { + center = foldBounds.center.x.roundToInt() + size = foldBounds.width.roundToInt() + } + + FoldingFeature( + windowBounds = bounds.toAndroidRect(), + center = center, + size = size, + state = localFoldingFeature.state, + orientation = localFoldingFeature.orientation, ) } +} private fun Rect.round(): IntRect = IntRect( diff --git a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt b/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt deleted file mode 100644 index 4d056b199..000000000 --- a/adaptive/src/sharedTest/kotlin/com/google/accompanist/adaptive/WindowGeometryTest.kt +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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 - * - * https://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 com.google.accompanist.adaptive - -import androidx.activity.ComponentActivity -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.ui.graphics.toComposeRect -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowLayoutInfo -import androidx.window.layout.WindowMetricsCalculator -import androidx.window.testing.layout.FoldingFeature -import androidx.window.testing.layout.WindowLayoutInfoPublisherRule -import com.google.common.truth.Truth.assertThat -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -@RunWith(AndroidJUnit4::class) -class WindowGeometryTest { - - @get:Rule - val composeTestRule = createAndroidComposeRule() - - @get:Rule - val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule() - - @Test - fun empty_folding_features_is_correct() { - lateinit var expectedWindowSizeClass: WindowSizeClass - lateinit var windowGeometry: WindowGeometry - - composeTestRule.setContent { - expectedWindowSizeClass = WindowSizeClass.calculateFromSize( - with(LocalDensity.current) { - WindowMetricsCalculator - .getOrCreate() - .computeCurrentWindowMetrics(composeTestRule.activity) - .bounds - .toComposeRect() - .size - .toDpSize() - } - ) - windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) - } - - windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) - - assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) - assertThat(windowGeometry.displayFeatures).isEmpty() - } - - @Test - fun single_folding_features_is_correct() { - lateinit var expectedWindowSizeClass: WindowSizeClass - lateinit var windowGeometry: WindowGeometry - - composeTestRule.setContent { - expectedWindowSizeClass = WindowSizeClass.calculateFromSize( - with(LocalDensity.current) { - WindowMetricsCalculator - .getOrCreate() - .computeCurrentWindowMetrics(composeTestRule.activity) - .bounds - .toComposeRect() - .size - .toDpSize() - } - ) - windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) - } - - val fakeFoldingFeature = FoldingFeature( - activity = composeTestRule.activity, - center = 200, - size = 40, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.VERTICAL, - ) - - windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( - WindowLayoutInfo( - listOf( - fakeFoldingFeature - ) - ) - ) - - assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) - assertThat(windowGeometry.displayFeatures).hasSize(1) - assertThat(windowGeometry.displayFeatures[0]).isEqualTo(fakeFoldingFeature) - } - - @Test - fun updating_folding_features_is_correct() { - lateinit var expectedWindowSizeClass: WindowSizeClass - lateinit var windowGeometry: WindowGeometry - - composeTestRule.setContent { - expectedWindowSizeClass = WindowSizeClass.calculateFromSize( - with(LocalDensity.current) { - WindowMetricsCalculator - .getOrCreate() - .computeCurrentWindowMetrics(composeTestRule.activity) - .bounds - .toComposeRect() - .size - .toDpSize() - } - ) - windowGeometry = calculateWindowGeometry(activity = composeTestRule.activity) - } - - windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(WindowLayoutInfo(emptyList())) - - val fakeFoldingFeature = FoldingFeature( - activity = composeTestRule.activity, - center = 200, - size = 40, - state = FoldingFeature.State.HALF_OPENED, - orientation = FoldingFeature.Orientation.VERTICAL, - ) - - windowLayoutInfoPublisherRule.overrideWindowLayoutInfo( - WindowLayoutInfo( - listOf( - fakeFoldingFeature - ) - ) - ) - - assertThat(windowGeometry.windowSizeClass).isEqualTo(expectedWindowSizeClass) - assertThat(windowGeometry.displayFeatures).hasSize(1) - assertThat(windowGeometry.displayFeatures[0]).isEqualTo(fakeFoldingFeature) - } -} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index c25b9381f..98877ba6a 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -423,6 +423,26 @@ + + + + + + + + + + + + + + diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt index 3d5564637..2be94a394 100644 --- a/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/BasicTwoPaneSample.kt @@ -31,7 +31,7 @@ import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.google.accompanist.adaptive.TwoPaneStrategy import com.google.accompanist.adaptive.VerticalTwoPaneStrategy -import com.google.accompanist.adaptive.calculateWindowGeometry +import com.google.accompanist.adaptive.calculateDisplayFeatures import com.google.accompanist.sample.AccompanistSampleTheme class BasicTwoPaneSample : ComponentActivity() { @@ -39,7 +39,7 @@ class BasicTwoPaneSample : ComponentActivity() { super.onCreate(savedInstanceState) setContent { AccompanistSampleTheme { - val windowGeometry = calculateWindowGeometry(this) + val displayFeatures = calculateDisplayFeatures(this) TwoPane( first = { @@ -66,23 +66,19 @@ class BasicTwoPaneSample : ComponentActivity() { } } }, - strategy = TwoPaneStrategy( - windowGeometry = windowGeometry, - defaultStrategy = { density, layoutDirection, layoutCoordinates -> - // Split vertically if the height is larger than the width - if (layoutCoordinates.size.height >= layoutCoordinates.size.width) { - VerticalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 0.75f, - ) - } else { - HorizontalTwoPaneStrategy( - windowGeometry = windowGeometry, - splitFraction = 0.75f, - ) - }.calculateSplitResult(density, layoutDirection, layoutCoordinates) - }, - ), + strategy = TwoPaneStrategy { density, layoutDirection, layoutCoordinates -> + // Split vertically if the height is larger than the width + if (layoutCoordinates.size.height >= layoutCoordinates.size.width) { + VerticalTwoPaneStrategy( + splitFraction = 0.75f + ) + } else { + HorizontalTwoPaneStrategy( + splitFraction = 0.75f + ) + }.calculateSplitResult(density, layoutDirection, layoutCoordinates) + }, + displayFeatures = displayFeatures, modifier = Modifier.padding(8.dp) ) } diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt new file mode 100644 index 000000000..723b07b0c --- /dev/null +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/HorizontalTwoPaneSample.kt @@ -0,0 +1,78 @@ +/* + * 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 + * + * https://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 com.google.accompanist.sample.adaptive + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.accompanist.adaptive.FoldAwareConfiguration +import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy +import com.google.accompanist.adaptive.TwoPane +import com.google.accompanist.adaptive.calculateDisplayFeatures +import com.google.accompanist.sample.AccompanistSampleTheme + +class HorizontalTwoPaneSample : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AccompanistSampleTheme { + val displayFeatures = calculateDisplayFeatures(this) + + TwoPane( + first = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("First") + } + } + }, + second = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("Second") + } + } + }, + strategy = HorizontalTwoPaneStrategy( + splitFraction = 1f / 3f, + ), + displayFeatures = displayFeatures, + foldAwareConfiguration = FoldAwareConfiguration.VerticalFoldsOnly, + modifier = Modifier.padding(8.dp) + ) + } + } + } +} diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt new file mode 100644 index 000000000..9455a1021 --- /dev/null +++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/VerticalTwoPaneSample.kt @@ -0,0 +1,78 @@ +/* + * 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 + * + * https://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 com.google.accompanist.sample.adaptive + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.accompanist.adaptive.FoldAwareConfiguration +import com.google.accompanist.adaptive.TwoPane +import com.google.accompanist.adaptive.VerticalTwoPaneStrategy +import com.google.accompanist.adaptive.calculateDisplayFeatures +import com.google.accompanist.sample.AccompanistSampleTheme + +class VerticalTwoPaneSample : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AccompanistSampleTheme { + val displayFeatures = calculateDisplayFeatures(this) + + TwoPane( + first = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("First") + } + } + }, + second = { + Card( + modifier = Modifier.padding(8.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Text("Second") + } + } + }, + strategy = VerticalTwoPaneStrategy( + splitOffset = 200.dp, + ), + displayFeatures = displayFeatures, + foldAwareConfiguration = FoldAwareConfiguration.HorizontalFoldsOnly, + modifier = Modifier.padding(8.dp) + ) + } + } + } +} diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index d656dbb5e..d5747166e 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -65,4 +65,7 @@ WebView: Basic Adaptive: TwoPane Basic + Adaptive: TwoPane Horizontal + Adaptive: TwoPane Vertical + From 5e01d34a5f6fb98cbd5088ae18f12266bd13e736 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Tue, 9 Aug 2022 14:08:44 -0700 Subject: [PATCH 09/10] Remove material3 library --- adaptive/build.gradle | 1 - gradle/libs.versions.toml | 3 --- 2 files changed, 4 deletions(-) diff --git a/adaptive/build.gradle b/adaptive/build.gradle index d4689258e..fa867381b 100644 --- a/adaptive/build.gradle +++ b/adaptive/build.gradle @@ -84,7 +84,6 @@ android { dependencies { api libs.compose.foundation.foundation api libs.compose.ui.ui - api libs.compose.material3.windowSizeClass api libs.androidx.window implementation libs.napier diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c454cc72..ea2a291a4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,8 +14,6 @@ coroutines = "1.6.0" okhttp = "3.12.13" coil = "1.3.2" -composeMaterial3 = "1.0.0-alpha15" - androidxtest = "1.4.0" androidxnavigation = "2.5.0-rc02" androidxWindow = "1.0.0" @@ -30,7 +28,6 @@ compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", ve compose-foundation-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "compose" } compose-material-material = { module = "androidx.compose.material:material", version.ref = "compose" } -compose-material3-windowSizeClass = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "composeMaterial3" } compose-material-iconsext = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } compose-animation-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } From 11fee5ef102da65356d4d38adb877a50223c5a4d Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Wed, 10 Aug 2022 10:24:27 -0700 Subject: [PATCH 10/10] Update to latest Compose dependencies --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea2a291a4..357cf6564 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -compose = "1.3.0-SNAPSHOT" -composeCompiler = "1.3.0-rc01" -composesnapshot = "8898423" # a single character = no snapshot +compose = "1.3.0-alpha03" +composeCompiler = "1.3.0" +composesnapshot = "-" # a single character = no snapshot # gradlePlugin and lint need to be updated together gradlePlugin = "7.3.0-beta05"