Incorporación de actividades

La incorporación de actividades optimiza las apps en dispositivos de pantalla grande mediante la división de la ventana de tareas de una aplicación entre dos actividades o dos instancias de la misma actividad.

Figura 1: App de Configuración con actividades en paralelo.

Si tu app consta de varias actividades, incorporarlas te permitirá proporcionar con facilidad una experiencia del usuario mejorada en tablets, dispositivos plegables y dispositivos ChromeOS.

La incorporación de actividades no requiere refactorización de código. Tú determinas cómo la app muestra sus actividades, una al lado de la otra, o apiladas, mediante la creación de un archivo de configuración XML o llamadas a la API de Jetpack WindowManager.

La compatibilidad con pantallas pequeñas se mantiene automáticamente. Cuando tu app está en un dispositivo con una pantalla pequeña, las actividades se apilan una sobre otra. En pantallas grandes, las actividades se muestran una al lado de la otra. El sistema determina la presentación en función de la configuración que creaste: no se requiere lógica de ramificación.

La incorporación de actividades admite cambios de orientación del dispositivo y funciona a la perfección en dispositivos plegables, en los que se modifica la pila de actividades a medida que el dispositivo se pliega y se despliega.

La incorporación de actividades es compatible con la mayoría de los dispositivos con pantalla grande con Android 12L (nivel de API 32) y versiones posteriores.

Ventana de tareas dividida

La incorporación de actividades divide la ventana de tareas de la app en dos contenedores: uno principal y otro secundario. Estos contenedores incluyen las actividades iniciadas desde la actividad principal o desde otras que ya se encuentran en ellos.

Las actividades se apilan en el contenedor secundario a medida que se inician, y este se apila sobre el principal en pantallas pequeñas, por lo que la pila de actividades y la navegación hacia atrás son coherentes con el orden de las actividades ya integradas en tu app.

La incorporación de actividades te permite mostrarlas de varias formas. Tu app puede dividir la ventana de tareas iniciando dos actividades, una al lado de la otra y de manera simultánea:

Figura 2: Dos actividades en paralelo.

Alternativamente, una actividad que ocupa toda la ventana de tareas puede crear una división mediante el lanzamiento de una nueva actividad junto a ella:

Figura 3: La actividad A inicia la actividad B al costado.

Las actividades que ya están divididas y comparten una ventana de tareas pueden iniciar otras actividades de las siguientes maneras:

  • Al costado y sobre otra actividad:

    Figura 4: La actividad A inicia la actividad C al costado y sobre la actividad B.
  • Al costado y desplazando la división hacia un lado, ocultando la actividad principal anterior:

    Figura 5: La actividad B inicia la actividad C al costado y mueve la división hacia un lado.
  • Iniciando una actividad en el lugar, sobre la otra, es decir, en la misma pila de actividades:

    Figura 6: La actividad B inicia la actividad C sin marcas de intents adicionales.
  • Iniciando una ventana de actividad completa en la misma tarea:

    Figura 7: La actividad A o la actividad B inician la actividad C, que ocupa la ventana de tareas por completo.

Navegación hacia atrás

Los diferentes tipos de aplicaciones pueden tener distintas reglas de navegación hacia atrás en un estado de ventana de tareas dividida según las dependencias entre las actividades o la forma en que los usuarios activan el evento Atrás, por ejemplo:

  • En conjunto: Si las actividades están relacionadas y una no se debería mostrar sin la otra, se puede configurar la navegación hacia atrás para finalizar ambas.
  • Por separado: Si las actividades son completamente independientes, la navegación hacia atrás en una actividad no afecta el estado de otra actividad en la ventana de tareas.

El evento Atrás se envía a la última actividad enfocada cuando se usa la navegación con botones.

Para la navegación basada en gestos, haz lo siguiente:

  • Android 14 (nivel de API 34) y versiones anteriores: El evento Atrás se envía a la actividad donde ocurrió el gesto. Cuando los usuarios deslizan el dedo desde el lado izquierdo de la pantalla, el evento Atrás se envía a la actividad en el panel izquierdo de la ventana dividida. Cuando los usuarios deslizan el dedo desde el lado derecho de la pantalla, el evento de atrás se envía a la actividad en el panel derecho.

  • Android 15 (nivel de API 35) y versiones posteriores

    • Cuando se trata de varias actividades de la misma app, el gesto finaliza la actividad superior independientemente de la dirección del deslizamiento, lo que proporciona una experiencia más unificada.

    • En situaciones que involucran dos actividades de diferentes apps (superposición), el evento Atrás se dirige a la última actividad en primer plano, y se alinea con el comportamiento de la navegación con botones.

Diseño multipanel

Jetpack WindowManager te permite compilar un diseño que incorpore actividad de varios paneles en dispositivos con pantalla grande con Android 12L (nivel de API 32) o versiones posteriores, y en algunos dispositivos con versiones de plataforma anteriores. Las apps existentes que se basan en varias actividades en lugar de fragmentos o diseños basados en vistas, como SlidingPaneLayout, pueden brindar una experiencia mejorada del usuario de pantallas grandes sin refactorización del código fuente.

Un ejemplo común es una división de lista-detalles. Para garantizar una presentación de alta calidad, el sistema inicia la actividad de la lista y, luego, la aplicación inicia de inmediato la actividad de los detalles. El sistema de transición espera hasta que se dibujen ambas actividades y, luego, las muestra juntas. Para el usuario, las dos actividades se inician como una sola.

Figura 8: Dos actividades iniciadas simultáneamente en un diseño multipanel

Atributos de división

Puedes especificar cómo se determinará la proporción de la ventana de tareas entre los contenedores divididos y cómo se organizan los contenedores en relación con los demás.

