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
This commit is contained in:
Родитель
c36a3bcb4c
Коммит
d4cb092e21
|
@ -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
|
||||
}
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,6 @@ dependencies {
|
|||
|
||||
implementation androidxDependencies.ktxCore
|
||||
implementation androidxDependencies.appCompat
|
||||
implementation androidxDependencies.window
|
||||
|
||||
implementation composeDependencies.composeUI
|
||||
implementation composeDependencies.composeRuntime
|
||||
|
|
|
@ -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')
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
-->
|
||||
|
||||
<manifest package="com.microsoft.device.dualscreen.testutils"></manifest>
|
||||
<manifest package="com.microsoft.device.dualscreen.testutils" />
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
~ Licensed under the MIT License.
|
||||
-->
|
||||
|
||||
<manifest package="com.microsoft.device.dualscreen.windowstate" />
|
|
@ -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 }
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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'
|
||||
|
|
Загрузка…
Ссылка в новой задаче