Incorporamento delle attività

L'incorporamento delle attività ottimizza le app sui dispositivi con schermi di grandi dimensioni dividendo la finestra delle attività di un'applicazione tra due attività o due istanze della stessa attività.

Figura 1. App Impostazioni con le attività affiancate.

Se la tua app è composta da più attività, l'incorporamento delle attività ti consente di offrire un'esperienza utente migliorata su tablet, dispositivi pieghevoli e ChromeOS.

L'incorporamento delle attività non richiede il refactoring del codice. Determina la modalità di visualizzazione delle attività dell'app, affiancate o impilate, creando un file di configurazione XML o effettuando chiamate API Jetpack WindowManager.

Il supporto per gli schermi di piccole dimensioni viene gestito automaticamente. Quando la tua app è su un dispositivo con uno schermo piccolo, le attività sono impilate una sopra l'altra. Sugli schermi di grandi dimensioni, le attività vengono visualizzate una accanto all'altra. Il sistema determina la presentazione in base alla configurazione che hai creato, senza richiedere una logica di ramificazione.

L'incorporamento delle attività si adatta ai cambiamenti di orientamento del dispositivo e funziona perfettamente sui dispositivi pieghevoli, impilando e separando le attività quando il dispositivo si piega e si apre.

L'incorporamento delle attività è supportato sulla maggior parte dei dispositivi con schermi di grandi dimensioni con Android 12L (livello API 32) e versioni successive.

Finestra dell'attività divisa

L'incorporamento delle attività divide la finestra delle attività dell'app in due container: principale e secondario. I contenitori contengono attività avviate dall'attività principale o da altre attività già presenti nei contenitori.

Le attività vengono impilate nel contenitore secondario man mano che vengono avviate e il contenitore secondario viene impilato sopra il contenitore principale sugli schermi piccoli, quindi l'impilamento delle attività e la navigazione indietro sono coerenti con l'ordine delle attività già integrate nell'app.

L'incorporamento delle attività ti consente di visualizzarle in vari modi. La tua app può dividere la finestra dell'attività avviando due attività contemporaneamente una accanto all'altra o una sopra l'altra:

Figura 2. Due attività affiancate e una sopra l'altra.

Un'attività che occupa l'intera finestra dell'attività può creare una divisione avviando una nuova attività contemporaneamente:

Figura 3. L'attività A avvia l'attività B di lato.

Le attività già in una finestra di divisione e condivisione di un'attività possono essere avviate in altri modi:

  • Di lato, sopra un'altra attività:

    Figura 4. L'attività A avvia l'attività C lateralmente rispetto all'attività B.
  • Di lato e sposta la divisione lateralmente, nascondendo l'attività principale precedente:

    Figura 5. L'attività B avvia l'attività C lateralmente e sposta la divisione lateralmente.
  • Avvia un'attività in primo piano, ovvero nella stessa pila di attività:

    Figura 6. L'attività B avvia l'attività C senza flag di intent aggiuntivi.
  • Avvia un'attività a schermo intero nella stessa attività:

    Figura 7. L'attività A o l'attività B avvia l'attività C che riempie la finestra dell'attività.

Navigazione a ritroso

I diversi tipi di applicazioni possono avere regole di navigazione indietro diverse in uno stato della finestra di divisione delle attività a seconda delle dipendenze tra le attività o del modo in cui gli utenti attivano l'evento Indietro, ad esempio:

  • Andare insieme: se le attività sono correlate e una non deve essere mostrata senza l'altra, la navigazione indietro può essere configurata per completarle entrambe.
  • Se le attività sono completamente indipendenti, la navigazione indietro in un'attività non influisce sullo stato di un'altra attività nella finestra delle attività.

L'evento Indietro viene inviato all'ultima attività selezionata quando si utilizza la navigazione con i pulsanti.

Per la navigazione basata sui gesti:

  • Android 14 (livello API 34) e versioni precedenti: l'evento Indietro viene inviato all'attività in cui si è verificato il gesto. Quando gli utenti scorrono dal lato sinistro dello schermo, l'evento Indietro viene inviato all'attività nel riquadro a sinistra della finestra divisa. Quando gli utenti scorrono dal lato destro dello schermo, l'evento Indietro viene inviato all'attività nel riquadro a destra.

  • Android 15 (livello API 35) e versioni successive

    • Quando si gestiscono più attività della stessa app, il gesto termina l'attività in primo piano indipendentemente dalla direzione dello scorrimento, offrendo un'esperienza più unificata.

    • Negli scenari che coinvolgono due attività di app diverse (sovrapposizione), l'evento Indietro viene indirizzato all'ultima attività in primo piano, in linea con il comportamento della navigazione tramite pulsanti.

Layout a più riquadri

Jetpack WindowManager ti consente di creare un layout multi-riquadro con incorporamento di attività su dispositivi con schermi di grandi dimensioni con Android 12L (livello API 32) o versioni successive e su alcuni dispositivi con versioni precedenti della piattaforma. Le app esistenti basate su più attività anziché su layout basati su fragment o visualizzazioni, come SlidingPaneLayout, possono offrire un'esperienza utente migliorata su schermi di grandi dimensioni senza eseguire il refactoring del codice sorgente.

Un esempio comune è la suddivisione elenco-dettagli. Per garantire una presentazione di alta qualità, il sistema avvia l'attività di elenco, quindi l'applicazione avvia immediatamente l'attività di dettaglio. Il sistema di transizione attende che vengano disegnate entrambe le attività, quindi le visualizza insieme. Per l'utente, le due attività vengono avviate come una sola.

Figura 8. Due attività avviate contemporaneamente in un layout multischermo.

Attributi di suddivisione

Puoi specificare la proporzione della finestra dell'attività tra i contenitori suddivisi e la disposizione dei contenitori l'uno rispetto all'altro.

Per le regole definite in un file di configurazione XML, imposta i seguenti attributi:

  • splitRatio: imposta le proporzioni del contenitore. Il valore è un numero in virgola mobile nell'intervallo aperto (0,0, 1,0).
  • splitLayoutDirection: specifica la disposizione dei contenitori suddivisi l'uno rispetto all'altro. I valori includono:
    • ltr: da sinistra a destra
    • rtl: da destra a sinistra
    • locale: ltr o rtl viene determinato dall'impostazione delle impostazioni internazionali

Per esempi, consulta la sezione Configurazione XML.

Per le regole create utilizzando le API WindowManager, crea un oggetto SplitAttributes con SplitAttributes.Builder e chiama i seguenti metodi del builder:

Per esempi, consulta la sezione API WindowManager.

Figura 9. Due suddivisioni dell'attività disposte da sinistra a destra, ma con rapporti di suddivisione diversi.

Orientamento della suddivisione

Le dimensioni e le proporzioni del display determinano il posizionamento delle attività nelle suddivisioni dell'incorporamento delle attività. Sui display orizzontali di grandi dimensioni, le attività vengono visualizzate una accanto all'altra; sui display verticali o in modalità tavolo sui dispositivi pieghevoli, una sopra l'altra.

Puoi specificare l'orientamento della suddivisione con il SplitController SplitAttributes. Il calcolatore calcola SplitAttributes per l'SplitRule attivo.

Utilizza il calcolatore per dividere il contenitore principale in direzioni diverse per diversi stati del dispositivo, ad esempio:

