surface-duo-compose-sdk/TwoPaneLayout
Kristen Halper 17769ca638
Remove usage of private backQueue field (#64)
* Remove backQueue usage

* Bump release version

* Update dependencies

* Revert if statement logic
2023-06-13 16:10:50 -07:00
..
gradle/wrapper Add argument support for TwoPaneLayoutNav destinations (#60) 2023-05-23 15:19:38 -07:00
library Remove usage of private backQueue field (#64) 2023-06-13 16:10:50 -07:00
nav_sample Add backstack support to TwoPaneLayoutNav (#50) 2022-11-15 10:44:48 -08:00
sample Update dependencies and add SinglePane mode to TwoPaneLayout (#49) 2022-11-02 13:45:44 -07:00
screenshots Add callbacks for pane mode switches and test scope instances (#34) 2022-05-25 16:54:03 -04:00
.gitignore Copy in TwoPaneLayout project (#1) 2021-11-10 15:00:48 -05:00
README.md Remove usage of private backQueue field (#64) 2023-06-13 16:10:50 -07:00
build.gradle Add dokka to TwoPaneLayout (#6) 2021-12-27 09:17:04 -05:00
dependencies.gradle Remove usage of private backQueue field (#64) 2023-06-13 16:10:50 -07:00
gradle.properties Add argument support for TwoPaneLayoutNav destinations (#60) 2023-05-23 15:19:38 -07:00
gradlew Copy in TwoPaneLayout project (#1) 2021-11-10 15:00:48 -05:00
gradlew.bat Copy in TwoPaneLayout project (#1) 2021-11-10 15:00:48 -05:00
ktlint.gradle Update TwoPaneLayout to use WindowState (#13) 2022-02-09 16:43:02 -05:00
publishing.gradle Add navController parameter to constructor (#22) 2022-03-17 16:26:42 -04:00
settings.gradle Add TwoPaneLayoutNav and update TwoPaneScope (#33) 2022-05-17 14:45:40 -04:00

README.md

TwoPaneLayout - Surface Duo Compose SDK

TwoPaneLayout is a Jetpack Compose component that helps you create UI for dual-screen, foldable, and large-screen devices. TwoPaneLayout provides a two-pane layout for use at the top level of a UI. The component will place two panes side-by-side when the app is spanned on dual-screen, foldable and large-screen devices, otherwise only one pane will be shown. These panes can be horizontal or vertical, depending on the orientation of the device and the selected paneMode. TwoPaneLayoutNav is a new version of TwoPaneLayout that can be used for complex navigation scenarios, and it follows the same pane logic as the original TwoPaneLayout.

When the app is spanned across a separating vertical hinge or fold, or when the width is larger than height of the screen on a large-screen device, pane 1 will be placed on the left, while pane 2 will be on the right. If the device rotates, the app is spanned across a separating horizontal hinge or fold, or the width is smaller than the height of screen on large-screen device, pane 1 will be placed on the top and pane 2 will be on the bottom.

Add to your project

  1. Make sure you have mavenCentral() repository in your top level build.gradle file:

    allprojects {
        repositories {
            google()
            mavenCentral()
         }
    }
    
  2. Add dependencies to the module-level build.gradle file (current version may be different from what's shown here).

    implementation "com.microsoft.device.dualscreen:twopanelayout:1.0.1-alpha08"
    
  3. Also ensure the compileSdkVersion is set to API 33 and the targetSdkVersion is set to API 32 or newer in the module-level build.gradle file.

    android { 
        compileSdkVersion 33
    
        defaultConfig { 
            targetSdkVersion 32
        } 
        ... 
    }
    
  4. Build layout with TwoPaneLayout or TwoPaneLayoutNav. Please refer to the TwoPaneLayout sample and TwoPaneLayoutNav sample for more details.

API reference

The sections below describe how to use and customize TwoPaneLayout for different scenarios. Please refer to the TwoPaneLayout documentation for more details.

To learn more about common use cases for two-pane layouts, please check out the dual-screen user interface patterns.

TwoPaneLayout

The main TwoPaneLayout constructors both accept parameters for a modifier, pane mode, and content to show in pane 1 and pane 2. The second constructor also accepts a NavHostController parameter, which is useful for accessing navigation information in your app.

@Composable
fun TwoPaneLayout(
    modifier: Modifier = Modifier,
    paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
    pane1: @Composable TwoPaneScope.() -> Unit,
    pane2: @Composable TwoPaneScope.() -> Unit
)

@Composable
fun TwoPaneLayout(
    modifier: Modifier = Modifier,
    paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
    navController: NavHostController,
    pane1: @Composable TwoPaneScope.() -> Unit,
    pane2: @Composable TwoPaneScope.() -> Unit
)

The content shown in each pane can access methods from the TwoPaneScope interface:

interface TwoPaneScope {

    fun Modifier.weight(weight: Float): Modifier

    fun navigateToPane1()

    fun navigateToPane2()

    val currentSinglePaneDestination: String

    val isSinglePane: Boolean
}

The weight modifier is described in more detail below.

When writing UI tests for composables that use TwoPaneScope, you can use the TwoPaneScopeTest class. It provides empty implementations of TwoPaneScope methods, and you can set the values of currentSinglePaneDestination and isSinglePane in the class constructor before running your tests.

class TwoPaneScopeTest(
    currentSinglePaneDestination: String = "",
    isSinglePane: Boolean = true
) : TwoPaneScope

Pane mode

The pane mode affects when two panes are shown for TwoPaneLayout. By default, whenever there is a separating fold or a large window, two panes will be shown, but you can choose to show only one pane in these cases by changing the pane mode.

A separating fold means there's a FoldingFeature present that returns true for the isSeparating property.

A large window is one with a width WindowSizeClass of EXPANDED and a height size classs of at least MEDIUM.

enum class TwoPaneMode {
    TwoPane,
    HorizontalSingle,
    VerticalSingle,
    SinglePane
}

There are four pane modes available for TwoPaneLayout:

  • TwoPane - default mode, always shows two panes when there is a separating fold or large window, regardless of the orientation

  • HorizontalSingle - shows one big pane when there is a horizontal separating fold or a portrait large window (combines top/bottom panes)

    HorizontalSingle pane mode on a dual-screen device
  • VerticalSingle - shows one big pane when there is a vertical separating fold or a landscape large window (combines left/right panes)

    VerticalSingle pane mode on a foldable device
  • SinglePane - always shows one pane, regardless of window features and orientation

This table explains when one 🟩 or two 🟦🟦 panes will be shown for different pane modes and device configurations:

Pane mode Small window without separating fold Portrait large window / horizontal separating fold Landscape large window / vertical separating fold
TwoPane 🟩 🟦🟦 🟦🟦
HorizontalSingle 🟩 🟩 🟦🟦
VerticalSingle 🟩 🟦🟦 🟩
SinglePane 🟩 🟩 🟩

Weight modifier

TwoPaneLayout is able to assign children widths or heights according to their weights provided using the TwoPaneScope.weight and TwoPaneNavScope.weight modifiers. This only affects the layout for large screen and foldable devices, but for single-screen devices, there will still only be one pane visible, regardless of the weight.

fun Modifier.weight(weight: Float): Modifier

For large screens:

  • No weight → two panes displayed equally

  • Weight → layout split up proportionally according to the weight

    TwoPaneLayout on a tablet/large screen device, weight modifiers of 0.3 and 0.7 provided so panes are divided in a 3:7 ratio

For foldables:

  • Separating fold → layout split up according to fold boundaries (with or without weight)

    TwoPaneLayout on a dual-screen device (Surface Duo), no weight modifier provided so panes are divided by fold TwoPaneLayout on a foldable device, no weight modifier provided so panes are divided by fold
  • Non-separating fold → device treated as a large screen or single-screen depending on its size

TwoPaneLayoutNav

The TwoPaneLayoutNav constructor can be used for more complicated navigation scenarios. It accepts parameters for a modifier, pane mode, NavHostController, navigation graph, and start destinations.

@Composable
fun TwoPaneLayoutNav(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    paneMode: TwoPaneMode = TwoPaneMode.TwoPane,
    singlePaneStartDestination: String,
    pane1StartDestination: String,
    pane2StartDestination: String,
    builder: NavGraphBuilder.() -> Unit
)

The content shown in each destination can access methods from the TwoPaneNavScope interface:

interface TwoPaneNavScope {

    fun Modifier.weight(weight: Float): Modifier

    fun NavHostController.navigateTo(
        route: String,
        launchScreen: Screen,
        builder: NavOptionsBuilder.() -> Unit = { }
    )

    fun NavHostController.navigateBack(): Boolean

    fun NavHostController.navigateUpTo(
        route: String,
        inclusive: Boolean = false
    ): Boolean

    val twoPaneBackStack: List<TwoPaneBackStackEntry>

    val currentSinglePaneDestination: String

    val currentPane1Destination: String

    val currentPane2Destination: String

    val isSinglePane: Boolean
}

The navigateTo method is an enhanced version of the navigate method from NavHostController that works when one or two panes are shown. The launchScreen parameter determines which pane, or screen, a destination should be shown in when in two pane mode: Screen.Pane1 or Screen.Pane2.

sealed class Screen(val route: String) {

    object Pane1 : Screen("pane1")

    object Pane2 : Screen("pane2")
}

The navigateBack method is an enhanced version of the navigateUp method from NavHostController that also works when one or two panes are shown. If navigation is successful, the function will return true, otherwise it will return false.

The navigateUpTo method calls navigateBack until you reach your desired destination, allowing you to pop several destinations off the backstack at once. Note that, as expected when calling navigateBack in dual-screen mode, if there are fewer than two destinations left in the backstack, the activity will be finished. So, if your stack is [A, B, C] in dual-screen mode and you call navigateUpTo("A"), then the activity will be finished.

TwoPaneLayoutNav manages an internal backstack to maintain state across configuration changes. To access and update the backstack, use the twoPaneBackStack mutable list field.

If you want to override the default back press behavior and write a custom handler, make sure you call navigateBack to maintain the backstack correctly.

When writing UI tests for composables that use TwoPaneNavScope, you can use the TwoPaneScopeNavTest class. It provides empty implementations of TwoPaneNavScope methods, and you can set the values of currentSinglePaneDestination, currentPane1Destination, currentPane2Destination, and isSinglePane in the class constructor before running your tests.

class TwoPaneNavScopeTest(
    currentSinglePaneDestination: String = "",
    currentPane1Destination: String = "",
    currentPane2Destination: String = "",
    isSinglePane: Boolean = true
) : TwoPaneNavScope

The animation below shows an example of how to use TwoPaneLayoutNav to create custom navigation flows. The layout was created with four app destinations, where destination 1 was passed in as the singlePaneStartDestination and pane1StartDestination and destination two was passed in as the pane2StartDestination. The navigation flow, which works in both single and two pane modes, uses navigateTo to go from destination 1-4 in panes 1, 2, 2, and 1 respectively.

Example usage of the TwoPaneLayout nav in the nav sample

Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

License

Copyright (c) Microsoft Corporation.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.