Composant TwoPaneLayout Jetpack Compose pour les appareils pliables

Important

Les fonctionnalités et l’aide décrites dans cet article sont en préversion publique et peuvent faire l’objet de modifications importantes avant leur lancement en disponibilité générale. Microsoft ne donne aucune garantie, expresse ou implicite, concernant les informations fournies ici.

TwoPaneLayout est un composant Jetpack Compose qui vous aide à créer une interface utilisateur pour des appareils double écran, pliables et grand écran. TwoPaneLayout fournit une disposition à deux volets à utiliser au niveau supérieur d’une IU. Le composant place deux volets côte à côte quand l’application s’étend sur des appareils double écran, pliables et grand écran, sinon un seul volet s’affiche. Ces volets peuvent être horizontaux ou verticaux, en fonction de l’orientation de l’appareil et du paneMode sélectionné.

Notes

TwoPaneLayout considère qu’un appareil est à écran large quand la catégorie de taille de fenêtre est étendue, à savoir au-delà de 840 dp.

Quand l’application s’étend sur une charnière ou pliure verticale, ou quand la largeur de l’écran est supérieure à la hauteur sur un appareil grand écran, le volet 1 est placé à gauche et le volet 2 à droite. Si l’appareil pivote, que l’application s’étend sur une charnière ou pliure horizontale, ou quand la largeur de l’écran est inférieure à la hauteur sur un appareil grand écran, le volet 1 est placé en haut et le volet 2 en bas.

Ajouter une dépendance

  1. Vérifiez que vous disposez du référentiel mavenCentral() dans votre fichier build.gradle de premier niveau :

    allprojects {
        repositories {
            google()
            mavenCentral()
         }
    }
    
  2. Ajoutez des dépendances au fichier build.gradle au niveau module (la version actuelle peut être différente de ce qui est montré ici) :

    implementation "com.microsoft.device.dualscreen:twopanelayout:1.0.1-alpha05"
    
  3. Vérifiez également que est défini sur compileSdkVersion API 33 et que est targetSdkVersion défini sur API 32 ou version ultérieure dans le fichier build.gradle au niveau du module :

    android { 
        compileSdkVersion 33
    
        defaultConfig { 
            targetSdkVersion 32
        } 
        ... 
    }
    
  4. Générez la disposition avec TwoPaneLayout ou TwoPaneLayoutNav.

    Pour plus de détails, consultez l’exemple de TwoPaneLayout et l’exemple de TwoPaneLayoutNav.

Utiliser TwoPaneLayout dans votre projet

Plusieurs concepts importants sont à prendre en compte quand vous utilisez TwoPaneLayout dans vos projets :

  • Constructeurs TwoPaneLayout

    Selon votre application, vous pouvez choisir entre trois constructeurs TwoPaneLayout différents au niveau supérieur de votre projet : TwoPaneLayout de base, TwoPaneLayout avec navController et TwoPaneLayoutNav.

  • Personnaliser votre disposition

    TwoPaneLayout offre deux façons de personnaliser l’affichage des volets : pondération et mode volet.

  • Naviguer avec TwoPaneLayout

    TwoPaneLayout propose également des méthodes de navigation internes qui peuvent contrôler le contenu affiché dans chaque volet. Selon le constructeur utilisé, vous aurez accès aux TwoPaneScope méthodes ou TwoPaneNavScope .

  • Tester les composables TwoPaneLayout

    Pour aider à tester les composables qui utilisent TwoPaneScope ou TwoPaneNavScope, TwoPaneLayout propose des implémentations de test des deux étendues à utiliser dans les tests d’interface utilisateur.

Constructeurs TwoPaneLayout

TwoPaneLayout doit toujours être le composable de niveau supérieur dans votre application, pour que la taille du volet soit calculée correctement. Trois constructeurs TwoPaneLayout différents sont disponibles et correspondent à différents scénarios. Pour plus d’informations de référence sur l’API, consultez le fichier README.md TwoPaneLayout.

TwoPaneLayout de base

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

Le constructeur TwoPaneLayout de base doit être utilisé quand vous n’avez pas besoin de plus de deux écrans de contenu. Dans les composables pane1 et pane2, vous pouvez accéder aux champs et méthodes fournis par l’interface TwoPaneScope.

Exemple d'utilisation :

TwoPaneLayout(
    pane1 = { Pane1Content() },
    pane2 = { Pane2Content() }
)

TwoPaneLayout avec navController

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

Le constructeur TwoPaneLayout avec navController doit être utilisé quand :

  • vous n’avez pas besoin d’afficher plus de deux écrans de contenu
  • vous avez besoin d’accéder aux informations de navigation dans votre application