Kotlin

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val parentConfiguration = params.parentConfiguration
        val builder = SplitAttributes.Builder()
        return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build()
        } else {
            // Fallback to expand the secondary container.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        Configuration parentConfiguration = params.getParentConfiguration();
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build();
        } else {
            // Fallback to expand the secondary container.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

Sui dispositivi pieghevoli, puoi dividere lo schermo verticalmente se il dispositivo è in orizzontale, visualizzare una singola attività se il dispositivo è in verticale e dividere lo schermo orizzontalmente se il dispositivo è in modalità Tabletop:

Kotlin

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val tag = params.splitRuleTag
        val parentWindowMetrics = params.parentWindowMetrics
        val parentConfiguration = params.parentConfiguration
        val foldingFeatures =
            params.parentWindowLayoutInfo.displayFeatures.filterIsInstance<FoldingFeature>()
        val feature = if (foldingFeatures.size == 1) foldingFeatures[0] else null
        val builder = SplitAttributes.Builder()
        builder.setSplitType(SPLIT_TYPE_HINGE)
        return@setSplitAttributesCalculator if (feature?.isSeparating == true) {
            // Horizontal split for tabletop posture.
            builder
                .setSplitType(SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
                        SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                    } else {
                        SplitAttributes.LayoutDirection.LOCALE
                    }
                )
                .build()
        } else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else {
            // No split for tall displays.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        String tag = params.getSplitRuleTag();
        WindowMetrics parentWindowMetrics = params.getParentWindowMetrics();
        Configuration parentConfiguration = params.getParentConfiguration();
        List<FoldingFeature> foldingFeatures =
            params.getParentWindowLayoutInfo().getDisplayFeatures().stream().filter(
                    item -> item instanceof FoldingFeature)
                .map(item -> (FoldingFeature) item)
                .collect(Collectors.toList());
        FoldingFeature feature = foldingFeatures.size() == 1 ? foldingFeatures.get(0) : null;
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        builder.setSplitType(SplitType.SPLIT_TYPE_HINGE);
        if (feature != null && feature.isSeparating()) {
            // Horizontal slit for tabletop posture.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    feature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL
                        ? SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                        : SplitAttributes.LayoutDirection.LOCALE)
                .build();
        }
        else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else {
            // No split for tall displays.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

Segnaposto

Le attività segnaposto sono attività secondarie vuote che occupano un'area di una divisione dell'attività. In definitiva, devono essere sostituite con un'altra attività che contenga contenuti. Ad esempio, un'attività segnaposto potrebbe occupare il lato secondario di una divisione dell'attività in un layout elenco-dettagli finché non viene selezionato un elemento dall'elenco, a quel punto un'attività contenente le informazioni dettagliate per l'elemento dell'elenco selezionato sostituisce il segnaposto.

Per impostazione predefinita, il sistema mostra i segnaposto solo quando c'è spazio sufficiente per una divisione dell'attività. I segnaposto vengono completati automaticamente quando le dimensioni del display cambiano in una larghezza o un'altezza troppo piccole per visualizzare una divisione. Quando lo spazio lo consente, il sistema riavvia il segnaposto con uno stato reinizializzato.

Figura 10. Dispositivo pieghevole che si piega e si apre. L'attività del segnaposto è terminata e viene ricreata al variare delle dimensioni del display.

Tuttavia, l'attributo stickyPlaceholder di un metodo SplitPlaceholderRule o setSticky() di SplitPlaceholder.Builder può sostituire il comportamento predefinito. Quando l'attributo o il metodo specifica un valore di true, il sistema visualizza il segnaposto come attività principale nella finestra delle attività quando il display viene ridimensionato a un display a un solo riquadro da un display a due riquadri (vedi Configurazione split per un esempio).

Figura 11. Dispositivo pieghevole che si piega e si apre. L'attività del segnaposto è persistente.

Modifiche alle dimensioni della finestra

Quando le modifiche alla configurazione del dispositivo riducono la larghezza della finestra dell'attività in modo che non sia abbastanza grande per un layout a più riquadri (ad esempio, quando un dispositivo pieghevole con schermo grande si piega dalle dimensioni di un tablet a quelle di uno smartphone o quando la finestra dell'app viene ridimensionata in modalità multi-finestra), le attività non segnaposto nel riquadro secondario della finestra dell'attività vengono impilate sopra le attività nel riquadro principale.

Le attività segnaposto vengono visualizzate solo quando la larghezza del display è sufficiente per una divisione. Sugli schermi più piccoli, il segnaposto viene chiuso automaticamente. Quando l'area di visualizzazione diventa di nuovo sufficientemente grande, il segnaposto viene ricreato. (Vedi la sezione Segnaposto.)

L'impilamento delle attività è possibile perché WindowManager ordina le attività nel riquadro secondario sopra le attività nel riquadro principale.

Più attività nel riquadro secondario

L'attività B avvia l'attività C in loco senza flag di intent aggiuntivi:

Suddivisione dell&#39;attività contenente le attività A, B e C con C impilata sopra B.

con il seguente ordine di visualizzazione delle attività nella stessa attività:

Stack delle attività secondarie contenente l&#39;attività C impilata sopra la B.
          Lo stack secondario è impilato sopra lo stack di attività principale
          che contiene l&#39;attività A.

Pertanto, in una finestra dell'attività più piccola, l'applicazione si riduce a una singola attività con C nella parte superiore dello stack:

Piccola finestra che mostra solo l&#39;attività C.

Tornare indietro nella finestra più piccola consente di scorrere le attività impilate una sopra l'altra.

Se la configurazione della finestra delle attività viene ripristinata a una dimensione maggiore in grado di ospitare più riquadri, le attività vengono visualizzate di nuovo una accanto all'altra.

Divisioni in pila

L'attività B avvia l'attività C di lato e sposta la divisione lateralmente:

Finestra delle attività che mostra le attività A e B, poi le attività B e C.

Il risultato è il seguente ordine di visualizzazione delle attività nella stessa attività:

Attività A, B e C in un unico stack. Le attività sono impilate
          nel seguente ordine dall&#39;alto verso il basso: C, B, A.

In una finestra delle attività più piccola, l'applicazione si riduce a una singola attività con C in alto:

Piccola finestra che mostra solo l&#39;attività C.

Orientamento verticale fisso

L'impostazione del manifest android:screenOrientation consente alle app di limitare le attività all'orientamento verticale o orizzontale. Per migliorare l'esperienza utente sui dispositivi con schermi di grandi dimensioni, come tablet e pieghevoli, i produttori di dispositivi (OEM) possono ignorare le richieste di orientamento dello schermo e aggiungere barre nere all'app in orientamento verticale sui display orizzontali o in orientamento orizzontale sui display verticali.

Figura 12. Attività in letterbox: verticale fissa su dispositivo orizzontale (a sinistra), orizzontale fissa su dispositivo verticale (a destra).

Allo stesso modo, quando l'incorporamento delle attività è attivato, gli OEM possono personalizzare i dispositivi per letterbox attività con orientamento verticale fisso in orientamento orizzontale su schermi di grandi dimensioni (larghezza ≥ 600 dp). Quando un'attività con orientamento verticale fisso avvia una seconda attività, il dispositivo può visualizzare le due attività affiancate in una visualizzazione a due riquadri.

Figura 13. L'attività A in formato verticale fisso avvia l'attività B lateralmente.