Para las reglas definidas en un archivo de configuración XML, configura los siguientes atributos:

  • splitRatio: Establece las proporciones del contenedor. El valor es un número de punto flotante en el intervalo abierto (0.0, 1.0).
  • splitLayoutDirection: Especifica cómo se organizan los contenedores divididos en relación con los demás. Entre los valores, se incluyen los siguientes:
    • ltr: De izquierda a derecha
    • rtl: De derecha a izquierda
    • locale: Se determina ltr o rtl a partir de la configuración regional

Consulta la sección Configuración de XML para ver ejemplos.

Para las reglas creadas con las APIs de WindowManager, crea un objeto SplitAttributes con SplitAttributes.Builder y llama a los siguientes métodos de compilador:

Consulta la sección API de WindowManager para ver ejemplos.

Figura 9: Dos divisiones de actividad distribuidas de izquierda a derecha, pero con diferentes relaciones de división

Marcadores de posición

Las actividades de marcadores de posición son actividades secundarias vacías que ocupan un área de una división de actividad. En definitiva, se las reemplazará por otra actividad que incluya contenido. Por ejemplo, una actividad de marcador de posición puede ocupar el lado secundario de una división de actividad en un diseño de lista-detalles hasta que se seleccione un elemento de la lista. En ese momento, una actividad que contiene la información de los detalles del elemento de lista seleccionado reemplaza el marcador de posición.

De forma predeterminada, el sistema muestra marcadores de posición solo cuando hay suficiente espacio para dividir actividades. Los marcadores de posición terminan automáticamente cuando el tamaño de la pantalla cambia a un ancho o una altura demasiado pequeños para mostrar una división. Cuando el espacio lo permite, el sistema reinicia el marcador de posición con un estado de nueva inicialización.

Figura 10: Plegado y desplegado de un dispositivo plegable. La actividad del marcador de posición finaliza y se vuelve a crear cuando cambia el tamaño de la pantalla.

Sin embargo, el atributo stickyPlaceholder de un método SplitPlaceholderRule o setSticky() de SplitPlaceholder.Builder puede anular el comportamiento predeterminado. Cuando el atributo o el método especifican un valor de true, el sistema muestra el marcador de posición como la actividad superior en la ventana de tareas cuando el tamaño de la pantalla se reduce de dos paneles a uno solo (consulta Configuración de división para ver un ejemplo).

Figura 11: Plegado y desplegado de un dispositivo plegable. La actividad del marcador de posición es fija.

Cambios en el tamaño de la ventana

Cuando los cambios en la configuración del dispositivo reducen el ancho de la ventana de tareas para que no sea lo suficientemente grande para un diseño de varios paneles (por ejemplo, cuando un dispositivo plegable de pantalla grande se pliega del tamaño de una tablet al de un teléfono o se cambia el tamaño de la ventana de la app en el modo multiventana), las actividades que no son de marcador de posición en el panel secundario de la ventana de tareas se apilan sobre las actividades del panel principal.

Las actividades de marcadores de posición solo se muestran cuando el ancho de la pantalla es el suficiente para realizar una división. En pantallas más pequeñas, el marcador de posición se descarta automáticamente. Cuando el área de visualización vuelve a ser lo bastante grande, se vuelve a crear el marcador de posición. (Consulta la sección Marcadores de posición).

Es posible apilar las actividades porque WindowManager aplica el orden Z a las actividades del panel secundario sobre las actividades del panel principal.

Varias actividades en el panel secundario

La actividad B inicia la actividad C en el lugar, sin marcas de intents adicionales:

División de actividad que contiene las actividades A, B y C, con C apilada sobre B.

Esto da como resultado el siguiente orden Z de las actividades en la misma tarea:

Pila secundaria de actividades que contiene la actividad C apilada sobre B.
          La pila secundaria se apila sobre la principal que contiene la actividad A.

Por lo tanto, en una ventana de tareas más pequeña, la aplicación se reduce a una sola actividad, con C en la parte superior de la pila:

Ventana pequeña que muestra solo la actividad C.

Si vuelves a la ventana más pequeña, podrás navegar por las actividades apiladas unas sobre otras.

Si se restablece la configuración de la ventana de tareas a un tamaño más grande que admite varios paneles, las actividades se volverán a mostrar una al lado de la otra.

Divisiones apiladas

La actividad B inicia la actividad C al costado y mueve la división hacia un lado.

Ventana de tareas que muestra las actividades A y B, y, luego, las actividades B y C.

El resultado es el siguiente orden Z de las actividades en la misma tarea:

Actividades A, B y C en una sola pila. Las actividades se apilan en el siguiente orden de arriba hacia abajo: C, B y A.

En una ventana de tareas más pequeña, la aplicación se reduce a una sola actividad, con C en la parte superior:

Ventana pequeña que muestra solo la actividad C.

Orientación vertical fija

La configuración del manifiesto android:screenOrientation permite a las apps restringir las actividades a la orientación vertical u horizontal. Para mejorar la experiencia del usuario en dispositivos de pantalla grande, como tablets y dispositivos plegables, los fabricantes de dispositivos (OEMs) pueden ignorar las solicitudes de orientación de la pantalla y colocar la app en formato letterbox en orientación vertical en pantallas horizontales u orientación horizontal en pantallas verticales.

Figura 12: Actividades en formato letterbox: orientación vertical fija en dispositivo horizontal (izquierda) y orientación horizontal fija en dispositivo vertical (derecha)

De manera similar, cuando se habilita la incorporación de actividades, los OEMs pueden personalizar los dispositivos para que realicen actividades con formato letterbox y formato vertical fijo en orientación horizontal en pantallas grandes (ancho superior o igual a 600 dp). Cuando una actividad con formato vertical fijo inicia una segunda actividad, el dispositivo puede mostrar ambas actividades en paralelo en una pantalla con dos paneles.

Figura 13: La actividad con orientación vertical fija A inicia la actividad B al costado.