Dans les composables pane1 et pane2, vous pouvez accéder aux champs et méthodes fournis par l’interface TwoPaneScope.

Exemple d'utilisation :

val navController = rememberNavController()

TwoPaneLayout(
    navController = navController,
    pane1 = { Pane1Content() },
    pane2 = { Pane2Content() }
)

TwoPaneLayoutNav

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

Le constructeur TwoPaneLayoutNav doit être utilisé quand vous voulez afficher plus de deux écrans de contenu et que vous avez besoin d’une prise en charge de navigation personnalisable. Dans chaque composable de destination, vous pouvez accéder aux champs et méthodes fournis par l’interface TwoPaneNavScope.

Exemple d'utilisation :

val navController = rememberNavController()

TwoPaneLayoutNav(
    navController = navController,
    singlePaneStartDestination = "A",
    pane1StartDestination = "A",
    pane2StartDestination = "B"
) {
    composable("A") {
        ContentA()
    }
    composable("B") {
        ContentB()
    }
    composable("C") {
        ContentC()
    }
}

Personnaliser votre disposition

Il existe deux façons de personnaliser TwoPaneLayout :

  • weight : détermine comment disposer les deux volets de manière proportionnelle
  • paneMode : détermine s’il faut montrer un ou deux volets en mode double écran horizontalement et verticalement

Poids

TwoPaneLayout peut attribuer des largeurs ou des hauteurs enfants en fonction de la pondération fournie par les modificateurs TwoPaneScope.weight et TwoPaneNavScope.weight.

Exemple d'utilisation :

TwoPaneLayout(
    pane1 = { Pane1Content(modifier = Modifier.weight(.3f)) },
    pane2 = { Pane2Content(modifier = Modifier.weight(.7f)) }
)

La pondération affecte la disposition différemment sur ces divers appareils :

  • grands écrans
  • pliables

Grands écrans

Quand aucune pondération n’est fournie, les deux volets sont divisés de manière égale.

Quand la pondération est fournie, la disposition est divisée proportionnellement en fonction du rapport de pondération.

Par exemple, cette capture d’écran montre TwoPaneLayout sur une tablette avec un ratio de pondération de 3:7 :

TwoPaneLayout sur une tablette/un appareil avec un grand écran avec les modificateurs de pondération 0,3 et 0,7 pour diviser les volets selon un rapport 3:7

Pliables

Quand une pliure de séparation est présente, la disposition est divisée en fonction des limites de la pliure, que la pondération soit fournie ou non.

Si la pliure ne marque pas de séparation, l’appareil est traité comme un grand écran ou un seul écran, en fonction de sa taille.

Par exemple, cette image montre la disposition TwoPaneLayout sur un appareil double écran, qui a une pliure de séparation :

TwoPaneLayout sur un appareil à double écran (Surface Duo) - quelle que soit la pondération, les volets sont divisés en fonction des limites de la pliure

Mode volet

Le mode de volet affecte lorsque deux volets sont affichés pour TwoPaneLayout. Par défaut, chaque fois qu’il existe un pli séparant ou une grande fenêtre, deux volets s’affichent, mais vous pouvez choisir d’afficher un seul volet dans ces cas en modifiant le mode de volet.

Un pli de séparation signifie qu’il existe une propriété FoldingFeature qui retourne true pour la propriété isSeparating .

Une grande fenêtre est une fenêtre avec une largeur WindowSizeClass de et des classes de EXPANDED taille de hauteur d’au moins MEDIUM.

Exemple d’utilisation :

TwoPaneLayout(
    paneMode = TwoPaneMode.HorizontalSingle,
    pane1 = { Pane1Content() },
    pane2 = { Pane2Content() }
)

Il existe quatre valeurs possibles paneMode :

  • TwoPane
  • HorizontalSingle
  • VerticalSingle
  • SinglePane

TwoPane

TwoPane est le mode de volet par défaut, et il affiche toujours deux volets lorsqu’il existe un pli séparant ou une grande fenêtre, quelle que soit l’orientation

Mode volet TwoPane sur un appareil pliable

HorizontalSingle

HorizontalSingle affiche un grand volet lorsqu’il existe un pli de séparation horizontal ou une grande fenêtre portrait (combine les volets supérieur/inférieur).

Mode volet HorizontalSingle sur un appareil à double écran

VerticalSingle

VerticalSingle affiche un grand volet lorsqu’il existe un pli de séparation vertical ou une grande fenêtre paysage (combine des volets gauche/droit).

Mode volet VerticalSingle sur un appareil pliable

SinglePane

SinglePane affiche toujours un volet, quelles que soient les caractéristiques et l’orientation de la fenêtre.

Tableau de comportement en mode volet