Aggiungi sempre la proprietà android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED al file manifest dell'app per comunicare ai dispositivi che la tua app supporta l'incorporamento di attività (vedi la sezione Configurazione split). I dispositivi personalizzati dall'OEM possono quindi determinare se aggiungere barre nere alle attività in formato verticale fisso.

Configurazione della suddivisione

Le regole di divisione configurano le divisioni delle attività. Definisci le regole di suddivisione in un file di configurazione XML o effettuando chiamate API WindowManager di Jetpack.

In entrambi i casi, l'app deve accedere alla libreria WindowManager e deve comunicare al sistema che ha implementato l'incorporamento delle attività.

Esegui le seguenti azioni:

  1. Aggiungi la dipendenza della libreria WindowManager più recente al file build.gradle a livello di modulo dell'app, ad esempio:

    implementation 'androidx.window:window:1.1.0-beta02'

    La libreria WindowManager fornisce tutti i componenti necessari per l'incorporamento delle attività.

  2. Comunica al sistema che la tua app ha implementato l'incorporamento di attività.

    Aggiungi la proprietà android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED all'elemento <application> del file manifest dell'app e imposta il valore su true, ad esempio:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <property
                android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
                android:value="true" />
        </application>
    </manifest>
    

    Nelle versioni 1.1.0-alpha06 e successive di WindowManager, le suddivisioni dell'incorporamento di attività sono disattivate a meno che la proprietà non venga aggiunta al manifest e impostata su true.

    Inoltre, i produttori di dispositivi utilizzano l'impostazione per attivare funzionalità personalizzate per le app che supportano l'incorporamento di attività. Ad esempio, i dispositivi possono inserire una barra nera in alto e in basso in un'attività solo verticale sui display orizzontali per orientare l'attività per la transizione a un layout a due riquadri quando inizia una seconda attività (vedi Orientamento verticale fisso).

Configurazione XML

Per creare un'implementazione basata su XML dell'incorporamento dell'attività, completa i seguenti passaggi:

  1. Crea un file di risorse XML che:

    • Definisce le attività che condividono una divisione
    • Configura le opzioni di suddivisione
    • Crea un segnaposto per il contenitore secondario della suddivisione quando i contenuti non sono disponibili
    • Specifica le attività che non devono mai far parte di una divisione

    Ad esempio:

    <!-- main_split_config.xml -->
    
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Define a split for the named activities. -->
        <SplitPairRule
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:finishPrimaryWithSecondary="never"
            window:finishSecondaryWithPrimary="always"
            window:clearTop="false">
            <SplitPairFilter
                window:primaryActivityName=".ListActivity"
                window:secondaryActivityName=".DetailActivity"/>
        </SplitPairRule>
    
        <!-- Specify a placeholder for the secondary container when content is
             not available. -->
        <SplitPlaceholderRule
            window:placeholderActivityName=".PlaceholderActivity"
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:stickyPlaceholder="false">
            <ActivityFilter
                window:activityName=".ListActivity"/>
        </SplitPlaceholderRule>
    
        <!-- Define activities that should never be part of a split. Note: Takes
             precedence over other split rules for the activity named in the
             rule. -->
        <ActivityRule
            window:alwaysExpand="true">
            <ActivityFilter
                window:activityName=".ExpandedActivity"/>
        </ActivityRule>
    
    </resources>
    
  2. Crea un inizializzatore.

    Il componente WindowManager RuleController analizza il file di configurazione XML e rende disponibili le regole per il sistema. Una libreria Startup di Jetpack Initializer rende disponibile il file XML a RuleController all'avvio dell'app, in modo che le regole siano effettive all'avvio di qualsiasi attività.

    Per creare un inizializzatore:

    1. Aggiungi la dipendenza della libreria Jetpack Startup più recente al file build.gradle a livello di modulo, ad esempio:

      implementation 'androidx.startup:startup-runtime:1.1.1'

    2. Crea una classe che implementi l'interfaccia Initializer.

      L'inizializzatore rende disponibili le regole di suddivisione per RuleController passando l'ID del file di configurazione XML (main_split_config.xml) al metodo RuleController.parseRules().

      Kotlin

      class SplitInitializer : Initializer<RuleController> {
      
          override fun create(context: Context): RuleController {
              return RuleController.getInstance(context).apply {
                  setRules(RuleController.parseRules(context, R.xml.main_split_config))
              }
          }
      
          override fun dependencies(): List<Class<out Initializer<*>>> {
              return emptyList()
          }
      }

      Java

      public class SplitInitializer implements Initializer<RuleController> {
      
          @NonNull
          @Override
          public RuleController create(@NonNull Context context) {
              RuleController ruleController = RuleController.getInstance(context);
              ruleController.setRules(
                  RuleController.parseRules(context, R.xml.main_split_config)
              );
               return ruleController;
           }
      
           @NonNull
           @Override
           public List<Class<? extends Initializer<?>>> dependencies() {
               return Collections.emptyList();
           }
      }

  3. Crea un fornitore di contenuti per le definizioni delle regole.

    Aggiungi androidx.startup.InitializationProvider al file manifest dell'app come <provider>. Includi un riferimento all'implementazione dell'inizializzatore RuleController, SplitInitializer:

    <!-- AndroidManifest.xml -->
    
    <provider android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <!-- Make SplitInitializer discoverable by InitializationProvider. -->
        <meta-data android:name="${applicationId}.SplitInitializer"
            android:value="androidx.startup" />
    </provider>
    

    InitializationProvider rileva e inizializza SplitInitializer prima che venga chiamato il metodo onCreate() dell'app. Di conseguenza, le regole di suddivisione sono attive all'avvio dell'attività principale dell'app.

API WindowManager

Puoi implementare l'incorporamento delle attività in modo programmatico con una serie di chiamate API. Esegui le chiamate nel metodo onCreate() di una sottoclasse di Application per assicurarti che le regole siano in vigore prima dell'avvio di qualsiasi attività.