Agrega siempre la propiedad android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED al archivo de manifiesto de tu app para informar a los dispositivos que tu app admite la incorporación de actividades (consulta la sección Configuración de divisiones). Los dispositivos personalizados para OEMs pueden determinar si aplican el formato letterbox a las actividades con orientación vertical fija.

Configuración de divisiones

Las divisiones de actividad se configuran con las reglas de división. Puedes definir las reglas de división en un archivo de configuración XML o mediante llamadas a la API de WindowManager de Jetpack.

En cualquier caso, tu app debe acceder a la biblioteca de WindowManager y debe informar al sistema que la app implementó la incorporación de actividades.

Haz lo siguiente:

  1. Agrega la dependencia más reciente de la biblioteca de WindowManager al archivo build.gradle de nivel de módulo de tu app, por ejemplo:

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

    La biblioteca de WindowManager proporciona todos los componentes necesarios para la incorporación de actividades.

  2. Informa al sistema que tu app implementó la incorporación de actividades.

    Agrega la propiedad android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED al elemento <application> del archivo de manifiesto de la app y establece el valor en "true". Por ejemplo:

    <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>
    

    En WindowManager versión 1.1.0-alpha06 y posteriores, se inhabilitaron las divisiones de incorporación de actividades, a menos que se agregue la propiedad al manifiesto y se configure como "true".

    Además, los fabricantes de dispositivos usan el parámetro de configuración para habilitar capacidades personalizadas para las apps que admiten la incorporación de actividades. Por ejemplo, los dispositivos pueden utilizar formato letterbox en una actividad exclusiva del modo vertical en el modo horizontal para la transición a un diseño de panel dual cuando se inicia una segunda actividad (consulta orientación vertical fija).

Configuración de XML

Para crear una implementación basada en XML de incorporación de actividad, completa los siguientes pasos:

  1. Crea un archivo de recursos XML que realice las siguientes acciones:

    • Definir las actividades que comparten una división
    • Configurar las opciones de división
    • Crea un marcador de posición para el contenedor secundario de la división cuando el contenido no está disponible.
    • Especificar las actividades que nunca deben ser parte de una división

    Por ejemplo:

    <!-- 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. Crear un inicializador.

    El componente RuleController de WindowManager analiza el archivo de configuración XML y pone las reglas a disposición del sistema. Una biblioteca de Startup de Jetpack Initializer pone el archivo XML a disposición de RuleController en el inicio de la app para que se apliquen las reglas cuando se inicie una actividad.

    Para crear un inicializador, haz lo siguiente:

    1. Agrega la dependencia más reciente de la biblioteca de Jetpack Startup al archivo build.gradle de nivel de módulo, por ejemplo:

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

    2. Crea una clase que implemente la interfaz Initializer.

      El inicializador hace que las reglas de división estén disponibles para RuleController pasando el ID del archivo de configuración XML (main_split_config.xml) al método 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 proveedor de contenido para las definiciones de la regla.

    Agrega androidx.startup.InitializationProvider al archivo de manifiesto de tu app como un <provider>. Incluye una referencia a la implementación de tu inicializador de 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 descubre y luego inicializa SplitInitializer antes de que se llame al método onCreate() de la app. Como resultado, las reglas de división están vigentes cuando comienza la actividad principal de la app.

API de WindowManager

Puedes implementar la incorporación de actividades de manera programática con unas pocas llamadas a la API. Realiza las llamadas en el método onCreate() de una subclase de Application para asegurarte de que las reglas estén vigentes antes de que se inicien las actividades.

