From d4cb092e2143f420fa9962565a529973f88dfdca Mon Sep 17 00:00:00 2001 From: Kristen Halper <33138268+khalp@users.noreply.github.com> Date: Fri, 10 Dec 2021 15:11:37 -0500 Subject: [PATCH] Create foldable/large screen helper module (#70) * Create window-info module * Update build.gradle settings * Add helper classes * Update ComposeGallery to use module * Update to wm beta04 and twopanelayout alpha10 * Update Compose Gallery tests to follow new app structure * Rename module and classes * Update NavigationRail sample to use module * Add window mode (~device posture) to window state * Update companion pane to use module * Fix module manifest formatting * Update dual view to use module * Update list detail to use module * Update two page to use module * Refactor WindowState properties * Expose additional fold info in WindowState fold state, isSeparating, and occlusion type * Rename module to window state * Rename package to windowstate * Update large screen logic to only include expanded size class * Add tests for window state module * Remove FoldingFeature dependency in WindowState * Remove redundant JWM dependencies * Update ComposeGallery tests to use new fold state enum * Remove unnecessary JWM imports from ComposeGallery tests --- CompanionPane/build.gradle | 2 +- .../display/samples/companionpane/HomePage.kt | 75 +++---------- .../samples/companionpane/MainActivity.kt | 12 +- ComposeGallery/build.gradle | 2 +- .../composegallery/PaneSynchronizationTest.kt | 40 ++++++- .../samples/composegallery/TopAppBarTest.kt | 16 ++- .../samples/composegallery/MainActivity.kt | 86 +-------------- .../composegallery/ui/view/HomePage.kt | 12 +- DualView/build.gradle | 2 +- .../display/samples/dualview/MainActivity.kt | 13 ++- .../samples/dualview/ui/home/HomePage.kt | 48 +------- ExtendedCanvas/build.gradle | 1 - ListDetail/build.gradle | 2 +- .../samples/listdetail/MainActivity.kt | 13 ++- .../samples/listdetail/ui/view/DetailView.kt | 4 +- .../samples/listdetail/ui/view/MainPage.kt | 40 +------ NavigationRail/build.gradle | 2 +- .../samples/navigationrail/MainActivity.kt | 15 ++- .../ui/components/ContentDrawer.kt | 8 +- .../navigationrail/ui/view/HomePage.kt | 72 ++---------- .../navigationrail/ui/view/ItemDetailView.kt | 6 +- .../ui/view/ItemDetailsDrawer.kt | 4 +- TestUtils/src/main/AndroidManifest.xml | 5 +- TwoPage/build.gradle | 2 +- .../display/samples/twopage/MainActivity.kt | 14 +-- .../samples/twopage/ui/home/HomePage.kt | 78 ++++--------- WindowState/.gitignore | 1 + WindowState/build.gradle | 57 ++++++++++ .../dualscreen/windowstate/WindowStateTest.kt | 100 +++++++++++++++++ WindowState/src/main/AndroidManifest.xml | 7 ++ .../dualscreen/windowstate/Dimension.kt | 8 ++ .../dualscreen/windowstate/FoldState.kt | 8 ++ .../dualscreen/windowstate/WindowMode.kt | 25 +++++ .../dualscreen/windowstate/WindowSizeClass.kt | 30 +++++ .../dualscreen/windowstate/WindowState.kt | 104 ++++++++++++++++++ .../windowstate/WindowStateHelper.kt | 74 +++++++++++++ .../dualscreen/windowstate/WindowModeTest.kt | 23 ++++ .../windowstate/WindowSizeClassTest.kt | 42 +++++++ dependencies.gradle | 10 +- settings.gradle | 2 +- 40 files changed, 652 insertions(+), 413 deletions(-) create mode 100644 WindowState/.gitignore create mode 100644 WindowState/build.gradle create mode 100644 WindowState/src/androidTest/java/com/microsoft/device/dualscreen/windowstate/WindowStateTest.kt create mode 100644 WindowState/src/main/AndroidManifest.xml create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/Dimension.kt create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/FoldState.kt create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowMode.kt create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClass.kt create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowState.kt create mode 100644 WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowStateHelper.kt create mode 100644 WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowModeTest.kt create mode 100644 WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClassTest.kt diff --git a/CompanionPane/build.gradle b/CompanionPane/build.gradle index c3221a9..84b561d 100644 --- a/CompanionPane/build.gradle +++ b/CompanionPane/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime @@ -44,6 +43,7 @@ dependencies { implementation composeDependencies.activityCompose implementation microsoftDependencies.twoPaneLayout + implementation project(':WindowState') implementation googleDependencies.material } \ No newline at end of file diff --git a/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/HomePage.kt b/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/HomePage.kt index e40c0d3..139cdf9 100644 --- a/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/HomePage.kt +++ b/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/HomePage.kt @@ -5,7 +5,6 @@ package com.microsoft.device.display.samples.companionpane -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -26,7 +25,6 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -34,14 +32,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowInfoRepository import com.microsoft.device.display.samples.companionpane.uicomponent.BrightnessPanel import com.microsoft.device.display.samples.companionpane.uicomponent.DefinitionPanel import com.microsoft.device.display.samples.companionpane.uicomponent.EffectPanel @@ -51,56 +46,20 @@ import com.microsoft.device.display.samples.companionpane.uicomponent.MagicWandP import com.microsoft.device.display.samples.companionpane.uicomponent.ShortFilterControl import com.microsoft.device.display.samples.companionpane.uicomponent.VignettePanel import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout -import kotlinx.coroutines.flow.collect +import com.microsoft.device.dualscreen.windowstate.WindowMode +import com.microsoft.device.dualscreen.windowstate.WindowState private val shortSlideWidth = 200.dp private val longSlideWidth = 350.dp -const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585 - -enum class ScreenState { - SinglePortrait, - SingleLandscape, - DualPortrait, - DualLandscape -} @Composable -fun SetupUI(windowInfoRep: WindowInfoRepository) { - var screenState by remember { mutableStateOf(ScreenState.SinglePortrait) } - var isAppSpanned by remember { mutableStateOf(false) } - var isHingeHorizontal by remember { mutableStateOf(false) } - - LaunchedEffect(windowInfoRep) { - windowInfoRep.windowLayoutInfo - .collect { newLayoutInfo -> - val displayFeatures = newLayoutInfo.displayFeatures - isAppSpanned = displayFeatures.isNotEmpty() - if (isAppSpanned) { - val foldingFeature = displayFeatures.first() as? FoldingFeature - foldingFeature?.let { - isHingeHorizontal = it.orientation == FoldingFeature.Orientation.HORIZONTAL - } - } - } - } - - val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp - val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP - val isDualScreen = (isAppSpanned || isTablet) - - screenState = - if (isDualScreen) { - val showDualLandscape = if (isAppSpanned) isHingeHorizontal else isPortrait - if (showDualLandscape) ScreenState.DualLandscape else ScreenState.DualPortrait - } else { - // NOTE: the LocalConfiguration orientation info should only be used in single screen mode - if (isPortrait) ScreenState.SinglePortrait else ScreenState.SingleLandscape - } +fun CompanionPaneApp(windowState: WindowState) { + var windowMode by remember { mutableStateOf(WindowMode.SINGLE_PORTRAIT) } + windowMode = windowState.windowMode TwoPaneLayout( - pane1 = { Pane1(screenState) }, - pane2 = { Pane2(screenState) }, + pane1 = { Pane1(windowMode) }, + pane2 = { Pane2(windowMode) }, ) } @@ -127,14 +86,14 @@ fun ShowWithTopBar(content: @Composable () -> Unit, title: String? = null) { } @Composable -fun Pane1(screenState: ScreenState) { +fun Pane1(windowMode: WindowMode) { ShowWithTopBar( content = { - when (screenState) { - ScreenState.SinglePortrait -> PortraitLayout() - ScreenState.SingleLandscape -> LandscapeLayout() - ScreenState.DualPortrait -> DualPortraitPane1() - ScreenState.DualLandscape -> DualLandscapePane1() + when (windowMode) { + WindowMode.SINGLE_PORTRAIT -> PortraitLayout() + WindowMode.SINGLE_LANDSCAPE -> LandscapeLayout() + WindowMode.DUAL_PORTRAIT -> DualPortraitPane1() + WindowMode.DUAL_LANDSCAPE -> DualLandscapePane1() } }, title = stringResource(R.string.app_name) @@ -142,14 +101,14 @@ fun Pane1(screenState: ScreenState) { } @Composable -fun Pane2(screenState: ScreenState) { - when (screenState) { - ScreenState.DualPortrait -> { +fun Pane2(windowMode: WindowMode) { + when (windowMode) { + WindowMode.DUAL_PORTRAIT -> { ShowWithTopBar( content = { DualPortraitPane2() } ) } - ScreenState.DualLandscape -> DualLandscapePane2() + WindowMode.DUAL_LANDSCAPE -> DualLandscapePane2() else -> {} } } diff --git a/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/MainActivity.kt b/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/MainActivity.kt index 9634f7f..c21c28c 100644 --- a/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/MainActivity.kt +++ b/CompanionPane/src/main/java/com/microsoft/device/display/samples/companionpane/MainActivity.kt @@ -8,21 +8,21 @@ package com.microsoft.device.display.samples.companionpane import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.window.layout.WindowInfoRepository -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import com.microsoft.device.display.samples.companionpane.ui.CompanionPaneAppsTheme +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState class MainActivity : AppCompatActivity() { - private lateinit var windowInfoRep: WindowInfoRepository + private lateinit var windowState: WindowState override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowInfoRep = windowInfoRepository() - setContent { + windowState = rememberWindowState() + CompanionPaneAppsTheme { - SetupUI(windowInfoRep) + CompanionPaneApp(windowState) } } } diff --git a/ComposeGallery/build.gradle b/ComposeGallery/build.gradle index 56544ec..6d6eb42 100644 --- a/ComposeGallery/build.gradle +++ b/ComposeGallery/build.gradle @@ -40,7 +40,6 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime @@ -49,6 +48,7 @@ dependencies { implementation composeDependencies.activityCompose implementation microsoftDependencies.twoPaneLayout + implementation project(':WindowState') implementation googleDependencies.material diff --git a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt index 55a4a27..fd0670b 100644 --- a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt +++ b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/PaneSynchronizationTest.kt @@ -5,6 +5,7 @@ package com.microsoft.device.display.samples.composegallery +import android.graphics.Rect import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor @@ -26,6 +27,9 @@ import com.microsoft.device.display.samples.composegallery.ui.view.ComposeGaller import com.microsoft.device.dualscreen.testutils.getString import com.microsoft.device.dualscreen.testutils.simulateHorizontalFold import com.microsoft.device.dualscreen.testutils.simulateVerticalFold +import com.microsoft.device.dualscreen.windowstate.FoldState +import com.microsoft.device.dualscreen.windowstate.WindowSizeClass +import com.microsoft.device.dualscreen.windowstate.WindowState import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -52,8 +56,16 @@ class PaneSynchronizationTest { composeTestRule.setContent { ComposeGalleryTheme { ComposeGalleryApp( - foldableState = FoldableState(hasFold = true, isFoldHorizontal = false), - widthSizeClass = WindowSizeClass.Medium + WindowState( + hasFold = true, + isFoldHorizontal = false, + foldBounds = Rect(), + foldState = FoldState.HALF_OPENED, + foldSeparates = true, + foldOccludes = true, + widthSizeClass = WindowSizeClass.MEDIUM, + heightSizeClass = WindowSizeClass.MEDIUM + ) ) } } @@ -93,8 +105,16 @@ class PaneSynchronizationTest { composeTestRule.setContent { ComposeGalleryTheme { ComposeGalleryApp( - foldableState = FoldableState(hasFold = false, isFoldHorizontal = false), - widthSizeClass = WindowSizeClass.Compact + WindowState( + hasFold = false, + isFoldHorizontal = false, + foldBounds = Rect(), + foldState = FoldState.HALF_OPENED, + foldSeparates = true, + foldOccludes = true, + widthSizeClass = WindowSizeClass.COMPACT, + heightSizeClass = WindowSizeClass.MEDIUM + ) ) } } @@ -126,8 +146,16 @@ class PaneSynchronizationTest { composeTestRule.setContent { ComposeGalleryTheme { ComposeGalleryApp( - foldableState = FoldableState(hasFold = true, isFoldHorizontal = true), - widthSizeClass = WindowSizeClass.Compact + WindowState( + hasFold = true, + isFoldHorizontal = true, + foldBounds = Rect(), + foldState = FoldState.HALF_OPENED, + foldSeparates = true, + foldOccludes = true, + widthSizeClass = WindowSizeClass.COMPACT, + heightSizeClass = WindowSizeClass.MEDIUM + ) ) } } diff --git a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt index 9be8104..5b69ee8 100644 --- a/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt +++ b/ComposeGallery/src/androidTest/java/com/microsoft/device/display/samples/composegallery/TopAppBarTest.kt @@ -5,6 +5,7 @@ package com.microsoft.device.display.samples.composegallery +import android.graphics.Rect import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed @@ -21,6 +22,9 @@ import com.microsoft.device.display.samples.composegallery.ui.view.ComposeGaller import com.microsoft.device.display.samples.composegallery.ui.view.DetailPane import com.microsoft.device.display.samples.composegallery.ui.view.ListPane import com.microsoft.device.dualscreen.testutils.getString +import com.microsoft.device.dualscreen.windowstate.FoldState +import com.microsoft.device.dualscreen.windowstate.WindowSizeClass +import com.microsoft.device.dualscreen.windowstate.WindowState import org.junit.Rule import org.junit.Test @@ -178,8 +182,16 @@ class TopAppBarTest { composeTestRule.setContent { ComposeGalleryTheme { ComposeGalleryApp( - foldableState = FoldableState(hasFold = false, isFoldHorizontal = false), - widthSizeClass = WindowSizeClass.Compact + WindowState( + hasFold = false, + isFoldHorizontal = false, + foldBounds = Rect(), + foldState = FoldState.HALF_OPENED, + foldSeparates = true, + foldOccludes = true, + widthSizeClass = WindowSizeClass.COMPACT, + heightSizeClass = WindowSizeClass.MEDIUM + ) ) } } diff --git a/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/MainActivity.kt b/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/MainActivity.kt index 2a15ff9..5e1e75b 100644 --- a/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/MainActivity.kt +++ b/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/MainActivity.kt @@ -5,102 +5,26 @@ package com.microsoft.device.display.samples.composegallery -import android.app.Activity import android.os.Bundle import androidx.activity.compose.setContent -import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import com.microsoft.device.display.samples.composegallery.ui.ComposeGalleryTheme import com.microsoft.device.display.samples.composegallery.ui.view.ComposeGalleryApp -import kotlinx.coroutines.flow.collect - -enum class WindowSizeClass { Compact, Medium, Expanded } -data class FoldableState(val hasFold: Boolean, val isFoldHorizontal: Boolean) +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState class MainActivity : AppCompatActivity() { + private lateinit var windowState: WindowState override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - val widthSizeClass = rememberWidthSizeClass() - val foldableState = rememberFoldableState() + windowState = rememberWindowState() ComposeGalleryTheme { - ComposeGalleryApp(foldableState, widthSizeClass) + ComposeGalleryApp(windowState) } } } } - -@Composable -fun Activity.rememberFoldableState(): FoldableState { - val windowInfoRepo = windowInfoRepository() - var hasFold by remember { mutableStateOf(false) } - var isFoldHorizontal by remember { mutableStateOf(false) } - - LaunchedEffect(windowInfoRepo) { - windowInfoRepo.windowLayoutInfo.collect { newLayoutInfo -> - hasFold = newLayoutInfo.displayFeatures.isNotEmpty() - if (hasFold) { - val fold = newLayoutInfo.displayFeatures.firstOrNull() as? FoldingFeature - fold?.let { - isFoldHorizontal = it.orientation == FoldingFeature.Orientation.HORIZONTAL - } - } - } - } - - return FoldableState(hasFold, isFoldHorizontal) -} - -/** - * Implementation taken from JetNews sample - * https://github.com/android/compose-samples/blob/main/JetNews/app/src/main/java/com/example/jetnews/utils/WindowSize.kt - * - * Remembers the [WindowSizeClass] class for the window corresponding to the current window metrics. - */ -@Composable -fun rememberWidthSizeClass(): WindowSizeClass { - // Get the size (in pixels) of the window - val windowSize = rememberWindowSize() - - // Calculate the width window size class - return getWindowSizeClass(windowSize) -} - -/** - * Remembers the [Size] in pixels of the window corresponding to the current window metrics. - */ -@Composable -private fun rememberWindowSize(): Dp { - val configuration = LocalConfiguration.current - - val windowMetrics = remember(configuration) { - configuration.smallestScreenWidthDp.dp - } - return windowMetrics -} - -/** - * Partitions a [Dp] into a enumerated [WindowSizeClass] class. - */ -@VisibleForTesting -fun getWindowSizeClass(windowDpWidth: Dp): WindowSizeClass = when { - windowDpWidth < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative") - windowDpWidth < 600.dp -> WindowSizeClass.Compact - windowDpWidth < 840.dp -> WindowSizeClass.Medium - else -> WindowSizeClass.Expanded -} diff --git a/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/ui/view/HomePage.kt b/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/ui/view/HomePage.kt index cd7f9bb..c347ce5 100644 --- a/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/ui/view/HomePage.kt +++ b/ComposeGallery/src/main/java/com/microsoft/device/display/samples/composegallery/ui/view/HomePage.kt @@ -5,7 +5,6 @@ package com.microsoft.device.display.samples.composegallery.ui.view -import android.content.res.Configuration import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -16,27 +15,22 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.microsoft.device.display.samples.composegallery.FoldableState import com.microsoft.device.display.samples.composegallery.R -import com.microsoft.device.display.samples.composegallery.WindowSizeClass import com.microsoft.device.display.samples.composegallery.models.DataProvider import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout import com.microsoft.device.dualscreen.twopanelayout.TwoPaneMode +import com.microsoft.device.dualscreen.windowstate.WindowState @Composable -fun ComposeGalleryApp(foldableState: FoldableState, widthSizeClass: WindowSizeClass) { +fun ComposeGalleryApp(windowState: WindowState) { // Check if app should be in dual mode - val isDualPortraitFoldable = foldableState.hasFold && !foldableState.isFoldHorizontal - val isLargeScreen = !foldableState.hasFold && widthSizeClass != WindowSizeClass.Compact - val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - val isDualMode = isDualPortraitFoldable || (isLargeScreen && isLandscape) + val isDualMode = windowState.isDualPortrait() // Get relevant image data for the panes val models = DataProvider.imageModels diff --git a/DualView/build.gradle b/DualView/build.gradle index 9ac5f4b..2fa9eec 100644 --- a/DualView/build.gradle +++ b/DualView/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime @@ -46,4 +45,5 @@ dependencies { implementation googleDependencies.material implementation microsoftDependencies.twoPaneLayout + implementation project(':WindowState') } \ No newline at end of file diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/MainActivity.kt b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/MainActivity.kt index b64c838..bbb1e96 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/MainActivity.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/MainActivity.kt @@ -9,25 +9,26 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider -import androidx.window.layout.WindowInfoRepository -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import com.microsoft.device.display.samples.dualview.models.AppStateViewModel -import com.microsoft.device.display.samples.dualview.ui.home.SetupUI +import com.microsoft.device.display.samples.dualview.ui.home.DualViewApp import com.microsoft.device.display.samples.dualview.ui.theme.DualViewComposeSampleTheme +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState class MainActivity : AppCompatActivity() { - private lateinit var windowInfoRep: WindowInfoRepository + private lateinit var windowState: WindowState private lateinit var appStateViewModel: AppStateViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowInfoRep = windowInfoRepository() appStateViewModel = ViewModelProvider(this).get(AppStateViewModel::class.java) setContent { + windowState = rememberWindowState() + DualViewComposeSampleTheme { - SetupUI(appStateViewModel, windowInfoRep) + DualViewApp(appStateViewModel, windowState) } } } diff --git a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/home/HomePage.kt b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/home/HomePage.kt index a049947..afc939c 100644 --- a/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/home/HomePage.kt +++ b/DualView/src/main/java/com/microsoft/device/display/samples/dualview/ui/home/HomePage.kt @@ -7,58 +7,18 @@ package com.microsoft.device.display.samples.dualview.ui.home import androidx.compose.foundation.Image import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowInfoRepository import com.microsoft.device.display.samples.dualview.models.AppStateViewModel import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout -import kotlinx.coroutines.flow.collect - -private lateinit var appStateViewModel: AppStateViewModel -const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585 +import com.microsoft.device.dualscreen.windowstate.WindowState @Composable -fun SetupUI(viewModel: AppStateViewModel, windowInfoRep: WindowInfoRepository) { - appStateViewModel = viewModel - - var isAppSpanned by remember { mutableStateOf(false) } - LaunchedEffect(windowInfoRep) { - windowInfoRep.windowLayoutInfo - .collect { newLayoutInfo -> - val displayFeatures = newLayoutInfo.displayFeatures - isAppSpanned = displayFeatures.isNotEmpty() - var viewWidth = 0 - if (isAppSpanned) { - val foldingFeature = displayFeatures.first() as FoldingFeature - viewWidth = if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) { - foldingFeature.bounds.left - } else { - foldingFeature.bounds.top - } - } - appStateViewModel.viewWidth = viewWidth - } - } - - val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp - val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP - val isDualScreen = isAppSpanned || isTablet - DualScreenUI(isDualScreen) -} - -@Composable -fun DualScreenUI(isDualScreen: Boolean) { +fun DualViewApp(viewModel: AppStateViewModel, windowState: WindowState) { TwoPaneLayout( - pane1 = { RestaurantViewWithTopBar(isDualScreen = isDualScreen, appStateViewModel = appStateViewModel) }, - pane2 = { MapViewWithTopBar(isDualScreen = isDualScreen, appStateViewModel = appStateViewModel) } + pane1 = { RestaurantViewWithTopBar(windowState.isDualScreen(), viewModel) }, + pane2 = { MapViewWithTopBar(windowState.isDualScreen(), viewModel) } ) } diff --git a/ExtendedCanvas/build.gradle b/ExtendedCanvas/build.gradle index 5e55a8e..5718d1a 100644 --- a/ExtendedCanvas/build.gradle +++ b/ExtendedCanvas/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime diff --git a/ListDetail/build.gradle b/ListDetail/build.gradle index 32bfab0..850b839 100644 --- a/ListDetail/build.gradle +++ b/ListDetail/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime @@ -46,4 +45,5 @@ dependencies { implementation googleDependencies.material implementation microsoftDependencies.twoPaneLayout + implementation project(':WindowState') } \ No newline at end of file diff --git a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/MainActivity.kt b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/MainActivity.kt index b602e5b..e8de7c2 100644 --- a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/MainActivity.kt +++ b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/MainActivity.kt @@ -9,25 +9,26 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider -import androidx.window.layout.WindowInfoRepository -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import com.microsoft.device.display.samples.listdetail.models.AppStateViewModel import com.microsoft.device.display.samples.listdetail.ui.theme.ListDetailComposeSampleTheme -import com.microsoft.device.display.samples.listdetail.ui.view.SetupUI +import com.microsoft.device.display.samples.listdetail.ui.view.ListDetailApp +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState class MainActivity : AppCompatActivity() { - private lateinit var windowInfoRep: WindowInfoRepository + private lateinit var windowState: WindowState private lateinit var appStateViewModel: AppStateViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowInfoRep = windowInfoRepository() appStateViewModel = ViewModelProvider(this).get(AppStateViewModel::class.java) setContent { + windowState = rememberWindowState() + ListDetailComposeSampleTheme { - SetupUI(appStateViewModel, windowInfoRep) + ListDetailApp(appStateViewModel, windowState) } } } diff --git a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/DetailView.kt b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/DetailView.kt index 3cad70f..539c4c4 100644 --- a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/DetailView.kt +++ b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/DetailView.kt @@ -44,13 +44,13 @@ private val verticalPadding = 35.dp private val horizontalPadding = 20.dp @Composable -fun DetailViewWithTopBar(isAppSpanned: Boolean, appStateViewModel: AppStateViewModel) { +fun DetailViewWithTopBar(isDualScreen: Boolean, appStateViewModel: AppStateViewModel) { Scaffold( topBar = { TopAppBar( title = { }, navigationIcon = { - if (!isAppSpanned) { + if (!isDualScreen) { DetailViewTopBar() } } diff --git a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/MainPage.kt b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/MainPage.kt index ddc3f6c..35d61bd 100644 --- a/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/MainPage.kt +++ b/ListDetail/src/main/java/com/microsoft/device/display/samples/listdetail/ui/view/MainPage.kt @@ -5,53 +5,21 @@ package com.microsoft.device.display.samples.listdetail.ui.view -import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource -import androidx.window.layout.WindowInfoRepository import com.microsoft.device.display.samples.listdetail.models.AppStateViewModel import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout import com.microsoft.device.dualscreen.twopanelayout.TwoPaneMode -import kotlinx.coroutines.flow.collect - -private lateinit var appStateViewModel: AppStateViewModel -const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585 +import com.microsoft.device.dualscreen.windowstate.WindowState @Composable -fun SetupUI(viewModel: AppStateViewModel, windowInfoRep: WindowInfoRepository) { - var isAppSpanned by remember { mutableStateOf(false) } - - LaunchedEffect(windowInfoRep) { - windowInfoRep.windowLayoutInfo - .collect { newLayoutInfo -> - val displayFeatures = newLayoutInfo.displayFeatures - isAppSpanned = displayFeatures.isNotEmpty() - } - } - - appStateViewModel = viewModel - - val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp - val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP - val isDualScreen = (isAppSpanned || isTablet) && !isPortrait - DualScreenUI(isDualScreen) -} - -@Composable -fun DualScreenUI(isDualScreen: Boolean) { +fun ListDetailApp(viewModel: AppStateViewModel, windowState: WindowState) { TwoPaneLayout( paneMode = TwoPaneMode.HorizontalSingle, - pane1 = { ListViewWithTopBar(appStateViewModel = appStateViewModel) }, - pane2 = { DetailViewWithTopBar(isAppSpanned = isDualScreen, appStateViewModel = appStateViewModel) } + pane1 = { ListViewWithTopBar(viewModel) }, + pane2 = { DetailViewWithTopBar(windowState.isDualPortrait(), viewModel) } ) } diff --git a/NavigationRail/build.gradle b/NavigationRail/build.gradle index 6b81585..967f2f1 100644 --- a/NavigationRail/build.gradle +++ b/NavigationRail/build.gradle @@ -41,9 +41,9 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation microsoftDependencies.twoPaneLayout + implementation project(':WindowState') implementation composeDependencies.composeMaterialForNavRail implementation composeDependencies.composeRuntime diff --git a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/MainActivity.kt b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/MainActivity.kt index 9ab9213..7168090 100644 --- a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/MainActivity.kt +++ b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/MainActivity.kt @@ -12,27 +12,26 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.ui.unit.ExperimentalUnitApi -import androidx.window.layout.WindowInfoRepository -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import com.microsoft.device.display.samples.navigationrail.ui.theme.ComposeSamplesTheme -import com.microsoft.device.display.samples.navigationrail.ui.view.SetupUI +import com.microsoft.device.display.samples.navigationrail.ui.view.NavigationRailApp +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState @ExperimentalAnimationApi @ExperimentalMaterialApi @ExperimentalFoundationApi @ExperimentalUnitApi class MainActivity : AppCompatActivity() { - private lateinit var windowInfoRep: WindowInfoRepository + private lateinit var windowState: WindowState override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowInfoRep = windowInfoRepository() - setContent { + windowState = rememberWindowState() + ComposeSamplesTheme { - // Set up app UI - SetupUI(windowInfoRep) + NavigationRailApp(windowState) } } } diff --git a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/components/ContentDrawer.kt b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/components/ContentDrawer.kt index b0b81da..45ea751 100644 --- a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/components/ContentDrawer.kt +++ b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/components/ContentDrawer.kt @@ -50,7 +50,7 @@ private enum class DrawerState { Collapsed, Expanded } * @param collapseHeight: height of the drawer when collpased (in dp) * @param hingeOccludes: optional param for foldable support, indicates whether there is a hinge * that occludes content in the current layout - * @param hingeSize: optional param for foldable support, indicates the size of a hinge + * @param foldSize: optional param for foldable support, indicates the size of a fold * @param hiddenContent: the content that will only be shown when the drawer is expanded * @param peekContent: the content that will be shown even when the drawer is collapsed */ @@ -61,7 +61,7 @@ fun ContentDrawer( expandHeight: Dp, collapseHeight: Dp, hingeOccludes: Boolean = false, - hingeSize: Dp = 0.dp, + foldSize: Dp = 0.dp, hiddenContent: @Composable ColumnScope.() -> Unit, peekContent: @Composable ColumnScope.() -> Unit, ) { @@ -81,11 +81,11 @@ fun ContentDrawer( // Check if a spacer needs to be included to render content around an occluding hinge val spacerHeight = if (hingeOccludes) { val isExpanding = swipeableState.progress.to == DrawerState.Expanded - val progressHeight = (hingeSize.value * swipeableState.progress.fraction).dp + val progressHeight = (foldSize.value * swipeableState.progress.fraction).dp if (isExpanding) progressHeight else - hingeSize - progressHeight + foldSize - progressHeight } else { 0.dp } diff --git a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/HomePage.kt b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/HomePage.kt index 246c157..42aa674 100644 --- a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/HomePage.kt +++ b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/HomePage.kt @@ -5,90 +5,38 @@ package com.microsoft.device.display.samples.navigationrail.ui.view -import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowInfoRepository import com.microsoft.device.display.samples.navigationrail.models.DataProvider import com.microsoft.device.display.samples.navigationrail.ui.components.ItemTopBar import com.microsoft.device.dualscreen.twopanelayout.TwoPaneLayout import com.microsoft.device.dualscreen.twopanelayout.TwoPaneMode import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1 import com.microsoft.device.dualscreen.twopanelayout.navigateToPane2 -import kotlinx.coroutines.flow.collect - -const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585 +import com.microsoft.device.dualscreen.windowstate.WindowState @ExperimentalAnimationApi @ExperimentalUnitApi @ExperimentalMaterialApi @ExperimentalFoundationApi @Composable -fun SetupUI(windowInfoRep: WindowInfoRepository) { - // Create variables to track foldable device layout information - var isAppSpanned by remember { mutableStateOf(false) } - var isHingeVertical by remember { mutableStateOf(false) } - var hingeSize by remember { mutableStateOf(0) } +fun NavigationRailApp(windowState: WindowState) { + // Extract window state information + val isDualScreen = windowState.isDualScreen() + val isDualPortrait = windowState.isDualPortrait() + val isDualLandscape = windowState.isDualLandscape() + val foldSize = windowState.foldSize.dp - LaunchedEffect(windowInfoRep) { - windowInfoRep.windowLayoutInfo - .collect { newLayoutInfo -> - val displayFeatures = newLayoutInfo.displayFeatures - isAppSpanned = displayFeatures.isNotEmpty() - if (isAppSpanned) { - val foldingFeature = displayFeatures.first() as FoldingFeature - isHingeVertical = - foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL - hingeSize = if (isHingeVertical) - foldingFeature.bounds.width() - else - foldingFeature.bounds.height() - } - } - } - - val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp - val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP - val isDualScreen = (isAppSpanned || isTablet) - val isDualPortrait = when { - isAppSpanned -> isHingeVertical - isTablet -> LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - else -> false - } - val isDualLandscape = when { - isAppSpanned -> !isHingeVertical - isTablet -> LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT - else -> false - } - - DualScreenUI(isDualScreen, isDualPortrait, isDualLandscape, hingeSize.dp) -} - -@ExperimentalAnimationApi -@ExperimentalUnitApi -@ExperimentalFoundationApi -@ExperimentalMaterialApi -@Composable -fun DualScreenUI( - isDualScreen: Boolean, - isDualPortrait: Boolean, - isDualLandscape: Boolean, - hingeSize: Dp, -) { // Set up starting route for navigation in pane 1 var currentRoute by rememberSaveable { mutableStateOf(navDestinations[0].route) } val updateRoute: (String) -> Unit = { newRoute -> currentRoute = newRoute } @@ -103,7 +51,7 @@ fun DualScreenUI( Pane1(isDualScreen, isDualPortrait, imageId, updateImageId, currentRoute, updateRoute) }, pane2 = { - Pane2(isDualPortrait, isDualLandscape, hingeSize, imageId, updateImageId, currentRoute) + Pane2(isDualPortrait, isDualLandscape, foldSize, imageId, updateImageId, currentRoute) }, ) @@ -137,7 +85,7 @@ fun Pane1( fun Pane2( isDualPortrait: Boolean, isDualLandscape: Boolean, - hingeSize: Dp, + foldSize: Dp, imageId: Int?, updateImageId: (Int?) -> Unit, currentRoute: String, @@ -152,7 +100,7 @@ fun Pane2( } BackHandler { if (!isDualPortrait) onBackPressed() } - ItemDetailView(isDualPortrait, isDualLandscape, hingeSize, selectedImage, currentRoute) + ItemDetailView(isDualPortrait, isDualLandscape, foldSize, selectedImage, currentRoute) // If only one pane is being displayed, show a "back" icon if (!isDualPortrait) { ItemTopBar( diff --git a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailView.kt b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailView.kt index ffbf140..bc4a1b9 100644 --- a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailView.kt +++ b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailView.kt @@ -26,7 +26,7 @@ import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1 * * @param isDualPortrait: true if device is in dual portrait mode * @param isDualLandscape: true if device is in dual landscape mode - * @param hingeSize: size of hinge in dp (0 if no hinge) + * @param foldSize: size of fold in dp (0 if no fold) * @param selectedImage: currently selected image * @param currentRoute: current route in gallery NavHost */ @@ -36,7 +36,7 @@ import com.microsoft.device.dualscreen.twopanelayout.navigateToPane1 fun ItemDetailView( isDualPortrait: Boolean, isDualLandscape: Boolean, - hingeSize: Dp, + foldSize: Dp, selectedImage: Image? = null, currentRoute: String, ) { @@ -61,7 +61,7 @@ fun ItemDetailView( modifier = Modifier.align(Alignment.BottomCenter), image = selectedImage, isDualLandscape = isDualLandscape, - hingeSize = hingeSize, + foldSize = foldSize, gallerySection = gallerySection, ) } diff --git a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailsDrawer.kt b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailsDrawer.kt index 534fbaf..2bcf4a4 100644 --- a/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailsDrawer.kt +++ b/NavigationRail/src/main/java/com/microsoft/device/display/samples/navigationrail/ui/view/ItemDetailsDrawer.kt @@ -60,7 +60,7 @@ fun BoxWithConstraintsScope.ItemDetailsDrawer( modifier: Modifier, image: Image, isDualLandscape: Boolean, - hingeSize: Dp, + foldSize: Dp, gallerySection: GallerySections?, ) { // Set max/min height for drawer based on orientation @@ -84,7 +84,7 @@ fun BoxWithConstraintsScope.ItemDetailsDrawer( expandHeight = expandedHeight, collapseHeight = collapsedHeight, hingeOccludes = isDualLandscape, - hingeSize = hingeSize, + foldSize = foldSize, hiddenContent = { ItemDetailsLong(image.details) } ) { DrawerPill() diff --git a/TestUtils/src/main/AndroidManifest.xml b/TestUtils/src/main/AndroidManifest.xml index 05bf12d..cf85b36 100644 --- a/TestUtils/src/main/AndroidManifest.xml +++ b/TestUtils/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/TwoPage/build.gradle b/TwoPage/build.gradle index 2e9c5e0..809d219 100644 --- a/TwoPage/build.gradle +++ b/TwoPage/build.gradle @@ -37,13 +37,13 @@ dependencies { implementation androidxDependencies.ktxCore implementation androidxDependencies.appCompat - implementation androidxDependencies.window implementation composeDependencies.composeUI implementation composeDependencies.composeRuntime implementation composeDependencies.composeMaterial implementation composeDependencies.composeUITooling implementation composeDependencies.activityCompose + implementation project(':WindowState') implementation googleDependencies.material } \ No newline at end of file diff --git a/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/MainActivity.kt b/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/MainActivity.kt index c45468d..ef1ec43 100644 --- a/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/MainActivity.kt +++ b/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/MainActivity.kt @@ -8,22 +8,22 @@ package com.microsoft.device.display.samples.twopage import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.window.layout.WindowInfoRepository -import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository -import com.microsoft.device.display.samples.twopage.ui.home.SetupUI +import com.microsoft.device.display.samples.twopage.ui.home.TwoPageApp import com.microsoft.device.display.samples.twopage.ui.theme.TwoPageComposeSamplesTheme +import com.microsoft.device.dualscreen.windowstate.WindowState +import com.microsoft.device.dualscreen.windowstate.rememberWindowState class MainActivity : AppCompatActivity() { - private lateinit var windowInfoRep: WindowInfoRepository + private lateinit var windowState: WindowState override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - windowInfoRep = windowInfoRepository() - setContent { + windowState = rememberWindowState() + TwoPageComposeSamplesTheme { - SetupUI(windowInfoRep) + TwoPageApp(windowState) } } } diff --git a/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/ui/home/HomePage.kt b/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/ui/home/HomePage.kt index 502a539..d9b4ca7 100644 --- a/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/ui/home/HomePage.kt +++ b/TwoPage/src/main/java/com/microsoft/device/display/samples/twopage/ui/home/HomePage.kt @@ -5,12 +5,10 @@ package com.microsoft.device.display.samples.twopage.ui.home -import android.content.res.Configuration import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -20,64 +18,36 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import androidx.window.layout.FoldingFeature -import androidx.window.layout.WindowInfoRepository import com.microsoft.device.display.samples.twopage.utils.PagerState import com.microsoft.device.display.samples.twopage.utils.ViewPager -import kotlinx.coroutines.flow.collect - -const val SMALLEST_TABLET_SCREEN_WIDTH_DP = 585 +import com.microsoft.device.dualscreen.windowstate.WindowState @Composable -fun SetupUI(windowInfoRep: WindowInfoRepository) { +fun TwoPageApp(windowState: WindowState) { val density = LocalDensity.current.density - var isAppSpanned by remember { mutableStateOf(false) } var viewWidth by remember { mutableStateOf(0) } - var hingeThickness by remember { mutableStateOf(0) } - var isHingeHorizontal by remember { mutableStateOf(false) } - LaunchedEffect(windowInfoRep) { - windowInfoRep.windowLayoutInfo - .collect { newLayoutInfo -> - val displayFeatures = newLayoutInfo.displayFeatures - isAppSpanned = displayFeatures.isNotEmpty() - if (isAppSpanned) { - val foldingFeature = displayFeatures.first() as FoldingFeature - val vWidth: Int + val vWidth = if (windowState.hasFold) { + when (windowState.isFoldHorizontal) { + true -> windowState.foldBounds.right + false -> windowState.foldBounds.left + } + } else 0 + viewWidth = if (windowState.isDualPortrait() && !windowState.hasFold) + LocalConfiguration.current.screenWidthDp / 2 + else + (vWidth / density).toInt() - if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) { - isHingeHorizontal = false - vWidth = foldingFeature.bounds.left - hingeThickness = foldingFeature.bounds.width() - } else { - isHingeHorizontal = true - vWidth = foldingFeature.bounds.right - hingeThickness = foldingFeature.bounds.height() - } - - viewWidth = (vWidth / density).toInt() - } - } - } - - val smallestScreenWidthDp = LocalConfiguration.current.smallestScreenWidthDp - val isTablet = smallestScreenWidthDp > SMALLEST_TABLET_SCREEN_WIDTH_DP - val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - val isTabletDualMode = isTablet && isLandscape - if (isTabletDualMode) { - viewWidth = LocalConfiguration.current.screenWidthDp / 2 - } - val isDualPortraitMode = isAppSpanned && !isHingeHorizontal - - val isDualScreen = (isDualPortraitMode) || isTabletDualMode + val isDualScreen = windowState.isDualPortrait() val pages = setupPages(viewWidth) - PageViews(pages, isDualScreen, hingeThickness / 2) + PageViews(pages, isDualScreen, windowState.foldSize / 2) } @Composable fun PageViews(pages: List<@Composable () -> Unit>, isDualScreen: Boolean, pagePadding: Int) { val maxPage = (pages.size - 1).coerceAtLeast(0) - val pagerState: PagerState = remember { PagerState(currentPage = 0, minPage = 0, maxPage = maxPage) } + val pagerState: PagerState = + remember { PagerState(currentPage = 0, minPage = 0, maxPage = maxPage) } pagerState.isDualMode = isDualScreen ViewPager( state = pagerState, @@ -94,17 +64,9 @@ fun setupPages(width: Int): List<@Composable () -> Unit> { .fillMaxHeight() .clipToBounds() else Modifier.fillMaxSize() return listOf<@Composable () -> Unit>( - { - FirstPage(modifier = modifier) - }, - { - SecondPage(modifier = modifier) - }, - { - ThirdPage(modifier = modifier) - }, - { - FourthPage(modifier = modifier) - } + { FirstPage(modifier = modifier) }, + { SecondPage(modifier = modifier) }, + { ThirdPage(modifier = modifier) }, + { FourthPage(modifier = modifier) } ) } diff --git a/WindowState/.gitignore b/WindowState/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/WindowState/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/WindowState/build.gradle b/WindowState/build.gradle new file mode 100644 index 0000000..422836f --- /dev/null +++ b/WindowState/build.gradle @@ -0,0 +1,57 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion rootProject.ext.compileSdkVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + testInstrumentationRunner rootProject.ext.config.testInstrumentationRunner + + // REVISIT: can uncomment if we release this +// versionCode 1 +// versionName "1.0" + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion composeVersion + } +} + +dependencies { + implementation kotlinDependencies.kotlinStdlib + + implementation androidxDependencies.ktxCore + implementation androidxDependencies.appCompat + implementation androidxDependencies.window + + implementation composeDependencies.composeUI + implementation composeDependencies.composeRuntime + implementation composeDependencies.composeMaterial + implementation composeDependencies.composeUITooling + implementation composeDependencies.activityCompose + + testImplementation testDependencies.androidJunit + testImplementation testDependencies.androidxTestCore + testImplementation testDependencies.mockitoCore + + androidTestImplementation testDependencies.androidxTestCore + androidTestImplementation testDependencies.androidxTestRules + androidTestImplementation testDependencies.androidxTestRunner + androidTestImplementation testDependencies.espressoCore +} \ No newline at end of file diff --git a/WindowState/src/androidTest/java/com/microsoft/device/dualscreen/windowstate/WindowStateTest.kt b/WindowState/src/androidTest/java/com/microsoft/device/dualscreen/windowstate/WindowStateTest.kt new file mode 100644 index 0000000..5059140 --- /dev/null +++ b/WindowState/src/androidTest/java/com/microsoft/device/dualscreen/windowstate/WindowStateTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import android.graphics.Rect +import org.junit.Assert.assertEquals +import org.junit.Test + +class WindowStateTest { + private val hasFold = true + private val foldBounds = Rect(20, 20, 60, 100) + private val foldState = FoldState.HALF_OPENED + private val foldSeparates = true + private val foldOccludes = true + private val widthSizeClass = WindowSizeClass.MEDIUM + private val heightSizeClass = WindowSizeClass.EXPANDED + + private val horizontalFold = WindowState( + hasFold, + true, + foldBounds, + foldState, + foldSeparates, + foldOccludes, + widthSizeClass, + heightSizeClass + ) + private val verticalFold = WindowState( + hasFold, + false, + foldBounds, + foldState, + foldSeparates, + foldOccludes, + widthSizeClass, + heightSizeClass + ) + private val noFoldLargeScreen = WindowState( + hasFold = false, + isFoldHorizontal = true, + foldBounds = Rect(), + foldState = FoldState.FLAT, + foldSeparates = false, + foldOccludes = false, + widthSizeClass = WindowSizeClass.EXPANDED, + heightSizeClass = WindowSizeClass.EXPANDED + ) + private val noFoldCompact = WindowState( + hasFold = false, + isFoldHorizontal = true, + foldBounds = Rect(), + foldState = FoldState.FLAT, + foldSeparates = false, + foldOccludes = false, + widthSizeClass = WindowSizeClass.COMPACT, + heightSizeClass = WindowSizeClass.MEDIUM + ) + + @Test + fun returns_correct_fold_size() { + assertEquals(foldBounds.height(), horizontalFold.foldSize) + assertEquals(foldBounds.width(), verticalFold.foldSize) + assertEquals(0, noFoldLargeScreen.foldSize) + } + + @Test + fun portrait_large_screen_returns_dual_land() { + assertEquals(WindowMode.DUAL_LANDSCAPE, noFoldLargeScreen.calculateWindowMode(true)) + } + + @Test + fun landscape_large_screen_returns_dual_port() { + assertEquals(WindowMode.DUAL_PORTRAIT, noFoldLargeScreen.calculateWindowMode(false)) + } + + @Test + fun portrait_compact_returns_single_port() { + assertEquals(WindowMode.SINGLE_PORTRAIT, noFoldCompact.calculateWindowMode(true)) + } + + @Test + fun landscape_compact_returns_single_land() { + assertEquals(WindowMode.SINGLE_LANDSCAPE, noFoldCompact.calculateWindowMode(false)) + } + + @Test + fun vertical_fold_returns_dual_port() { + assertEquals(WindowMode.DUAL_PORTRAIT, verticalFold.calculateWindowMode(true)) + assertEquals(WindowMode.DUAL_PORTRAIT, verticalFold.calculateWindowMode(false)) + } + + @Test + fun horizontal_fold_returns_dual_land() { + assertEquals(WindowMode.DUAL_LANDSCAPE, horizontalFold.calculateWindowMode(true)) + assertEquals(WindowMode.DUAL_LANDSCAPE, horizontalFold.calculateWindowMode(false)) + } +} diff --git a/WindowState/src/main/AndroidManifest.xml b/WindowState/src/main/AndroidManifest.xml new file mode 100644 index 0000000..09d9625 --- /dev/null +++ b/WindowState/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/Dimension.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/Dimension.kt new file mode 100644 index 0000000..314695b --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/Dimension.kt @@ -0,0 +1,8 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +enum class Dimension { WIDTH, HEIGHT } diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/FoldState.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/FoldState.kt new file mode 100644 index 0000000..9964c19 --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/FoldState.kt @@ -0,0 +1,8 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +enum class FoldState { FLAT, HALF_OPENED } diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowMode.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowMode.kt new file mode 100644 index 0000000..b23a1c9 --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowMode.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +/** + * Class that represents the different modes in which content can be displayed in a window, + * depending on window size and orientation + * + * Window modes: + * - single portrait + * - single landscape + * - dual portrait + * - dual landscape + */ +enum class WindowMode { + SINGLE_PORTRAIT, + SINGLE_LANDSCAPE, + DUAL_PORTRAIT, + DUAL_LANDSCAPE; + + val isDualScreen: Boolean get() = this == DUAL_PORTRAIT || this == DUAL_LANDSCAPE +} diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClass.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClass.kt new file mode 100644 index 0000000..c5fc19d --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClass.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED } + +/** + * Calculates size class for a given dimension + * + * @param dimenDp: size of dimension in Dp + * @param dimen: which dimension is being measured (width or height) + */ +fun getWindowSizeClass(dimenDp: Dp, dimen: Dimension = Dimension.WIDTH): WindowSizeClass = + when (dimen) { + Dimension.WIDTH -> getSizeClass(dimenDp, 600.dp, 840.dp) + Dimension.HEIGHT -> getSizeClass(dimenDp, 480.dp, 900.dp) + } + +private fun getSizeClass(size: Dp, medium: Dp, expanded: Dp): WindowSizeClass = when { + size < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative") + size < medium -> WindowSizeClass.COMPACT + size < expanded -> WindowSizeClass.MEDIUM + else -> WindowSizeClass.EXPANDED +} diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowState.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowState.kt new file mode 100644 index 0000000..01d53d2 --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowState.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import android.content.res.Configuration +import android.graphics.Rect +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration + +/** + * Data class that contains foldable and large screen information extracted from the Jetpack + * Window Manager library + * + * @param hasFold: true if window contains a FoldingFeature, + * @param isFoldHorizontal: true if window contains a FoldingFeature with a horizontal orientation + * @param foldBounds: Rect object that describes the bound of the FoldingFeature + * @param foldState: state of the fold, based on state property of FoldingFeature + * @param foldSeparates: based on isSeparating property of FoldingFeature + * @param foldOccludes: true if FoldingFeature occlusion type is full + * @param widthSizeClass: size class (compact, medium, or expanded) for window width + * @param heightSizeClass: size class (compact, medium, or expanded) for window height + */ +data class WindowState( + val hasFold: Boolean, + val isFoldHorizontal: Boolean, + val foldBounds: Rect, + val foldState: FoldState, + val foldSeparates: Boolean, + val foldOccludes: Boolean, + val widthSizeClass: WindowSizeClass, + val heightSizeClass: WindowSizeClass, +) { + private val foldableFoldSize = when (isFoldHorizontal) { + true -> foldBounds.height() + false -> foldBounds.width() + } + + val foldSize = if (hasFold) foldableFoldSize else 0 + + val windowMode: WindowMode + @Composable get() { + // REVISIT: should width/height ratio be used instead of orientation? + val isPortrait = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT + + return calculateWindowMode(isPortrait) + } + + @VisibleForTesting + fun calculateWindowMode(isPortrait: Boolean): WindowMode { + // REVISIT: should height class also be considered? + // Also, right now we are considering large screens + foldables mutually exclusive + // (which seems necessary for dualscreen apps), but we may want to think about this + // more and change our approach if we think there are cases where we want an app to + // know about both properties + val isLargeScreen = !hasFold && widthSizeClass == WindowSizeClass.EXPANDED + + return when { + hasFold -> { + if (isFoldHorizontal) + WindowMode.DUAL_LANDSCAPE + else + WindowMode.DUAL_PORTRAIT + } + isLargeScreen -> { + if (isPortrait) + WindowMode.DUAL_LANDSCAPE + else + WindowMode.DUAL_PORTRAIT + } + isPortrait -> WindowMode.SINGLE_PORTRAIT + else -> WindowMode.SINGLE_LANDSCAPE + } + } + + @Composable + fun isDualScreen(): Boolean { + return windowMode.isDualScreen + } + + @Composable + fun isDualPortrait(): Boolean { + return windowMode == WindowMode.DUAL_PORTRAIT + } + + @Composable + fun isDualLandscape(): Boolean { + return windowMode == WindowMode.DUAL_LANDSCAPE + } + + @Composable + fun isSinglePortrait(): Boolean { + return windowMode == WindowMode.SINGLE_PORTRAIT + } + + @Composable + fun isSingleLandscape(): Boolean { + return windowMode == WindowMode.SINGLE_LANDSCAPE + } +} diff --git a/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowStateHelper.kt b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowStateHelper.kt new file mode 100644 index 0000000..eb1190f --- /dev/null +++ b/WindowState/src/main/java/com/microsoft/device/dualscreen/windowstate/WindowStateHelper.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import android.app.Activity +import android.graphics.Rect +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.window.layout.FoldingFeature +import androidx.window.layout.WindowInfoTracker +import androidx.window.layout.WindowMetricsCalculator +import kotlinx.coroutines.flow.collect + +@Composable +fun Activity.rememberWindowState(): WindowState { + val activity = this + val windowInfoRepo = WindowInfoTracker.getOrCreate(activity) + + var hasFold by remember { mutableStateOf(false) } + var isFoldHorizontal by remember { mutableStateOf(false) } + var foldBounds by remember { mutableStateOf(Rect()) } + var foldState by remember { mutableStateOf(FoldState.FLAT) } + var foldSeparates by remember { mutableStateOf(false) } + var foldOccludes by remember { mutableStateOf(false) } + + LaunchedEffect(windowInfoRepo) { + windowInfoRepo.windowLayoutInfo(activity).collect { newLayoutInfo -> + hasFold = newLayoutInfo.displayFeatures.isNotEmpty() + if (hasFold) { + val fold = newLayoutInfo.displayFeatures.firstOrNull() as? FoldingFeature + fold?.let { + isFoldHorizontal = it.orientation == FoldingFeature.Orientation.HORIZONTAL + foldBounds = it.bounds + foldState = when (it.state) { + FoldingFeature.State.HALF_OPENED -> FoldState.HALF_OPENED + else -> FoldState.FLAT + } + foldSeparates = it.isSeparating + foldOccludes = it.occlusionType == FoldingFeature.OcclusionType.FULL + } + } + } + } + + val config = LocalConfiguration.current + val windowMetrics = remember(config) { + WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this).bounds + } + + val windowWidth = with(LocalDensity.current) { windowMetrics.width().toDp() } + val windowHeight = with(LocalDensity.current) { windowMetrics.height().toDp() } + val widthSizeClass = getWindowSizeClass(windowWidth) + val heightSizeClass = getWindowSizeClass(windowHeight, Dimension.HEIGHT) + + return WindowState( + hasFold, + isFoldHorizontal, + foldBounds, + foldState, + foldSeparates, + foldOccludes, + widthSizeClass, + heightSizeClass + ) +} diff --git a/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowModeTest.kt b/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowModeTest.kt new file mode 100644 index 0000000..56e8158 --- /dev/null +++ b/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowModeTest.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import org.junit.Assert.assertFalse +import org.junit.Test + +class WindowModeTest { + @Test + fun single_modes_are_not_dualscreen() { + assertFalse(WindowMode.SINGLE_LANDSCAPE.isDualScreen) + assertFalse(WindowMode.SINGLE_PORTRAIT.isDualScreen) + } + + @Test + fun dual_modes_are_dualscreen() { + assert(WindowMode.DUAL_LANDSCAPE.isDualScreen) + assert(WindowMode.DUAL_PORTRAIT.isDualScreen) + } +} diff --git a/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClassTest.kt b/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClassTest.kt new file mode 100644 index 0000000..f29f8d9 --- /dev/null +++ b/WindowState/src/test/java/com/microsoft/device/dualscreen/windowstate/WindowSizeClassTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +package com.microsoft.device.dualscreen.windowstate + +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertEquals +import org.junit.Test + +class WindowSizeClassTest { + @Test + fun width_returns_compact() { + assertEquals(WindowSizeClass.COMPACT, getWindowSizeClass(500.dp, Dimension.WIDTH)) + } + + @Test + fun width_returns_medium() { + assertEquals(WindowSizeClass.MEDIUM, getWindowSizeClass(700.dp, Dimension.WIDTH)) + } + + @Test + fun width_returns_expanded() { + assertEquals(WindowSizeClass.EXPANDED, getWindowSizeClass(900.dp, Dimension.WIDTH)) + } + + @Test + fun height_returns_compact() { + assertEquals(WindowSizeClass.COMPACT, getWindowSizeClass(300.dp, Dimension.HEIGHT)) + } + + @Test + fun height_returns_medium() { + assertEquals(WindowSizeClass.MEDIUM, getWindowSizeClass(700.dp, Dimension.HEIGHT)) + } + + @Test + fun height_returns_expanded() { + assertEquals(WindowSizeClass.EXPANDED, getWindowSizeClass(1000.dp, Dimension.HEIGHT)) + } +} diff --git a/dependencies.gradle b/dependencies.gradle index 00e6c7a..905ba3b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -4,7 +4,7 @@ */ ext { - gradlePluginVersion = '7.0.3' + gradlePluginVersion = '7.0.4' kotlinVersion = "1.5.31" compileSdkVersion = 31 targetSdkVersion = compileSdkVersion @@ -23,7 +23,7 @@ ext { // AndroidX versions appCompatVersion = "1.3.0" ktxCoreVersion = "1.5.0" - windowVersion = "1.0.0-beta03" + windowVersion = "1.0.0-beta04" androidxDependencies = [ appCompat : "androidx.appcompat:appcompat:$appCompatVersion", ktxCore : "androidx.core:core-ktx:$ktxCoreVersion", @@ -49,6 +49,8 @@ ext { androidxTestVersion = "1.4.0" uiAutomatorVersion = "2.2.0" espressoVersion = "3.4.0" + junitVersion = "4.12" + mockitoVersion = "4.1.0" testDependencies = [ androidxTestCore : "androidx.test:core:$androidxTestVersion", androidxTestRules : "androidx.test:rules:$androidxTestVersion", @@ -59,6 +61,8 @@ ext { uiAutomator : "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion", windowTest : "androidx.window:window-testing:$windowVersion", espressoCore : "androidx.test.espresso:espresso-core:$espressoVersion", + androidJunit : "junit:junit:$junitVersion", + mockitoCore : "org.mockito:mockito-core:$mockitoVersion", ] // Google dependencies @@ -68,7 +72,7 @@ ext { ] // Microsoft dependencies - twoPaneLayoutVersion = "1.0.0-alpha09" + twoPaneLayoutVersion = "1.0.0-alpha10" microsoftDependencies = [ twoPaneLayout: "com.microsoft.device.dualscreen:twopanelayout:$twoPaneLayoutVersion", ] diff --git a/settings.gradle b/settings.gradle index 43bdcb6..75a320a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,4 @@ */ rootProject.name = "ComposeSamples" -include ':ListDetail', ':CompanionPane', ':DualView', ':ExtendedCanvas', ':ComposeGallery', ':TwoPage', ':NavigationRail', ':TestUtils' +include ':ListDetail', ':CompanionPane', ':DualView', ':ExtendedCanvas', ':ComposeGallery', ':TwoPage', ':NavigationRail', ':TestUtils', ':WindowState'