Pour résumer, ce tableau explique quand un 🟩 ou deux 🟦🟦 volets seront affichés pour différents modes de volet et configurations d’appareil :

Mode volet Petite fenêtre sans pli séparant Fenêtre grand portrait / pli de séparation horizontal Grande fenêtre paysage / pli de séparation verticale
TwoPane 🟩 🟦🟦 🟦🟦
HorizontalSingle 🟩 🟩 🟦🟦
VerticalSingle 🟩 🟦🟦 🟩
SinglePane 🟩 🟩 🟩

TwoPaneLayout fournit deux interfaces avec des options de navigation interne. Selon le constructeur que vous utilisez, vous avez accès à différents champs et méthodes.

TwoPaneScope

interface TwoPaneScope {
    ...

    fun navigateToPane1()

    fun navigateToPane2()

    val currentSinglePaneDestination: String

    ...
}

Avec TwoPaneScope, vous pouvez naviguer entre les volets 1 et 2 en mode mono-volet.

Animation montrant les méthodes navigateToPane1 et navigateToPane2 en action

Vous pouvez également accéder à la route de la destination du seul volet actuel, qui est Screen.Pane1.route ou Screen.Pane2.route.

Exemple d'utilisation :

TwoPaneLayout(
        pane1 = { Pane1Content(modifier = Modifier.clickable { navigateToPane2() }) },
        pane2 = { Pane2Content(modifier = Modifier.clickable { navigateToPane1() }) }
)

TwoPaneNavScope

interface TwoPaneNavScope {
    ...

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

    fun NavHostController.navigateBack(): Boolean
   
    val twoPaneBackStack: MutableList<TwoPaneBackStackEntry>

    val currentSinglePaneDestination: String

    val currentPane1Destination: String

    val currentPane2Destination: String

    val isSinglePane: Boolean

    ...
}

Avec TwoPaneNavScope, vous pouvez accéder à différentes destinations en mode mono-volet ou bi-volet.

Animation montrant comment TwoPaneLayoutNav peut être utilisé pour naviguer entre plus de deux destinations en mode mono-volet et en mode bi-volet.

Vous pouvez également accéder aux routes des destinations actuelles, aussi bien la destination d’un seul volet que les destinations des volets 1 et 2. Ces valeurs dépendent des routes des destinations passées dans le constructeur TwoPaneLayoutNav.

TwoPaneLayoutNav gère une backstack interne, de sorte que l’historique de navigation est enregistré lors du basculement entre un et deux volets. Le comportement du composant par défaut prend en charge l’appui arrière uniquement en mode volet unique. Si vous souhaitez ajouter la gestion des appuis précédents en mode à deux volets, ou remplacer le comportement par défaut en mode à volet unique, créez un backHandler personnalisé dans votre composant composable qui appelle navigateBack. Cela garantit que la backstack interne est correctement gérée. La backstack est également exposée via l’interface avec le champ, ce qui vous permet d’accéder à la twoPaneBackStack taille et au contenu de la pile arrière si nécessaire.

Animation montrant comment TwoPaneLayoutNav gère une backstack et prend en charge le comportement d’appui arrière en mode à volet unique.

Exemple d'utilisation :

val navController = rememberNavController()

TwoPaneLayoutNav(
    navController = navController,
    singlePaneStartDestination = "A",
    pane1StartDestination = "A",
    pane2StartDestination = "B"
) {
    composable("A") {
        ContentA(Modifier.clickable { navController.navigateTo("B", Screen.Pane2) })
    }
    composable("B") {
        ContentB(Modifier.clickable { navController.navigateTo("C", Screen.Pane2) })
    }
    composable("C") {
        ContentC(Modifier.clickable { navController.navigateTo("A", Screen.Pane1) })
    }
}

Tester les composables TwoPaneLayout

Quand vous écrivez des tests d’interface utilisateur pour les composables utilisés dans TwoPaneLayout, vous pouvez utiliser des classes d’étendue de test pour configurer vos tests. Ces classes, TwoPaneScopeTest et TwoPaneNavScopeTest, fournissent des implémentations vides pour toutes les méthodes d’interface et vous permettent de définir des valeurs de champ dans le constructeur de classe.

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

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

Exemple d'utilisation :

// Composable function in app
@Composable
fun TwoPaneScope.Example() {
    if (isSinglePane)
        Text("single pane")
    else
        Text("two pane")
}

...

// UI test in androidTest directory
@Test
fun exampleTest() {
    composeTestRule.setContent {
        val twoPaneScope = TwoPaneScopeTest(isSinglePane = true)
        twoPaneScope.Example()
    }

    composeTestRule.onNodeWithText("single pane").assertIsDisplayed()
    composeTestRule.onNodeWithText("two pane").assertDoesNotExist()
}