Para crear una división de actividad de manera programática, haz lo siguiente:

  1. Crea una regla de división:

    1. Crea un SplitPairFilter que identifique las actividades que comparten la división:

      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. Agrega el filtro a un conjunto de filtros:

      Kotlin

      val filterSet = setOf(splitPairFilter)

      Java

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
    3. Crea atributos de diseño para la división:

      Kotlin

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

      Java

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

      SplitAttributes.Builder crea un objeto que contiene atributos de diseño:

      • setSplitType(): Define cómo se asigna el área de visualización disponible a cada contenedor de actividades. El tipo de división de proporción especifica la proporción del área de visualización disponible asignada al contenedor principal. El contenedor secundario ocupa el resto del área de visualización disponible.
      • setLayoutDirection(): Especifica cómo se distribuyen los contenedores de actividades uno respecto del otro (el contenedor principal se distribuye primero).
    4. Compila una 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 y configura la regla:

      • filterSet: Contiene filtros de par dividido que determinan cuándo aplicar la regla mediante la identificación de actividades que comparten una división.
      • setDefaultSplitAttributes(): Aplica los atributos de diseño a la regla.
      • setMinWidthDp(): Establece el ancho de pantalla mínimo (en píxeles independientes de la densidad, dp) que permite una división.
      • setMinSmallestWidthDp(): Establece el valor mínimo (en dp) que debe tener la menor de las dos dimensiones de pantalla para permitir una división, independientemente de la orientación del dispositivo.
      • setMaxAspectRatioInPortrait(): Establece la relación de aspecto máxima de la pantalla (altura:ancho) en orientación vertical para la que se muestran las divisiones de actividad. Si la relación de aspecto de una pantalla vertical supera la relación de aspecto máxima, se inhabilitan las divisiones, independientemente del ancho de la pantalla. Nota: El valor predeterminado es 1.4, lo que da como resultado actividades que ocupan toda la ventana de tareas en orientación vertical en la mayoría de las tablets. Consulta también SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT y setMaxAspectRatioInLandscape(). El valor predeterminado para la orientación horizontal es ALWAYS_ALLOW.
      • setFinishPrimaryWithSecondary(): Establece cómo el hecho de finalizar todas las actividades del contenedor secundario afecta las actividades del contenedor principal. NEVER indica que el sistema no debe finalizar las actividades principales cuando terminan todas las actividades del contenedor secundario (consulta Finalizar actividades).
      • setFinishSecondaryWithPrimary(): Establece cómo el hecho de finalizar todas las actividades del contenedor principal afecta las actividades del contenedor secundario. ALWAYS indica que el sistema siempre debe finalizar las actividades del contenedor secundario cuando terminan todas las actividades del contenedor principal (consulta Finalizar actividades).
      • setClearTop(): Especifica si todas las actividades del contenedor secundario finalizan cuando se inicia una nueva actividad en el contenedor. Un valor false especifica que las actividades nuevas se apilan sobre las que ya se encuentran en el contenedor secundario.
    5. Obtén la instancia singleton de WindowManager RuleController y agrega la regla:

      Kotlin

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

      Java

        RuleController ruleController = RuleController.getInstance(this);
        ruleController.addRule(splitPairRule);
        
  2. Crea un marcador de posición para el contenedor secundario cuando el contenido no esté disponible:

    1. Crea un ActivityFilter que identifique la actividad con la que el marcador de posición comparte la ventana de tareas dividida:

      Kotlin

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

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );
    2. Agrega el filtro a un conjunto de filtros:

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);
    3. 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(context, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();

      SplitPlaceholderRule.Builder crea y configura la regla:

      • placeholderActivityFilterSet: Contiene filtros de actividad que determinan cuándo aplicar la regla mediante la identificación de actividades con las que se asocia la actividad del marcador de posición.
      • Intent: Especifica el inicio de la actividad del marcador de posición.
      • setDefaultSplitAttributes(): Aplica los atributos de diseño a la regla.
      • setMinWidthDp(): Establece el ancho de pantalla mínimo (en píxeles independientes de la densidad, dp) que permite una división.
      • setMinSmallestWidthDp(): Establece el valor mínimo (en dp) que debe tener la menor de las dos dimensiones de pantalla para permitir una división, independientemente de la orientación del dispositivo.
      • setMaxAspectRatioInPortrait(): Establece la relación de aspecto máxima de la pantalla (altura:ancho) en orientación vertical para la que se muestran las divisiones de actividad. Nota: El valor predeterminado es 1.4, lo que da como resultado actividades que ocupan toda la ventana de tareas en orientación vertical en la mayoría de las tablets. Consulta también SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT y setMaxAspectRatioInLandscape(). El valor predeterminado para la orientación horizontal es ALWAYS_ALLOW.
      • setFinishPrimaryWithPlaceholder(): Establece cómo el hecho de finalizar la actividad del marcador de posición afecta a las actividades del contenedor principal. SIEMPRE indica que el sistema siempre debe finalizar las actividades del contenedor principal cuando termina el marcador de posición (consulta Finalizar actividades).
      • setSticky(): Determina si la actividad del marcador de posición aparecerá sobre la pila de actividades en pantallas pequeñas una vez que este aparezca por primera vez en una división con suficiente ancho mínimo.
    4. Agrega la regla a RuleController de WindowManager:

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);
  3. Especifica las actividades que nunca deben ser parte de una división:

    1. Crea un ActivityFilter que identifique una actividad que siempre debe ocupar toda el área de visualización de la tarea:

      Kotlin

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

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
        new ComponentName(this, ExpandedActivity.class),
        null
      );
    2. Agrega el filtro a un conjunto de filtros:

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);
    3. Crea una ActivityRule:

      Kotlin

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

      Java

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

      ActivityRule.Builder crea y configura la regla:

      • expandedActivityFilterSet: Contiene filtros de actividad que determinan cuándo aplicar la regla mediante la identificación de actividades que deseas excluir de las divisiones.
      • setAlwaysExpand(): Especifica si la actividad debe ocupar toda la ventana de tareas.
    4. Agrega la regla a RuleController de WindowManager:

      Kotlin

      ruleController.addRule(activityRule)

      Java

      ruleController.addRule(activityRule);

Incorporación en varias aplicaciones

En Android 13 (nivel de API 33) y versiones posteriores, las apps pueden incorporar actividades de otras apps. La incorporación de actividades entre apps o UID permite la integración visual de actividades de varias aplicaciones para Android. El sistema muestra una actividad de la app host y una actividad incorporada de otra app en pantalla lado a lado o en las partes superior e inferior como en la incorporación de actividades de una app individual.

Por ejemplo, la app de Configuración podría incorporar la actividad del selector de fondo de pantalla de la app de WallpaperPicker:

Figura 14: App de Configuración (menú de la izquierda) con el selector de fondo de pantalla como actividad incorporada (derecha)

Modelo de confianza

Los procesos de host que incorporan actividades de otras apps pueden redefinir la presentación de las actividades incorporadas, incluidos el tamaño, la posición, el recorte y la transparencia. Los hosts maliciosos pueden usar esta función para engañar a los usuarios y crear una captura de clic u otros ataques de compensación de IU.

Para evitar el uso inadecuado de la incorporación de actividades entre apps, Android requiere que las apps acepten habilitar la incorporación de sus actividades. Las apps pueden designar hosts como confiables o no confiables.

Hosts de confianza

Para permitir que otras aplicaciones incorporen y controlen por completo la presentación de actividades desde tu app, especifica el certificado SHA-256 de la aplicación host en el atributo android:knownActivityEmbeddingCerts de <activity> o los elementos <application> del archivo de manifiesto de tu app.

Configura el valor de android:knownActivityEmbeddingCerts como una cadena:

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

O, para especificar varios certificados, un array de cadenas:

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

Que hace referencia a un recurso como el siguiente:

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

Para obtener un resumen de certificados SHA, los propietarios de la app pueden ejecutar la tarea signingReport de Gradle. El resumen del certificado es la huella digital SHA-256 sin los dos puntos separados. Para obtener más información, consulta Cómo ejecutar un informe de firma y Autenticación de tu cliente.

Hosts no confiables

Para permitir que cualquier app incorpore las actividades de tu app y controle su presentación, especifica el atributo android:allowUntrustedActivityEmbedding en los elementos <activity> o <application> del manifiesto de la app, por ejemplo:

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