Per creare una divisione dell'attività a livello di programmazione:

  1. Crea una regola di suddivisione:

    1. Crea un SplitPairFilter che identifichi le attività che condividono la suddivisione:

      Kotlin

      val splitPairFilter = SplitPairFilter(
          ComponentName(this, ListActivity::class.java),
          ComponentName(this, DetailActivity::class.java),
          null
      )

      Java

      SplitPairFilter splitPairFilter = new SplitPairFilter(
         new ComponentName(this, ListActivity.class),
         new ComponentName(this, DetailActivity.class),
         null
      );

    2. Aggiungi il filtro a un insieme di filtri:

      Kotlin

      val filterSet = setOf(splitPairFilter)

      Java

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
      ```

    3. Crea gli attributi di layout per la divisione:

      Kotlin

      val splitAttributes: SplitAttributes = SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build()

      Java

      SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();

      SplitAttributes.Builder crea un oggetto contenente gli attributi del layout:

      • setSplitType(): definisce come viene allocata l'area di visualizzazione disponibile a ciascun contenitore di attività. Il tipo di suddivisione del rapporto specifica la proporzione dell'area di visualizzazione disponibile allocata al contenitore principale; il contenitore secondario occupa la parte restante dell'area di visualizzazione disponibile.
      • setLayoutDirection(): specifica la disposizione dei contenitori delle attività uno rispetto all'altro, con il contenitore principale per primo.
    4. Crea un SplitPairRule:

      Kotlin

      val splitPairRule = SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build()

      Java

      SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build();

      SplitPairRule.Builder crea e configura la regola:

      • filterSet: contiene filtri di coppia di suddivisione che determinano quando applicare la regola identificando le attività che condividono una suddivisione.
      • setDefaultSplitAttributes(): applica gli attributi di layout alla regola.
      • setMinWidthDp(): imposta la larghezza minima del display (in pixel indipendenti dalla densità, dp) che consente una divisione.
      • setMinSmallestWidthDp(): imposta il valore minimo (in dp) che la più piccola delle due dimensioni del display deve avere per attivare una divisione indipendentemente dall'orientamento del dispositivo.
      • setMaxAspectRatioInPortrait(): imposta le proporzioni di visualizzazione massime (altezza:larghezza) in orientamento verticale per le quali vengono visualizzate le suddivisioni dell'attività. Se le proporzioni di un display verticale superano le proporzioni massime, le suddivisioni vengono disattivate indipendentemente dalla larghezza del display. Nota:il valore predefinito è 1,4, il che fa sì che le attività occupino l'intera finestra delle attività in orientamento verticale sulla maggior parte dei tablet. Vedi anche SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT e setMaxAspectRatioInLandscape(). Il valore predefinito per il formato orizzontale è ALWAYS_ALLOW.
      • setFinishPrimaryWithSecondary(): imposta il modo in cui il completamento di tutte le attività nel contenitore secondario influisce sulle attività nel contenitore principale. NEVER indica che il sistema non deve terminare le attività principali quando tutte le attività nel contenitore secondario terminano (vedi Termina attività).
      • setFinishSecondaryWithPrimary(): imposta il modo in cui il completamento di tutte le attività nel contenitore principale influisce sulle attività nel contenitore secondario. ALWAYS indica che il sistema deve sempre terminare le attività nel contenitore secondario quando tutte le attività nel contenitore principale sono terminate (vedi Terminare le attività).
      • setClearTop(): specifica se tutte le attività nel container secondario sono terminate quando viene avviata una nuova attività nel container. Un valore false specifica che le nuove attività vengono inserite in pila sopra le attività già presenti nel contenitore secondario.
    5. Ottieni l'istanza singleton di WindowManager RuleController, e aggiungi la regola:

      Kotlin

      val ruleController = RuleController.getInstance(this)
      ruleController.addRule(splitPairRule)

      Java

      RuleController ruleController = RuleController.getInstance(this);
      ruleController.addRule(splitPairRule);

    6. Crea un segnaposto per il contenitore secondario quando il contenuto non è disponibile:

    7. Crea un ActivityFilter che identifichi l'attività con cui il segnaposto condivide una suddivisione della finestra dell'attività:

      Kotlin

      val placeholderActivityFilter = ActivityFilter(
          ComponentName(this, ListActivity::class.java),
          null
      )

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );

    8. Aggiungi il filtro a un insieme di filtri:

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);

    9. Crea un SplitPlaceholderRule:

      Kotlin

      val splitPlaceholderRule = SplitPlaceholderRule.Builder(
          placeholderActivityFilterSet,
          Intent(context, PlaceholderActivity::class.java)
      ).setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
          .setSticky(false)
          .build()

      Java

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            new Intent(this, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();

      SplitPlaceholderRule.Builder crea e configura la regola:

      • placeholderActivityFilterSet: contiene i filtri attività che determinano quando applicare la regola identificando le attività a cui è associata l'attività segnaposto.
      • Intent: specifica l'avvio dell'attività segnaposto.
      • setDefaultSplitAttributes(): Applica gli attributi di layout alla regola.
      • setMinWidthDp(): Imposta la larghezza minima del display (in pixel indipendenti dalla densità, dp) che consente una divisione.
      • setMinSmallestWidthDp(): Imposta il valore minimo (in dp) che la più piccola delle due dimensioni del display deve avere per consentire una divisione indipendentemente dall'orientamento del dispositivo.
      • setMaxAspectRatioInPortrait(): Imposta le proporzioni massime di visualizzazione (altezza:larghezza) in orientamento verticale per cui vengono visualizzate le suddivisioni dell'attività. Nota:il valore predefinito è 1,4, il che fa sì che le attività riempiano la finestra delle attività in orientamento verticale sulla maggior parte dei tablet. Vedi anche SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT e setMaxAspectRatioInLandscape(). Il valore predefinito per il formato orizzontale è ALWAYS_ALLOW.
      • setFinishPrimaryWithPlaceholder(): Imposta il modo in cui il completamento dell'attività segnaposto influisce sulle attività nel contenitore principale. SEMPRE indica che il sistema deve sempre terminare le attività nel contenitore principale quando il segnaposto termina (vedi Termina attività).
      • setSticky(): determina se l'attività segnaposto viene visualizzata in cima alla pila di attività sui display di piccole dimensioni una volta che il segnaposto è apparso per la prima volta in una divisione con una larghezza minima sufficiente.
    10. Aggiungi la regola a WindowManager RuleController:

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);

  2. Specifica le attività che non devono mai far parte di una divisione:

    1. Crea un ActivityFilter che identifichi un'attività che deve sempre occupare l'intera area di visualizzazione dell'attività:

      Kotlin

      val expandedActivityFilter = ActivityFilter(
          ComponentName(this, ExpandedActivity::class.java),
          null
      )

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
          new ComponentName(this, ExpandedActivity.class),
          null
      );

    2. Aggiungi il filtro a un insieme di filtri:

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);

    3. Crea un ActivityRule:

      Kotlin

      val activityRule = ActivityRule.Builder(expandedActivityFilterSet)
          .setAlwaysExpand(true)
          .build()

      Java

      ActivityRule activityRule = new ActivityRule.Builder(
          expandedActivityFilterSet
      ).setAlwaysExpand(true)
       .build();

      ActivityRule.Builder crea e configura la regola:

      • expandedActivityFilterSet: contiene filtri attività che determinano quando applicare la regola identificando le attività che vuoi escludere dalle suddivisioni.
      • setAlwaysExpand(): specifica se l'attività deve riempire l'intera finestra dell'attività.
    4. Aggiungi la regola a WindowManager RuleController:

      Kotlin

      ruleController.addRule(activityRule)

      Java

      ruleController.addRule(activityRule);

Incorporamento tra applicazioni

Su Android 13 (livello API 33) e versioni successive, le app possono incorporare attività di altre app. L'incorporamento di attività tra applicazioni diverse o tra UID diversi consente l'integrazione visiva di attività provenienti da più applicazioni Android. Il sistema mostra un'attività dell'app host e un'attività incorporata di un'altra app sullo schermo affiancate o una sopra l'altra, proprio come nell'incorporamento di attività di una singola app.

Ad esempio, l'app Impostazioni potrebbe incorporare l'attività di selezione dello sfondo dall'app WallpaperPicker:

Figura 14. App Impostazioni (menu a sinistra) con il selettore di sfondi come attività incorporata (a destra).

Modello di attendibilità

I processi host che incorporano attività di altre app sono in grado di ridefinire la presentazione delle attività incorporate, tra cui dimensioni, posizione, ritaglio e trasparenza. Gli host dannosi possono utilizzare questa funzionalità per ingannare gli utenti e creare attacchi di clickjacking o di altro tipo di UI redressing.

Per evitare l'uso improprio dell'incorporamento dell'attività cross-app, Android richiede alle app di attivare l'incorporamento delle proprie attività. Le app possono designare gli host come attendibili o non attendibili.

Host attendibili

Per consentire ad altre applicazioni di incorporare e controllare completamente la presentazione delle attività della tua app, specifica il certificato SHA-256 dell'applicazione host nell'attributo android:knownActivityEmbeddingCerts degli elementi <activity> o <application> del file manifest della tua app.

Imposta il valore di android:knownActivityEmbeddingCerts come stringa:

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@string/known_host_certificate_digest"
    ... />

oppure, per specificare più certificati, un array di stringhe:

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@array/known_host_certificate_digests"
    ... />

che fa riferimento a una risorsa come la seguente:

<resources>
    <string-array name="known_host_certificate_digests">
      <item>cert1</item>
      <item>cert2</item>
      ...
    </string-array>
</resources>

I proprietari delle app possono ottenere un digest del certificato SHA eseguendo l'attività Gradle signingReport. Il digest del certificato è l'impronta SHA-256 senza i due punti che separano i caratteri. Per saperne di più, vedi Generare un report sulla firma e Autenticare il client.

Host non attendibili

Per consentire a qualsiasi app di incorporare le attività della tua app e controllarne la presentazione, specifica l'attributo android:allowUntrustedActivityEmbedding negli elementi <activity> o <application> nel manifest dell'app, ad esempio:

<activity
    android:name=".MyEmbeddableActivity"
    android:allowUntrustedActivityEmbedding="true"
    ... />

Il valore predefinito dell'attributo è false, che impedisce l'incorporamento dell'attività cross-app.

Autenticazione personalizzata

Per ridurre i rischi di incorporamento di attività non attendibili, crea un meccanismo di autenticazione personalizzato che verifichi l'identità dell'host. Se conosci i certificati host, utilizza la libreria androidx.security.app.authenticator per l'autenticazione. Se l'organizzatore si autentica dopo l'incorporamento della tua attività, puoi visualizzare i contenuti effettivi. In caso contrario, puoi comunicare all'utente che l'azione non è stata consentita e bloccare i contenuti.

Utilizza il metodo ActivityEmbeddingController#isActivityEmbedded() della libreria Jetpack WindowManager per verificare se un host sta incorporando la tua attività, ad esempio:

Kotlin

fun isActivityEmbedded(activity: Activity): Boolean {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
}

Java

boolean isActivityEmbedded(Activity activity) {
    return ActivityEmbeddingController.getInstance(context).isActivityEmbedded(activity);
}

Limitazione relativa alle dimensioni minime

Il sistema Android applica l'altezza e la larghezza minime specificate nell'elemento manifest <layout> alle attività incorporate. Se un'applicazione non specifica l'altezza e la larghezza minime, vengono applicati i valori predefiniti del sistema (sw220dp).

Se l'host tenta di ridimensionare il contenitore incorporato a una dimensione inferiore a quella minima, il contenitore incorporato si espande fino a occupare l'intero riquadro dell'attività.

<activity-alias>

Perché l'incorporamento di attività attendibili o non attendibili funzioni con l'elemento <activity-alias>, android:knownActivityEmbeddingCerts o android:allowUntrustedActivityEmbedding deve essere applicato all'attività di destinazione anziché all'alias. Il criterio che verifica la sicurezza sul server di sistema si basa sui flag impostati sulla destinazione, non sull'alias.

Applicazione host

Le applicazioni host implementano l'incorporamento dell'attività cross-app nello stesso modo in cui implementano l'incorporamento dell'attività in una singola app. Gli oggetti SplitPairRule e SplitPairFilter o ActivityRule e ActivityFilter specificano le attività incorporate e le suddivisioni della finestra delle attività. Le regole di suddivisione sono definite staticamente in XML o in fase di runtime utilizzando le chiamate API Jetpack WindowManager.

Se un'applicazione host tenta di incorporare un'attività che non ha attivato l'incorporamento tra app, l'attività occupa l'intero limite dell'attività. Di conseguenza, le applicazioni host devono sapere se le attività di destinazione consentono l'incorporamento tra app.

Se un'attività incorporata avvia una nuova attività nella stessa attività e la nuova attività non ha attivato l'incorporamento tra app, l'attività occupa l'intero limite dell'attività anziché sovrapporsi all'attività nel contenitore incorporato.

Un'applicazione host può incorporare le proprie attività senza limitazioni, a condizione che le attività vengano avviate nella stessa attività.

Esempi di suddivisione

Dividere lo schermo dalla finestra intera

Figura 15. L'attività A avvia l'attività B di lato.

Non è necessario il refactoring. Puoi definire la configurazione della suddivisione in modo statico o in fase di runtime e poi chiamare Context#startActivity() senza parametri aggiuntivi.

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Suddivisione predefinita

Quando la pagina di destinazione di un'applicazione è progettata per essere suddivisa in due contenitori su schermi di grandi dimensioni, l'esperienza utente è migliore quando entrambe le attività vengono create e presentate contemporaneamente. Tuttavia, i contenuti potrebbero non essere disponibili per il contenitore secondario della suddivisione finché l'utente non interagisce con l'attività nel contenitore principale (ad esempio, l'utente seleziona un elemento da un menu di navigazione). Un'attività segnaposto può riempire il vuoto finché i contenuti non possono essere visualizzati nel contenitore secondario della suddivisione (vedi la sezione Segnaposto).

Figura 16. Divisione creata aprendo due attività contemporaneamente. Un'attività è un segnaposto.

Per creare una suddivisione con un segnaposto, crea un segnaposto e associalo all'attività principale:

<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity">
    <ActivityFilter
        window:activityName=".MainActivity"/>
</SplitPlaceholderRule>

Quando un'app riceve un intent, l'attività di destinazione può essere mostrata come parte secondaria di una divisione dell'attività, ad esempio una richiesta di visualizzazione di una schermata di dettagli con informazioni su un elemento di un elenco. Sui display piccoli, i dettagli vengono visualizzati nella finestra completa dell'attività; sui dispositivi più grandi, accanto all'elenco.

Figura 17. Attività di dettaglio del deep link visualizzata da sola su un piccolo schermo, ma insieme a un'attività di elenco su un grande schermo.

La richiesta di avvio deve essere indirizzata all'attività principale e l'attività di dettaglio di destinazione deve essere avviata in una suddivisione. Il sistema sceglie automaticamente la presentazione corretta, in pila o affiancata, in base alla larghezza di visualizzazione disponibile.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    RuleController.getInstance(this)
        .addRule(new SplitPairRule.Builder(filterSet).build());
    startActivity(new Intent(this, DetailActivity.class));
}

La destinazione del link diretto potrebbe essere l'unica attività che dovrebbe essere disponibile per l'utente nello stack di navigazione indietro e potresti voler evitare di chiudere l'attività di dettaglio e lasciare solo l'attività principale:

Display di grandi dimensioni con attività dell&#39;elenco e attività dettagliate affiancate.
          La navigazione indietro non consente di chiudere l&#39;attività di dettaglio e lasciare
          l&#39;attività di elenco sullo schermo.

Piccolo display con solo attività dettagliata. La navigazione indietro non è in grado di
          chiudere l&#39;attività di dettaglio e mostrare l&#39;attività di elenco.

In alternativa, puoi completare entrambe le attività contemporaneamente utilizzando l'attributo finishPrimaryWithSecondary:

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".ListActivity"
        window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

Consulta la sezione Attributi di configurazione.

Più attività in contenitori suddivisi

L'impilamento di più attività in un contenitore diviso consente agli utenti di accedere a contenuti approfonditi. Ad esempio, con una suddivisione elenco-dettagli, l'utente potrebbe dover accedere a una sezione di dettagli secondari, ma mantenere l'attività principale:

Figura 18. L'attività è stata aperta nel riquadro secondario della finestra delle attività.

Kotlin

class DetailActivity : AppCompatActivity() {
    fun onOpenSubdetail() {
        startActivity(Intent(this, SubdetailActivity::class.java))
    }
}

Java

public class DetailActivity  extends AppCompatActivity {
    void onOpenSubdetail() {
        startActivity(new Intent(this, SubdetailActivity.class));
    }
}

L'attività secondaria viene posizionata sopra l'attività principale, nascondendola:

L'utente può quindi tornare al livello di dettaglio precedente tornando indietro nella pila:

Figura 19. L'attività è stata rimossa dalla parte superiore della pila.

L'impilamento delle attività una sopra l'altra è il comportamento predefinito quando le attività vengono avviate da un'attività nello stesso contenitore secondario. Le attività avviate dal contenitore principale all'interno di una divisione attiva finiscono anche nel contenitore secondario nella parte superiore dello stack di attività.

Attività in una nuova attività

Quando le attività in una finestra di attività divisa avviano attività in una nuova attività, la nuova attività è separata da quella che include la divisione e viene visualizzata a schermo intero. La schermata Recenti mostra due attività: l'attività nella divisione e la nuova attività.

Figura 20. Avvia l'attività C in una nuova attività dall'attività B.

Sostituzione dell'attività

Le attività possono essere sostituite nello stack del contenitore secondario, ad esempio quando l'attività principale viene utilizzata per la navigazione di primo livello e l'attività secondaria è una destinazione selezionata. Ogni selezione dalla navigazione di primo livello deve avviare una nuova attività nel contenitore secondario e rimuovere l'attività o le attività che erano presenti in precedenza.

Figura 21. L'attività di navigazione di primo livello nel riquadro principale sostituisce le attività di destinazione nel riquadro secondario.

Se l'app non completa l'attività nel contenitore secondario quando la selezione di navigazione cambia, la navigazione indietro potrebbe essere confusa quando la divisione viene compressa (quando il dispositivo è chiuso). Ad esempio, se hai un menu nel riquadro principale e le schermate A e B impilate nel riquadro secondario, quando l'utente chiude lo smartphone, B si trova sopra A e A sopra il menu. Quando l'utente torna indietro da B, viene visualizzata A anziché il menu.

In questi casi, la schermata A deve essere rimossa dallo stack precedente.

Il comportamento predefinito quando si avvia un'app lateralmente in un nuovo container su una divisione esistente è quello di posizionare i nuovi container secondari in primo piano e mantenere quelli precedenti nello stack precedente. Puoi configurare le suddivisioni in modo da cancellare i contenitori secondari precedenti con clearTop e avviare normalmente nuove attività.

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

Kotlin

inner class MenuActivity : AppCompatActivity() {
    fun onMenuItemSelected(selectedMenuItem: Int) {
        startActivity(Intent(this, classForItem(selectedMenuItem)))
    }
}

Java

public class MenuActivity extends AppCompatActivity{
    void onMenuItemSelected(int selectedMenuItem) {
        startActivity(new Intent(this, classForItem(selectedMenuItem)));
    }
}

In alternativa, utilizza la stessa attività secondaria e dall'attività principale (menu) invia nuovi intent che si risolvono nella stessa istanza, ma attivano un aggiornamento dello stato o dell'interfaccia utente nel contenitore secondario.

Divisioni multiple

Le app possono fornire una navigazione in profondità a più livelli avviando attività aggiuntive di lato.

Quando un'attività in un container secondario avvia una nuova attività lateralmente, viene creata una nuova divisione sopra quella esistente.

Figura 22. L'attività B avvia l'attività C di lato.

Lo stack precedente contiene tutte le attività aperte in precedenza, in modo che gli utenti possano passare alla suddivisione A/B dopo aver terminato C.

Attività A, B e C in uno stack. Le attività sono impilate nel seguente ordine dall&#39;alto verso il basso: C, B, A.

Per creare una nuova divisione, avvia la nuova attività lateralmente dal contenitore secondario esistente. Dichiara le configurazioni per le suddivisioni A/B e B/C e avvia l'attività C normalmente da B:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
    <SplitPairFilter
        window:primaryActivityName=".B"
        window:secondaryActivityName=".C"/>
</SplitPairRule>

Kotlin

class B : AppCompatActivity() {
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B extends AppCompatActivity{
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

Reagire alle modifiche dello stato di divisione

Diverse attività in un'app possono avere elementi dell'interfaccia utente che svolgono la stessa funzione, ad esempio un controllo che apre una finestra contenente le impostazioni dell'account.

Figura 23. Attività diverse con elementi dell'interfaccia utente funzionalmente identici.

Se due attività che hanno un elemento UI in comune sono in una divisione, è ridondante e forse confuso mostrare l'elemento in entrambe le attività.

Figura 24. Elementi dell'interfaccia utente duplicati nella divisione dell'attività.

Per sapere quando le attività sono in una divisione, controlla il flusso SplitController.splitInfoList o registra un listener con SplitControllerCallbackAdapter per le modifiche allo stato di divisione. Quindi, regola la UI di conseguenza:

Kotlin

val layout = layoutInflater.inflate(R.layout.activity_main, null)
val view = layout.findViewById<View>(R.id.infoButton)
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        splitController.splitInfoList(this@SplitDeviceActivity) // The activity instance.
            .collect { list ->
                view.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
            }
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    new SplitControllerCallbackAdapter(SplitController.getInstance(this))
        .addSplitListener(
            this,
            Runnable::run,
            splitInfoList -> {
                View layout = getLayoutInflater().inflate(R.layout.activity_main, null);
                layout.findViewById(R.id.infoButton).setVisibility(
                    splitInfoList.isEmpty() ? View.VISIBLE : View.GONE);
            });
}

Le coroutine possono essere avviate in qualsiasi stato del ciclo di vita, ma in genere vengono avviate nello stato STARTED per risparmiare risorse (per saperne di più, consulta Utilizzare le coroutine Kotlin con componenti sensibili al ciclo di vita).

I callback possono essere eseguiti in qualsiasi stato del ciclo di vita, anche quando un'attività è interrotta. Gli ascoltatori devono essere registrati in onStart() e non registrati in onStop().

Modale a finestra intera

Alcune attività impediscono agli utenti di interagire con l'applicazione finché non viene eseguita un'azione specifica, ad esempio un'attività della schermata di accesso, una schermata di conferma delle norme o un messaggio di errore. Le attività modali devono essere escluse dalla visualizzazione in una divisione.

Un'attività può essere forzata a riempire sempre la finestra dell'attività utilizzando la configurazione di espansione:

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

Terminare le attività

Gli utenti possono completare le attività su entrambi i lati della divisione scorrendo dal bordo del display:

Figura 25. Gesto di scorrimento per terminare l'attività B.
Figura 26. Gesto di scorrimento per terminare l'attività A.

Se il dispositivo è configurato per utilizzare il pulsante Indietro anziché la navigazione tramite gesti, l'input viene inviato all'attività selezionata, ovvero l'attività che è stata toccata o avviata per ultima.

L'effetto del completamento di tutte le attività in un container sul container opposto dipende dalla configurazione della divisione.

Attributi di configurazione

Puoi specificare gli attributi della regola di divisione della coppia per configurare in che modo il completamento di tutte le attività di un lato della divisione influisce sulle attività dell'altro lato della divisione. Gli attributi sono:

  • window:finishPrimaryWithSecondary — In che modo il completamento di tutte le attività nel contenitore secondario influisce sulle attività nel contenitore principale
  • window:finishSecondaryWithPrimary: in che modo il completamento di tutte le attività nel container principale influisce sulle attività nel container secondario

I valori possibili degli attributi includono:

  • always: completa sempre le attività nel contenitore associato
  • never: non completare mai le attività nel contenitore associato
  • adjacent: completa le attività nel contenitore associato quando i due contenitori vengono visualizzati uno accanto all'altro, ma non quando sono impilati

Ad esempio:

<SplitPairRule
    <!-- Do not finish primary container activities when all secondary container activities finish. -->
    window:finishPrimaryWithSecondary="never"
    <!-- Finish secondary container activities when all primary container activities finish. -->
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Configurazione predefinita

Quando tutte le attività in un container di una schermata divisa terminano, il container rimanente occupa l'intera finestra:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione contenente le attività A e B. A è terminato, quindi B occupa
          l&#39;intera finestra.

Divisione contenente le attività A e B. B è terminato, quindi A
          occupa l&#39;intera finestra.

Completare le attività insieme

Termina automaticamente le attività nel contenitore principale quando tutte le attività nel contenitore secondario sono terminate:

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione contenente le attività A e B. B è terminata, il che
          termina anche A, lasciando vuota la finestra dell&#39;attività.

Divisione contenente le attività A e B. A è terminata, lasciando B da sola
          nella finestra dell&#39;attività.

Termina automaticamente le attività nel contenitore secondario quando tutte le attività nel contenitore principale terminano:

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione contenente le attività A e B. A è terminato, il che
          termina anche B, lasciando la finestra delle attività vuota.

Divisione contenente le attività A e B. B viene completata, lasciando A da sola
          nella finestra dell&#39;attività.

Termina le attività insieme quando tutte le attività nel contenitore principale o secondario sono terminate:

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione contenente le attività A e B. A è terminato, il che
          termina anche B, lasciando la finestra delle attività vuota.

Divisione contenente le attività A e B. B è terminata, il che
          termina anche A, lasciando vuota la finestra dell&#39;attività.

Completare più attività nei container

Se più attività sono impilate in un contenitore diviso, il completamento di un'attività nella parte inferiore della pila non comporta il completamento automatico delle attività nella parte superiore.

Ad esempio, se due attività si trovano nel contenitore secondario, C sopra B:

Lo stack delle attività secondarie contenente l&#39;attività C impilata sopra B
          è impilato sopra lo stack delle attività principali contenente l&#39;attività
          A.

e la configurazione della suddivisione è definita dalla configurazione delle attività A e B:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Se completi l'attività in alto, la divisione viene mantenuta.

Divisione con l&#39;attività A nel contenitore principale e le attività B e C in quello secondario, con C impilata sopra B. C termina, lasciando A e B nella
          divisione dell&#39;attività.

Il completamento dell'attività principale (radice) del contenitore secondario non rimuove le attività sopra di essa e quindi mantiene anche la divisione.

Divisione con l&#39;attività A nel contenitore principale e le attività B e C in quello secondario, con C impilata sopra B. B termina, lasciando A e C nella
          divisione dell&#39;attività.

Vengono eseguite anche eventuali regole aggiuntive per il completamento delle attività insieme, ad esempio il completamento dell'attività secondaria con quella principale:

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione con l&#39;attività A nel contenitore principale e le attività B e C nel contenitore secondario, C impilata sopra B. A termina, terminando
          anche B e C.

E quando la suddivisione è configurata per terminare insieme la primaria e la secondaria:

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

Divisione con l&#39;attività A nel contenitore principale e le attività B e C in quello secondario, con C impilata sopra B. C termina, lasciando A e B nella
          divisione dell&#39;attività.

Divisione con l&#39;attività A nel contenitore principale e le attività B e C in quello secondario, con C impilata sopra B. B termina, lasciando A e C nella
          divisione dell&#39;attività.

Divisione con l&#39;attività A nel contenitore principale e le attività B e C in quello secondario, con C impilata sopra B. A termina, terminando anche B e
          C.

Modificare le proprietà di suddivisione in fase di runtime

Le proprietà di una suddivisione attiva e visibile non possono essere modificate. La modifica delle regole di suddivisione influisce su ulteriori avvii di attività e nuovi contenitori, ma non sulle suddivisioni esistenti e attive.

Per modificare le proprietà delle divisioni attive, completa l'attività secondaria o le attività nella divisione e avviala di nuovo lateralmente con una nuova configurazione.

Proprietà di suddivisione dinamica

Android 15 (livello API 35) e versioni successive supportate da Jetpack WindowManager 1.4 e versioni successive offrono funzionalità dinamiche che consentono la configurabilità delle suddivisioni dell'incorporamento delle attività, tra cui:

  • Espansione del riquadro: un divisore interattivo e trascinabile consente agli utenti di ridimensionare i riquadri in una presentazione divisa.
  • Blocco della pila di attività:gli utenti possono bloccare i contenuti in un container e isolare la navigazione nel container dalla navigazione nell'altro container.
  • Oscuramento a schermo intero della finestra di dialogo:quando viene visualizzata una finestra di dialogo, le app possono specificare se oscurare l'intera finestra dell'attività o solo il contenitore che ha aperto la finestra di dialogo.

Espansione del riquadro

L'espansione del riquadro consente agli utenti di regolare la quantità di spazio sullo schermo allocato alle due attività in un layout a due riquadri.

Per personalizzare l'aspetto del divisore della finestra e impostare l'intervallo trascinabile del divisore:

  1. Crea un'istanza di DividerAttributes

  2. Personalizza gli attributi del divisore:

    • color: il colore del separatore del riquadro trascinabile.

    • widthDp: la larghezza del separatore del riquadro trascinabile. Imposta su WIDTH_SYSTEM_DEFAULT per consentire al sistema di determinare la larghezza del divisore.

    • Intervallo di trascinamento:la percentuale minima dello schermo che può occupare ciascun riquadro. Può variare da 0,33 a 0,66. Imposta su DRAG_RANGE_SYSTEM_DEFAULT per consentire al sistema di determinare l'intervallo di trascinamento.

    Kotlin

    val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
    
    if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
        splitAttributesBuilder.setDividerAttributes(
            DividerAttributes.DraggableDividerAttributes.Builder()
                .setColor(getColor(R.color.divider_color))
                .setWidthDp(4)
                .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
                .build()
        )
    }
    val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

    Java

    SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);
    
    if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
        splitAttributesBuilder.setDividerAttributes(
          new DividerAttributes.DraggableDividerAttributes.Builder()
            .setColor(ContextCompat.getColor(this, R.color.divider_color))
            .setWidthDp(4)
            .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
            .build()
        );
    }
    SplitAttributes _splitAttributes = splitAttributesBuilder.build();

Blocco dello stack delle attività

Il blocco della pila di attività consente agli utenti di bloccare una delle finestre divise in modo che l'attività rimanga invariata mentre gli utenti navigano nell'altra finestra. Il blocco dello stack di attività offre un'esperienza di multitasking migliorata.

Per attivare il blocco della pila di attività nella tua app:

  1. Aggiungi un pulsante al file di layout dell'attività che vuoi bloccare, ad esempio l'attività di dettaglio di un layout elenco-dettaglio:

    <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/detailActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/white"
     tools:context=".DetailActivity">
    
    <TextView
       android:id="@+id/textViewItemDetail"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="36sp"
       android:textColor="@color/obsidian"
       app:layout_constraintBottom_toTopOf="@id/pinButton"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
    
    <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/pinButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/pin_this_activity"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. Nel metodo onCreate() dell'attività, imposta un listener onclick sul pulsante:

    Kotlin

    val pinButton: Button = findViewById(R.id.pinButton)
    pinButton.setOnClickListener {
        val splitAttributes: SplitAttributes = SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build()
    
        val pinSplitRule = SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build()
    
        SplitController.getInstance(applicationContext)
            .pinTopActivityStack(taskId, pinSplitRule)
    }

    Java

    Button pinButton = findViewById(R.id.pinButton);
    pinButton.setOnClickListener( (view) -> {
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();
    
        SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();
    
        SplitController.getInstance(getApplicationContext())
            .pinTopActivityStack(getTaskId(), pinSplitRule);
    });

Finestra di dialogo con attenuazione a schermo intero

In genere, le attività oscurano i display per attirare l'attenzione su una finestra di dialogo. Nell'incorporamento dell'attività, entrambi i riquadri della visualizzazione a doppio riquadro devono essere oscurati, non solo il riquadro contenente l'attività che ha aperto la finestra di dialogo, per un'esperienza UI unificata.

Con WindowManager 1.4 e versioni successive, l'intera finestra dell'app si attenua per impostazione predefinita quando si apre una finestra di dialogo (vedi EmbeddingConfiguration.DimAreaBehavior.ON_TASK).

Per oscurare solo il contenitore dell'attività che ha aperto la finestra di dialogo, utilizza EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK.

Estrarre un'attività da una finestra divisa a una finestra intera

Crea una nuova configurazione che mostri l'intera finestra dell'attività secondaria, quindi riavvia l'attività con un intent che si risolve nella stessa istanza.

Controllare il supporto della suddivisione in fase di runtime

L'incorporamento delle attività è supportato su Android 12L (livello API 32) e versioni successive, ma è disponibile anche su alcuni dispositivi con versioni precedenti della piattaforma. Per verificare in fase di runtime la disponibilità della funzionalità, utilizza la proprietà SplitController.splitSupportStatus o il metodo SplitController.getSplitSupportStatus():

Kotlin

if (SplitController.getInstance(this).splitSupportStatus ==
    SplitController.SplitSupportStatus.SPLIT_AVAILABLE
) {
    // Device supports split activity features.
}

Java

if (SplitController.getInstance(this).getSplitSupportStatus() ==
    SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
    // Device supports split activity features.
}

Se le suddivisioni non sono supportate, le attività vengono avviate in cima allo stack di attività (seguendo il modello di incorporamento non attività).

Impedire l'override del sistema

I produttori di dispositivi Android (produttori di apparecchiature originali o OEM) possono implementare l'incorporamento delle attività come funzione del sistema del dispositivo. Il sistema specifica le regole di divisione per le app multi-attività, ignorando il comportamento di ridimensionamento delle finestre delle app. L'override del sistema forza le app multi-attività in una modalità di incorporamento delle attività definita dal sistema.

L'incorporamento dell'attività di sistema può migliorare la presentazione dell'app tramite layout multischermo, come elenco-dettagli, senza alcuna modifica all'app. Tuttavia, l'incorporamento dell'attività di sistema potrebbe anche causare layout errati dell'app, bug o conflitti con l'incorporamento dell'attività implementato dall'app.

La tua app può impedire o consentire l'incorporamento dell'attività di sistema impostando PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE nel file manifest dell'app, ad esempio:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
            android:value="true|false" />
    </application>
</manifest>

Il nome della proprietà è definito nell'oggetto Jetpack WindowManager WindowProperties. Imposta il valore su false se la tua app implementa l'incorporamento di attività o se vuoi impedire al sistema di applicare le regole di incorporamento di attività alla tua app. Imposta il valore su true per consentire al sistema di applicare l'incorporamento di attività definito dal sistema alla tua app.

Limitazioni, restrizioni e avvertenze

  • Solo l'app host dell'attività, identificata come proprietaria dell'attività radice, può organizzare e incorporare altre attività nell'attività. Se le attività che supportano l'incorporamento e la suddivisione vengono eseguite in un'attività che appartiene a un'applicazione diversa, l'incorporamento e la suddivisione non funzioneranno per queste attività.
  • Le attività possono essere organizzate solo all'interno di una singola attività. L'avvio di un'attività in una nuova attività la inserisce sempre in una nuova finestra espansa al di fuori di eventuali divisioni esistenti.
  • Solo le attività nello stesso processo possono essere organizzate e inserite in una suddivisione. Il callback SplitInfo segnala solo le attività che appartengono allo stesso processo, poiché non è possibile conoscere le attività in processi diversi.
  • Ogni regola di attività singola o in coppia si applica solo agli avvii di attività che si verificano dopo la registrazione della regola. Al momento non è possibile aggiornare le suddivisioni esistenti o le loro proprietà visive.
  • La configurazione del filtro per coppie divise deve corrispondere agli intent utilizzati per avviare completamente le attività. La corrispondenza si verifica quando viene avviata una nuova attività dal processo dell'applicazione, quindi potrebbe non conoscere i nomi dei componenti che vengono risolti in un secondo momento nel processo di sistema quando si utilizzano intent impliciti. Se il nome di un componente non è noto al momento del lancio, è possibile utilizzare un carattere jolly ("*/*") e il filtro può essere eseguito in base all'azione intent.
  • Al momento non è possibile spostare le attività tra i contenitori o all'interno e all'esterno delle suddivisioni dopo la loro creazione. Le suddivisioni vengono create solo dalla libreria WindowManager quando vengono avviate nuove attività con regole corrispondenti e vengono eliminate quando l'ultima attività in un contenitore di suddivisione è terminata.
  • Le attività possono essere riavviate quando la configurazione cambia, quindi quando viene creata o rimossa una divisione e i limiti dell'attività cambiano, l'attività può subire la distruzione completa dell'istanza precedente e la creazione di quella nuova. Di conseguenza, gli sviluppatori di app devono prestare attenzione a elementi come l'avvio di nuove attività dai callback del ciclo di vita.
  • I dispositivi devono includere l'interfaccia delle estensioni della finestra per supportare l'incorporamento dell'attività. Quasi tutti i dispositivi con schermo di grandi dimensioni con Android 12L (livello API 32) o versioni successive includono l'interfaccia. Tuttavia, alcuni dispositivi con schermo di grandi dimensioni che non sono in grado di eseguire più attività non includono l'interfaccia delle estensioni della finestra. Se un dispositivo con schermo di grandi dimensioni non supporta la modalità multi-finestra, potrebbe non supportare l'incorporamento delle attività.

Risorse aggiuntive