El valor predeterminado del atributo es "false", lo cual impide que se incorpore la actividad en diferentes apps.

Autenticación personalizada

Para mitigar los riesgos de incorporación de actividad no confiable, crea un mecanismo de autenticación personalizado que verifique la identidad del host. Si conoces los certificados del host, usa la biblioteca androidx.security.app.authenticator para la autenticación. Si el host hace la autenticación después de incorporar tu actividad, puedes mostrar el contenido real. De lo contrario, puedes informarle al usuario que no se permitió la acción y bloquear el contenido.

Usa el método ActivityEmbeddingController#isActivityEmbedded() de la biblioteca de WindowManager de Jetpack para verificar si un host incorpora tu actividad, por ejemplo:

Kotlin

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

Java

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

Restricción de tamaño mínimo

El sistema Android aplica la altura y el ancho mínimos especificados en el elemento <layout> del manifiesto de la app a las actividades incorporadas. Si una aplicación no especifica la altura y el ancho mínimos, se aplican los valores predeterminados del sistema (sw220dp).

Si el host intenta cambiar el tamaño del contenedor incorporado a un tamaño inferior al mínimo, el contenedor incorporado se expande para ocupar todos los límites de la tarea.

<activity-alias>

Para que la incorporación de actividades confiables o no confiables funcione con el elemento <activity-alias>, se deben aplicar android:knownActivityEmbeddingCerts o android:allowUntrustedActivityEmbedding a la actividad objetivo, en lugar del alias. La política que verifica la seguridad en el servidor del sistema se basa en las marcas establecidas en el objetivo, no en el alias.

Aplicación de host

Las aplicaciones de host implementan la incorporación de actividades entre apps de la misma manera en que implementan la incorporación de actividades en una sola app. Los objetos SplitPairRule y SplitPairFilter o ActivityRule y ActivityFilter especifican actividades incorporadas y divisiones de ventanas de tareas. Las reglas de división se definen de manera estática en XML o en el tiempo de ejecución mediante las llamadas a la API de WindowManager de Jetpack.

Si una aplicación de host intenta incorporar una actividad que no habilitó la incorporación entre apps, la actividad ocupará todos los límites de la tarea. Como resultado, las aplicaciones de host deben saber si las actividades de destino permiten la incorporación entre apps.

Si una actividad incorporada inicia una nueva actividad en la misma tarea y la nueva actividad no habilitó la incorporación entre apps, la actividad ocupará todos los límites de la tarea en lugar de superponerse en el contenedor incorporado.

Una aplicación de host puede incorporar sus propias actividades sin restricciones, siempre que las actividades se inicien en la misma tarea.

Ejemplos de divisiones

División desde la ventana completa

Figura 15: La actividad A inicia la actividad B al costado.

No es necesario refactorizar. Puedes definir la configuración de la división de manera estática o en el tiempo de ejecución y, luego, llamar a Context#startActivity() sin ningún parámetro adicional.

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

División predeterminada

Cuando la página de destino de una aplicación está diseñada para dividirse en dos contenedores en pantallas grandes, la experiencia del usuario resulta óptima si ambas actividades se crean y se presentan de forma simultánea. Sin embargo, es posible que el contenido no esté disponible para el contenedor secundario de la división hasta que el usuario interactúe con la actividad en el contenedor principal (por ejemplo, cuando el usuario seleccione un elemento de un menú de navegación). Una actividad de marcador de posición puede llenar el vacío hasta que el contenido se pueda mostrar en el contenedor secundario de la división (consulta la sección Marcadores de posición).

Figura 16: División que se crea mediante la apertura simultánea de dos actividades. Una actividad es un marcador de posición.

Para crear una división con un marcador de posición, crea un marcador de posición y asócialo con la actividad principal:

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

Cuando una app recibe un intent, la actividad objetivo se puede mostrar como la parte secundaria de una división de actividad (por ejemplo, una solicitud para mostrar una pantalla de detalles con información sobre un elemento de una lista). En pantallas pequeñas, el detalle se muestra en toda la ventana de tareas. En dispositivos más grandes, se muestra junto a la lista.

Figura 17: Actividad de detalles de vínculos directos que se muestra sola en una pantalla pequeña, pero junto con una actividad de lista en una pantalla grande.

La solicitud de inicio se debe enrutar a la actividad principal, y la actividad objetivo de detalles debe iniciarse en una división. El sistema elige automáticamente la presentación correcta (apilada o en paralelo) según el ancho de la pantalla disponible.

Kotlin

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

Java

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

El destino del vínculo directo puede ser la única actividad que debe estar disponible para el usuario en la pila de navegación hacia atrás, y te recomendamos que evites descartar la actividad de detalles y dejar solo la actividad principal:

Pantalla grande con la actividad de lista y la actividad de detalles en paralelo.
          La navegación hacia atrás no puede descartar la actividad de detalles y dejar la actividad de lista en pantalla.

Pantalla pequeña solo con la actividad de detalles. La navegación hacia atrás no puede descartar la actividad de detalles y mostrar la actividad de lista.

En su lugar, puedes finalizar ambas actividades al mismo tiempo mediante el atributo finishPrimaryWithSecondary:

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

Consulta la sección Atributos de configuración.

Varias actividades en contenedores divididos

Apilar varias actividades en un contenedor dividido permite a los usuarios acceder a contenido específico. Por ejemplo, con una división de lista-detalles, es posible que el usuario necesite ir a una sección de detalles secundarios, pero mantener la actividad principal en su lugar:

Figura 18: Actividad abierta en el lugar, en el panel secundario de la ventana de tareas.

Kotlin

class DetailActivity {
    . . .
    fun onOpenSubDetail() {
      startActivity(Intent(this, SubDetailActivity::class.java))
    }
}

Java

public class DetailActivity {
    . . .
    void onOpenSubDetail() {
        startActivity(new Intent(this, SubDetailActivity.class));
    }
}

La actividad de detalles secundarios se coloca sobre la actividad de detalles, y la oculta:

Luego, el usuario puede regresar al nivel anterior de detalles navegando hacia atrás en la pila:

Figura 19: Se quitó la actividad de la parte superior de la pila.

El apilado de actividades unas sobre otras es el comportamiento predeterminado cuando estas se inician desde una que se encuentra en el mismo contenedor secundario. Las actividades iniciadas desde el contenedor principal dentro de una división activa también terminan en el contenedor secundario en la parte superior de la pila de actividades.

Actividades en una tarea nueva

Cuando las actividades en una ventana dividida de tareas inician actividades en una tarea nueva, esta última es independiente de la que incluye la división y se muestra en una ventana completa. En la pantalla Recents, se muestran dos tareas: la que está en la división y la nueva.

Figura 20: Se inicia la actividad C en una tarea nueva desde la actividad B.

Reemplazo de actividades

Las actividades se pueden reemplazar en la pila del contenedor secundario (por ejemplo, cuando se usa la actividad principal para la navegación de nivel superior y la actividad secundaria es un destino seleccionado). Cada selección de la navegación de nivel superior debe iniciar una actividad nueva en el contenedor secundario y quitar las actividades que antes estaban allí.

Figura 21: La actividad de navegación de nivel superior del panel principal reemplaza las actividades de destino del panel secundario.

La navegación hacia atrás puede resultar confusa cuando se contrae la división (cuando se pliega el dispositivo) si la app no finaliza la actividad en el contenedor secundario cuando cambia la selección de navegación. Por ejemplo, si tienes un menú en el panel principal y las pantallas A y B apiladas en el panel secundario, cuando el usuario pliegue el teléfono, B estará sobre A, y A estará sobre el menú. Cuando el usuario navegue hacia atrás desde B, aparecerá A en lugar del menú.

En esos casos, se debe quitar la pantalla A de la pila de actividades.

El comportamiento predeterminado cuando se realiza un inicio lateral en un contenedor nuevo sobre una división existente es colocar los nuevos contenedores secundarios en la parte superior y conservar los antiguos en la pila de actividades. Puedes configurar las divisiones de modo que se borren los contenedores secundarios anteriores mediante clearTop y que se inicien las actividades nuevas con normalidad.

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

Kotlin

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

Java

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

Como alternativa, usa la misma actividad secundaria y, desde la actividad principal (el menú), envía nuevos intents que se resuelvan en la misma instancia, pero que activen un estado o una actualización de la IU en el contenedor secundario.

Divisiones múltiples

Las apps pueden proporcionar navegación profunda de varios niveles iniciando actividades adicionales a un lado.

Cuando una actividad en un contenedor secundario inicia una actividad nueva a un costado, se crea una división nueva sobre la existente.

Figura 22: La actividad B inicia la actividad C al costado.

La pila de actividades contiene todas las actividades que se abrieron antes, por lo que los usuarios pueden navegar a la división A/B después de finalizar C.

Actividades A, B y C en una pila. Las actividades se apilan en el siguiente orden de arriba hacia abajo: C, B y A.

Para crear una nueva división, inicia la actividad nueva al lado del contenedor secundario existente. Declara las configuraciones para las divisiones A/B y B/C, e inicia la actividad C normalmente desde B:

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

Kotlin

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

Java

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

Cómo reaccionar a los cambios de estado de divisiones

Las diferentes actividades de una app pueden tener elementos de la IU que realizan la misma función (por ejemplo, un control que abre una ventana que contiene la configuración de la cuenta).

Figura 23: Diferentes actividades con elementos de la IU funcionalmente idénticos.

Si dos actividades que tienen un elemento de la IU en común están en una división, mostrar el elemento en ambas actividades resultará redundante y podría generar confusión.

Figura 24: Elementos de la IU duplicados en la división de actividad.

Para saber cuándo las actividades están en una división, verifica el flujo de SplitController.splitInfoList o registra un objeto de escucha con SplitControllerCallbackAdapter para ver los cambios en el estado de la división. Luego, ajusta la IU según corresponda:

Kotlin

val layout = layoutInflater.inflate(R.layout.activity_main, null)
val view = layout.findViewById<View>(R.id.infoButton)
lifecycleScope.launch {
    lifecycle.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) {
    . . .
    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);
            });
}

Las corrutinas se pueden iniciar en cualquier estado del ciclo de vida, pero, por lo general, se inician en el estado STARTED para conservar recursos (consulta Cómo usar corrutinas de Kotlin con componentes optimizados para ciclos de vida para obtener más información).

Las devoluciones de llamada se pueden realizar en cualquier estado del ciclo de vida, incluso cuando se detiene una actividad. Por lo general, los objetos de escucha deben estar registrados en onStart() y no deben registrarse en onStop().

Ventana modal completa

Algunas actividades bloquean a los usuarios para que no interactúen con la aplicación hasta que se realice una acción especificada (por ejemplo, una actividad de la pantalla de acceso, la pantalla de confirmación de la política o un mensaje de error). Se debe evitar que las actividades modales aparezcan en una división.

Una actividad puede verse forzada a llenar siempre la ventana de tareas mediante la configuración de expansión:

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

Cómo finalizar actividades

Los usuarios pueden finalizar las actividades en cualquiera de los lados de la división si deslizan el dedo desde el borde de la pantalla:

Figura 25. Gesto de deslizar el dedo para finalizar la actividad B.
Figura 26. Gesto de deslizar el dedo para finalizar la actividad A.

Si el dispositivo está configurado a fin de usar el botón Atrás en lugar de la navegación por gestos, la entrada se envía a la actividad enfocada, es decir, aquella que se tocó o se inició por última vez.

Cuando se terminan todas las actividades en un contenedor, el efecto en el contenedor opuesto dependerá de la configuración de la división.

Atributos de configuración

Puedes especificar atributos de reglas de vinculación de divisiones para configurar cómo el hecho de finalizar todas las actividades en un lado de la división afecta las actividades del otro lado. Los atributos son los siguientes:

  • window:finishPrimaryWithSecondary: Indica cómo el hecho de finalizar todas las actividades del contenedor secundario afecta las actividades del contenedor principal.
  • window:finishSecondaryWithPrimary: Indica cómo el hecho de finalizar todas las actividades del contenedor principal afecta las actividades del contenedor secundario.

Entre los valores posibles de los atributos, se incluyen los siguientes:

  • always: Siempre finaliza las actividades en el contenedor asociado.
  • never: Nunca finaliza las actividades en el contenedor asociado.
  • adjacent: Finaliza las actividades en el contenedor asociado cuando ambos contenedores se muestran uno al lado del otro, pero no cuando están apilados.

Por ejemplo:

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

Configuración predeterminada

Cuando terminan todas las actividades de un contenedor de una división, el contenedor restante ocupa toda la ventana:

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

División que contiene las actividades A y B. A finalizó, y toda la ventana queda ocupada por B.

División que contiene las actividades A y B. B finalizó y toda la ventana queda ocupada por A.

Cómo finalizar actividades al mismo tiempo

Finaliza automáticamente las actividades en el contenedor principal cuando finalicen todas las actividades en el contenedor secundario:

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

División que contiene las actividades A y B. B finalizó, lo que también finalizó A, y la ventana de tareas queda vacía.

División que contiene las actividades A y B. A finalizó, y solo B queda en la ventana de tareas.

Finaliza automáticamente las actividades en el contenedor secundario cuando finalicen todas las actividades en el contenedor principal:

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

División que contiene las actividades A y B. A finalizó, lo que también finalizó B, y la ventana de tareas queda vacía.

División que contiene las actividades A y B. B finalizó, y solo A queda en la ventana de tareas.

Finaliza las actividades juntas cuando finalicen todas las actividades del contenedor principal o secundario:

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

División que contiene las actividades A y B. A finalizó, lo que también finalizó B, y la ventana de tareas queda vacía.

División que contiene las actividades A y B. B finalizó, lo que también finalizó A, y la ventana de tareas queda vacía.

Cómo finalizar varias actividades en contenedores

Si se apilan varias actividades en un contenedor dividido, finalizar una de las que esté en la parte inferior de la pila no finalizará automáticamente aquellas que se encuentren en la parte superior.

Por ejemplo, si dos actividades están en el contenedor secundario, y C está sobre B:

La pila de actividades secundaria que contiene la actividad C sobre B se apila sobre la principal que contiene la actividad A.

La configuración de la división se define según la configuración de las actividades A y B:

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

Cuando termina la actividad superior, se conserva la división.

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. C finaliza y deja A y B en la división de actividad.

Finalizar la actividad inferior (raíz) del contenedor secundario no quita las actividades sobre ella. De esta manera, se conserva la división.

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. B finaliza y deja A y C en la división de actividad.

También se ejecutan las reglas adicionales para finalizar actividades al mismo tiempo, como finalizar la actividad secundaria con la principal:

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

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. A finaliza, lo que también finaliza B y C.

Cuando la división se configura para finalizar la actividad principal y la secundaria al mismo tiempo, se ve lo siguiente:

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

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. C finaliza y deja A y B en la división de actividad.

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. B finaliza y deja A y C en la división de actividad.

División con la actividad A en el contenedor principal y las actividades B y C en el secundario, con C apilada sobre B. A finaliza, lo que también finaliza B y C.

Cómo cambiar las propiedades de las divisiones durante el tiempo de ejecución

No se pueden cambiar las propiedades de una división activa y visible. El cambio en las reglas de división afecta los inicios de actividades adicionales y los contenedores nuevos, pero no las divisiones existentes y activas.

Para cambiar las propiedades de las divisiones activas, finaliza las actividades laterales o presentes en la división y vuelve a iniciarlas al lado con una configuración nueva.

Propiedades de división dinámicas

Android 15 (nivel de API 35) y versiones posteriores compatibles con WindowManager 1.4 de Jetpack y versiones posteriores ofrecen funciones dinámicas que permiten la configurabilidad de las divisiones de incorporación de actividades, incluidas las siguientes:

  • Expansión de paneles: Un divisor interactivo y arrastrable permite a los usuarios cambiar el tamaño de los paneles en una presentación dividida.
  • Fijado de la pila de actividades: Los usuarios pueden fijar el contenido en un contenedor y aislar la navegación en el contenedor de la navegación en el otro contenedor.
  • Atenuación de pantalla completa del diálogo: Cuando se muestra un diálogo, las apps pueden especificar si se atenúa toda la ventana de tareas o solo el contenedor que abrió el diálogo.

Expansión del panel

La expansión de paneles permite a los usuarios ajustar la cantidad de espacio de pantalla asignado a las dos actividades en un diseño de panel doble.

Para personalizar el aspecto del divisor de ventanas y establecer su rango de arrastre, haz lo siguiente:

  1. Crea una instancia de DividerAttributes

  2. Personaliza los atributos del divisor:

    • color: Es el color del separador de paneles arrastrable.

    • widthDp: Es el ancho del separador de paneles que se puede arrastrar. Establece el valor en WIDTH_SYSTEM_DEFAULT para permitir que el sistema determine el ancho del divisor.

    • Rango de arrastre: Es el porcentaje mínimo de la pantalla que puede ocupar cualquiera de los paneles. Puede variar de 0.33 a 0.66. Establece el valor en DRAG_RANGE_SYSTEM_DEFAULT para permitir que el sistema determine el rango de arrastre.

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(context, 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(context, R.color.divider_color))
        .setWidthDp(4)
        .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
        .build()
    );
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();

Fijación de la pila de actividades

La fijación de la pila de actividades permite a los usuarios fijar una de las ventanas divididas para que la actividad permanezca como está mientras los usuarios navegan en la otra. La fijación de pilas de actividades proporciona una experiencia de multitarea mejorada.

Para habilitar el anclaje de pila de actividades en tu app, haz lo siguiente:

  1. Agrega un botón al archivo de diseño de la actividad que deseas fijar, por ejemplo, la actividad de detalles de un diseño de lista-detalles:

    <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. En el método onCreate() de la actividad, establece un objeto de escucha onclick en el botón:

    Kotlin

    pinButton = 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);
    });

Atenuación del diálogo de pantalla completa

Por lo general, las actividades atenúan sus pantallas para llamar la atención a un diálogo. En la incorporación de actividades, se deben atenuar ambos paneles de la pantalla de dos paneles, no solo el panel que contiene la actividad que abrió el diálogo, para obtener una experiencia de IU unificada.

Con WindowManager 1.4 y versiones posteriores, toda la ventana de la app se atenúa de forma predeterminada cuando se abre un diálogo (consulta EmbeddingConfiguration.DimAreaBehavior.ON_TASK).

Para atenuar solo el contenedor de la actividad que abrió el diálogo, usa EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK.

Cómo extraer una actividad de una división a una ventana completa

Crea una nueva configuración que muestre la ventana completa de la actividad lateral y, luego, reinicia la actividad con un intent que se resuelva en la misma instancia.

Cómo comprobar la compatibilidad con las divisiones durante el tiempo de ejecución

La incorporación de actividades es compatible con Android 12L (nivel de API 32) y versiones posteriores, pero también está disponible en algunos dispositivos con versiones anteriores de la plataforma. Para comprobar la disponibilidad de la función durante el tiempo de ejecución, usa la propiedad SplitController.splitSupportStatus o el método 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.
}

Si no se admiten las divisiones, las actividades se iniciarán en la parte superior de la pila de actividades (como en el modelo de incorporación sin actividad).

Cómo evitar la anulación del sistema

Los fabricantes de dispositivos Android (fabricantes de equipos originales o OEMs) pueden implementar la incorporación de actividades como una función del sistema del dispositivo. El sistema especifica las reglas de división para las apps de varias actividades, lo que anula el comportamiento del sistema de ventanas de las apps. La anulación del sistema fuerza las apps de varias actividades a un modo de incorporación de actividad definido por el sistema.

La incorporación de la actividad del sistema puede mejorar la presentación de la app a través de diseños de varios paneles, como list-detail, sin ningún cambio en la app. Sin embargo, la incorporación de la actividad del sistema también puede causar errores de diseño, otros errores o conflictos con la incorporación de actividades que implementa la app.

Para evitar o permitir la incorporación de actividades del sistema, configura una propiedad en el archivo de manifiesto de la app, por ejemplo:

<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>

El nombre de la propiedad se define en el objeto WindowProperties de Jetpack WindowManager. Establece el valor en false si tu app implementa la incorporación de actividades o si quieres que el sistema no aplique sus reglas de incorporación de actividades a tu app. Establece el valor en true para permitir que el sistema aplique la incorporación de actividad definida por el sistema a tu app.

Limitaciones, restricciones y advertencias

  • Solo la app host de la tarea, que se identifica como propietaria de la actividad raíz de esta, puede organizar e incorporar otras actividades en la tarea. Si las actividades que admiten incorporación y divisiones se ejecutan en una tarea que pertenece a una aplicación diferente, la incorporación y las divisiones no funcionarán para esas actividades.
  • Las actividades solo se pueden organizar dentro de una misma tarea. Iniciar una actividad en una tarea nueva siempre la coloca en una ventana expandida nueva fuera de cualquier división existente.
  • Solo se pueden organizar y dividir las actividades que formen parte de un mismo proceso. La devolución de llamada SplitInfo solo informa actividades que pertenecen al mismo proceso, ya que no hay manera de conocer aquellas que existan en diferentes procesos.
  • Cada regla de actividad individual o de vinculación se aplica solo a los inicios de actividad que ocurran después de que se registre la regla. Por el momento, no hay forma de actualizar las divisiones existentes ni sus propiedades visuales.
  • La configuración del filtro de vinculación de divisiones debe coincidir con los intents que se usan cuando se inician actividades por completo. La coincidencia se produce cuando se inicia una actividad nueva desde el proceso de la aplicación, por lo que es posible que no conozcas los nombres de los componentes que se resuelven más adelante en el proceso del sistema cuando se usan intents implícitos. Si no se conoce el nombre de un componente al momento del inicio, se puede usar un comodín ("*/*") y realizar el filtrado según la acción de intent.
  • Por el momento, no hay forma de mover las actividades entre contenedores ni dentro o fuera de las divisiones después de crearlas. La biblioteca WindowManager solo crea las divisiones cuando se inician actividades nuevas con reglas que coincidan, y las divisiones se destruyen cuando finaliza la última actividad de un contenedor de divisiones.
  • Las actividades pueden volver a iniciarse cuando cambia la configuración, de modo que, cuando se crea o quita una división y cambian los límites de la actividad, esta puede pasar por la destrucción completa de la instancia anterior y la creación de la nueva. Como resultado, los desarrolladores de apps deberían tener cuidado con cuestiones como el inicio de actividades nuevas a partir de devoluciones de llamada de ciclo de vida.
  • Los dispositivos deben incluir la interfaz de extensiones de ventana para admitir la incorporación de actividades. Casi todos los dispositivos de pantalla grande con Android 12L (nivel de API 32) o versiones posteriores incluyen la interfaz. Sin embargo, algunos dispositivos de pantalla grande que no pueden ejecutar varias actividades no incluyen la interfaz de extensiones de ventana. Si un dispositivo de pantalla grande no admite el modo multiventana, es posible que no admita la incorporación de actividades.

Recursos adicionales