Incorporación de actividades avanzada

1. Introducción

La incorporación de actividades, que se introducen en Android 12L (nivel de API 32), permite que las apps que se basan en actividades muestren varias de ellas simultáneamente en pantallas grandes para crear diseños de dos paneles, como lista-detalles.

En el codelab Cómo compilar un diseño de lista-detalles con incorporación de actividades y Material Design, se explicó cómo usar llamadas a la API de XML o Jetpack WindowManager para crear un diseño de lista-detalles.

En este codelab, se te guiará a través de algunas funciones recién lanzadas para la incorporación de actividades, que mejoran aún más la experiencia en tu app en dispositivos con pantallas grandes. Las funciones incluyen la expansión de paneles, la fijación de actividades y la atenuación de diálogos en pantalla completa.

Requisitos previos

Qué aprenderás

Aprenderás a hacer lo siguiente:

  • Habilitar la expansión del panel
  • Implementar la fijación de actividades con una de las ventanas divididas
  • Usar la atenuación del diálogo de pantalla completa

Requisitos

  • Versión reciente de Android Studio
  • Emulador o teléfono Android con Android 15
  • Emulador o tablet grande de Android con un ancho más pequeño superior a 600 dp

2. Configuración

Obtén la app de ejemplo

Paso 1: Clona el repositorio

Clona el repositorio de Git de codelabs de pantallas grandes:

git clone https://github.com/android/large-screen-codelabs

Otra opción es descargar y desarchivar el archivo ZIP de codelabs de pantallas grandes:

Descargar el código fuente

Paso 2: Inspecciona los archivos fuente del codelab

Navega a la carpeta activity-embedding-advanced.

Paso 3: Abre el proyecto del codelab

En Android Studio, abre el proyecto de Kotlin o Java.

Lista de archivos de la carpeta de actividades en el repositorio y el archivo ZIP.

La carpeta activity-embedding-advanced en el repositorio y el archivo ZIP contiene dos proyectos de Android Studio: uno en Kotlin y otro en Java. Abre el proyecto que prefieras. Los fragmentos de Codelab se proporcionan en ambos lenguajes.

Crea dispositivos virtuales

Si no tienes un teléfono Android, una tablet pequeña o una grande con un nivel de API 35 o superior, abre el Administrador de dispositivos en Android Studio y crea cualquiera de los siguientes dispositivos virtuales que necesites:

  • Teléfono: Pixel 8, nivel de API 35 o superior
  • Tablet: Pixel Tablet, nivel de API 35 o superior

3. Ejecuta la app

La app de ejemplo muestra una lista de elementos. Cuando el usuario selecciona un elemento, la app muestra información sobre este.

La app consta de tres actividades:

  • ListActivity: Contiene una lista de elementos en un RecyclerView.
  • DetailActivity: Muestra información sobre un elemento de lista cuando el elemento se selecciona de esta.
  • SummaryActivity: Muestra un resumen de la información cuando se selecciona el elemento de la lista de resumen.

Continuar desde el codelab anterior

En el codelab Cómo compilar un diseño de lista-detalles con incorporación de actividades y Material Design, desarrollamos una aplicación con una vista de lista-detalles que usa la incorporación de actividades con una navegación facilitada por un riel de navegación y una barra de navegación inferior.

  1. Ejecuta la app en una tablet grande o en el emulador de Pixel en modo vertical. Verás la pantalla de la lista principal y una barra de navegación en la parte inferior.

74906232acad76f.png

  1. Gira la tablet hacia un lado (horizontal). La pantalla debería dividirse y mostrar la lista en un lado y los detalles en el otro. La barra de navegación de la parte inferior debe reemplazarse por un riel de navegación vertical.

dc6a7d1c02c49cd4.png

Nuevas funciones con incorporación de actividades

¿Todo listo para mejorar tu diseño de panel doble? En este codelab, agregaremos algunas nuevas e interesantes funciones para mejorar la experiencia de tus usuarios. Esto es lo que compilaremos:

  1. Haremos que los paneles sean dinámicos. Implementaremos la expansión de paneles, lo que les permitirá a los usuarios cambiar el tamaño (o expandir) los paneles para obtener una vista personalizada.

2ec5f7fd6df5d8cd.gif

  1. Permite que tus usuarios establezcan prioridades. Con la fijación de actividades, los usuarios pueden mantener sus tareas más importantes siempre en la pantalla.

980d0033972737ed.gif

  1. ¿Necesitas concentrarte en una tarea específica? Agregaremos una función para atenuar la pantalla completa para atenuar suavemente las distracciones y permitir que los usuarios se concentren en lo que más importa.

2d3455e0f8901f95.png

4. Expansión del panel

Cuando se usa un diseño de panel doble en una pantalla grande, en muchos casos los usuarios deben enfocarse en uno de los paneles divididos y mantener el otro en la pantalla, por ejemplo, leer artículos en un lado y mantener una lista de conversaciones de chat en el otro. Es común que los usuarios quieran cambiar el tamaño de los paneles para enfocarse en una actividad.

Para lograr este objetivo, la incorporación de actividades agrega una nueva API que te permite darles a los usuarios la posibilidad de cambiar la proporción de división y personalizar la transición de cambio de tamaño.

Cómo agregar una dependencia

Primero, agrega WindowManager 1.4 a tu archivo build.gradle.

Nota: Algunas de las funciones de esta biblioteca solo funcionan en Android 15 (nivel de API 35) y versiones posteriores.

build.gradle

 implementation 'androidx.window:window:1.4.0-alpha02'

Cómo personalizar el divisor de ventanas

Crea una instancia de DividerAttributes y agrégala a SplitAttributes. Este objeto configura el comportamiento general de tu diseño dividido. Puedes usar las propiedades de color, ancho y rango de arrastre de DividerAttributes para mejorar la experiencia del usuario.

Personaliza el divisor:

  1. Verifica el nivel de API de WindowManager Extensiones. Dado que la función de expansión de paneles solo está disponible en el nivel de API 6 y versiones posteriores, esto también se aplica al resto de las funciones nuevas.
  2. Crea DividerAttributes: Para aplicar diseño al divisor entre los paneles, crea un objeto DividerAttributes. Este objeto te permite establecer lo siguiente:
  • color: Cambia el color del divisor para que coincida con el tema de tu app o crea una separación visual.
  • widthDp: Ajusta el ancho del divisor para mejorar la visibilidad o lograr un aspecto más sutil.
  1. Agrega a SplitAttributes: Una vez que hayas personalizado el divisor, agrégalo a tu objeto DividerAttributes.
  2. Establecer rango de arrastre (opcional): También puedes controlar hasta dónde pueden arrastrar el divisor los usuarios para cambiar el tamaño de los paneles.
  • DRAG_RANGE_SYSTEM_DEFAULT: Usa este valor especial para permitir que el sistema determine un rango de arrastre adecuado según el tamaño de la pantalla y el factor de forma del dispositivo.
  • Valor personalizado (entre 0.33 y 0.66): Establece tu propio rango de arrastre para limitar hasta qué punto los usuarios pueden cambiar el tamaño de los paneles. Recuerda que, si arrastran más allá de este límite, el diseño dividido quedará inhabilitado.

Reemplaza splitAttributes por el siguiente código.

SplitManager.kt

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

SplitManager.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();

Crea divider_color.xml en la carpeta res/color con el siguiente contenido.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="#669df6" />
</selector>

¡Ejecútalo!

Eso es todo. Compila y ejecuta la app de ejemplo.

Deberías ver la expansión del panel y poder arrastrarlo.

2ec5f7fd6df5d8cd.gif

Cómo cambiar la proporción de división en versiones anteriores

Nota importante de compatibilidad: La función de expansión de paneles solo está disponible en WindowManager Extensions 6 o versiones posteriores, lo que significa que necesitas Android 15 (nivel de API 35) o versiones posteriores.

Sin embargo, te recomendamos que proporciones una buena experiencia a los usuarios que tengan versiones anteriores de Android.

En Android 14 (nivel de API 34) y versiones anteriores, puedes proporcionar ajustes dinámicos de la proporción de división con la clase SplitAttributesCalculator. Esto ofrece una forma de mantener cierto nivel de control del usuario respecto del diseño, incluso sin la expansión del panel.

a36f8ba4226353c5.gif

¿Quieres saber cuál es la mejor manera de usar estas funciones? En la sección “Prácticas recomendadas”, hablaremos de todas las prácticas recomendadas y sugerencias de expertos.

5. Fijación de actividades

¿Alguna vez quisiste mantener fija una parte de la vista de pantalla dividida mientras navegas libremente en la otra? Podrías leer un artículo en un lado y, al mismo tiempo, poder interactuar con el contenido de otra app en la otra mitad.

Aquí es donde entra en juego la fijación de actividades. Te permite fijar una de las ventanas divididas para que permanezca en la pantalla incluso mientras navegas en la otra. Esto proporciona una experiencia de multitarea más enfocada y productiva para tus usuarios.

Cómo agregar el botón de fijación

Primero, agreguemos un botón en DetailActivity.. La aplicación fija esta DetailActivity cuando los usuarios hacen clic en el botón.

Realiza los siguientes cambios en activity_detail.xml:

  1. Agrega un ID a ConstraintLayout
android:id="@+id/detailActivity"
  1. Agrega un botón en la parte inferior del diseño
<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"/>
  1. Restringe la parte inferior de TextView a la parte superior del botón
app:layout_constraintBottom_toTopOf="@id/pinButton"

Quita esta línea en TextView.

app:layout_constraintBottom_toBottomOf="parent"

Este es el código XML completo de tu archivo de diseño activity_detail.xml, incluido el botón FIJAR ESTA ACTIVIDAD que acabamos de agregar:

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

Se agregó la cadena pin_this_activity a res/values/strings.xml.

<string name="pin_this_activity">PIN THIS ACTIVITY</string>

Cómo conectar el botón de fijación

  1. Declara la variable: En tu archivo DetailActivity.kt, declara una variable para que contenga una referencia al botón FIJAR ESTA ACTIVIDAD:

DetailActivity.kt

private lateinit var pinButton: Button

DetailActivity.java

private Button pinButton;
  1. Busca el botón en el diseño y agrega una devolución de llamada setOnClickListener().

DetailActivity.kt / onCreate

pinButton = findViewById(R.id.pinButton)
pinButton.setOnClickListener {
 pinActivityStackExample(taskId)
}

DetailActivity.java / onCreate()

Button pinButton = findViewById(R.id.pinButton);
pinButton.setOnClickListener( (view) => {
        pinActivityStack(getTaskId());

});
  1. Crea un método nuevo llamado pinActivityStackExample en tu clase DetailActivity. Aquí implementaremos la lógica de fijación real.

DetailActivity.kt

private fun pinActivityStackExample(taskId: Int) {

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

DetailActivity.java

private void pinActivityStackExample(int taskId) {
    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(taskId, pinSplitRule);
}

Nota:

  1. Solo se puede fijar una actividad a la vez. Deja de fijar la actividad fijada actualmente con
unpinTopActivityStack()

antes de fijar otra.

  1. Para habilitar la expansión del panel cuando se fija la actividad, llama a
setDividerAttributes()

para el

SplitAttributes

recién creado.

Cambios en la navegación hacia atrás

Con WindowManager 1.4, cambió el comportamiento de la navegación hacia atrás. El evento Atrás se envía a la última actividad enfocada cuando se usa la navegación con botones.

Navegación con botones:

  • Mediante la navegación con botones, el evento Atrás ahora se envía de forma coherente a la última actividad enfocada. Esto simplifica el comportamiento de la navegación hacia atrás, lo que lo hace más predecible para los usuarios.

Navegación por gestos:

  • Android 14 (nivel de API 34) y versiones anteriores: El gesto atrás envía el evento a la actividad en la que se produjo el gesto, lo que puede provocar un comportamiento inesperado en situaciones de pantalla dividida.
  • Android 15 (nivel de API 35) y versiones posteriores:
  • Actividades de la misma app: El gesto atrás finaliza de forma coherente la actividad superior, independientemente de la dirección del deslizamiento, lo que proporciona una experiencia más unificada.
  • Actividades de diferentes apps (superposición): El evento Atrás va a la última actividad en primer plano, y se alinea con el comportamiento de la navegación con botones.

¡Ejecútalo!

Compila y ejecuta la app de ejemplo.

Cómo fijar la actividad

  • Navega a la pantalla DetailActivity.
  • Presiona el botón FIJAR ESTA ACTIVIDAD.

980d0033972737ed.gif

6. Atenuación del diálogo de pantalla completa

Si bien la incorporación de actividades facilita los diseños de pantalla dividida, los diálogos en versiones anteriores solo atenuaban el contenedor de su propia actividad. Esto podría crear una experiencia visual desarticulada, en especial cuando quieres que el diálogo sea el centro de atención.

La solución: WindowManager 1.4

  • Tenemos lo que necesitas Con WindowManager 1.4, los diálogos ahora atenúan toda la ventana de la app de forma predeterminada (DimAreaBehavior.Companion.ON_TASK), lo que proporciona una sensación más envolvente y enfocada.
  • ¿Necesitas volver al comportamiento anterior? No hay problema. Aún puedes atenuar solo el contenedor de la actividad con ON_ACTIVITY_STACK.

ON_ACTIVITY_STACK

ON_TASK

A continuación, te mostramos cómo puedes usar ActivityEmbeddingController para administrar el comportamiento de atenuación de la pantalla completa:

Nota: La atenuación del diálogo de pantalla completa está disponible con WindowManager Extensions 5 o versiones posteriores.

SplitManager.kt / createSplit()

with(ActivityEmbeddingController.getInstance(context)) {
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 5) {
       setEmbeddingConfiguration(
           EmbeddingConfiguration.Builder()
               .setDimAreaBehavior(ON_TASK)
               .build()
       )
   }
}

SplitManager.java / createSplit()

ActivityEmbeddingController controller = ActivityEmbeddingController.getInstance(context);
if (WindowSdkExtensions.getInstance().getExtensionVersion()  >= 5) {
    controller.setEmbeddingConfiguration(
        new EmbeddingConfiguration.Builder()
            .setDimAreaBehavior(EmbeddingConfiguration.DimAreaBehavior.ON_TASK)
            .build()
    );
}

Para mostrar la función de atenuación de pantalla completa, presentaremos un diálogo de alerta que le solicitará al usuario su confirmación antes de fijar la actividad. Cuando aparece, este diálogo atenúa toda la ventana de la aplicación, no solo el contenedor en el que reside la actividad.

DetailActivity.kt

pinButton.setOnClickListener {
 showAlertDialog(taskId)
}

...
private fun showAlertDialog(taskId: Int) {
 val builder = AlertDialog.Builder(this)
 builder.setTitle(getString(R.string.dialog_title))
 builder.setMessage(getString(R.string.dialog_message))
 builder.setPositiveButton(getString(R.string.button_yes)) { _, _ ->
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 6) {
     pinActivityStackExample(taskId)
   }
 }
 builder.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
   // Cancel
 }
 val dialog: AlertDialog = builder.create()
 dialog.show()
}

DetailActivity.java

pinButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       showAlertDialog(getTaskId());
   }
});

...

private void showAlertDialog(int taskId) {
   AlertDialog.Builder builder = new AlertDialog.Builder(this);
   builder.setTitle(getString(R.string.dialog_title));
   builder.setMessage(getString(R.string.dialog_message));

   builder.setPositiveButton(getString(R.string.button_yes), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
               pinActivityStackExample(taskId);
           }
       }
   });
   builder.setNegativeButton(getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           // Cancel
       }
   });
   AlertDialog dialog = builder.create();
   dialog.show();
}

Agrega las siguientes cadenas a res/values/strings.xml.

<!-- Dialog information -->
<string name="dialog_title">Activity Pinning</string>
<string name="dialog_message">Confirm to pin this activity</string>
<string name="button_yes">Yes</string>
<string name="button_cancel">Cancel</string>

¡Ejecútalo!

Compila y ejecuta la app de ejemplo.

Haz clic en el botón para fijar la actividad:

  • Aparecerá un diálogo de alerta en el que se te pedirá que confirmes la acción de fijar.
  • Observa cómo toda la pantalla, incluidos los dos paneles divididos, se atenúa, lo que enfoca la atención en el diálogo.

2d3455e0f8901f95.png

7. Prácticas recomendadas

Cómo permitir que los usuarios desactiven el diseño de panel doble

Para que la transición a los nuevos diseños sea más fluida, les daremos a los usuarios la capacidad de cambiar entre las vistas de panel doble y de una sola columna. Podemos lograrlo con SplitAttributesCalculator y SharedPreferences para almacenar las preferencias del usuario.

Cómo cambiar la proporción de división en Android 14 y versiones anteriores

Exploramos la expansión de paneles, que ofrece una excelente manera para que los usuarios ajusten la proporción de división en Android 15 y versiones posteriores. Sin embargo, ¿cómo podemos ofrecer un nivel de flexibilidad similar a los usuarios de versiones anteriores de Android?

Analicemos cómo SplitAttributesCalculator puede ayudarnos a lograr esto y garantizar una experiencia coherente en una amplia variedad de dispositivos.

Este es un ejemplo de cómo se ve:

a87452341434c86d.gif

Cómo crear una pantalla de configuración

Para comenzar, crearemos una pantalla de configuración dedicada para la configuración del usuario.

En esta pantalla de configuración, incorporaremos un interruptor para habilitar o inhabilitar la función de incorporación de actividades para toda la aplicación. Además, incluiremos una barra de progreso que permita a los usuarios ajustar la proporción de división del diseño de panel doble. Ten en cuenta que el valor de la proporción de división solo se aplicará si el interruptor de incorporación de actividades está activado.

Después de que el usuario establece valores en SettingsActivity, los guardamos en SharedPreferences para usarlos más adelante en otros lugares de la aplicación.

build.gradle

Agrega una dependencia de preferencia.

implementation 'androidx.preference:preference-ktx:1.2.1' // Kotlin

O

implementation 'androidx.preference:preference:1.2.1' // Java

SettingsActivity.kt

package com.example.activity_embedding

import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat

class SettingsActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.settings_activity)
    if (savedInstanceState == null) {
      supportFragmentManager
        .beginTransaction()
        .replace(R.id.settings, SettingsFragment())
        .commit()
    }
    supportActionBar?.setDisplayHomeAsUpEnabled(true)
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.itemId == android.R.id.home) finishActivity()
    return super.onOptionsItemSelected(item)
  }

  private fun finishActivity() { finish() }

  class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
      setPreferencesFromResource(R.xml.root_preferences, rootKey)
findPreference<SwitchPreferenceCompat>("dual_pane")?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue as Boolean) {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(true)
          }
        } else {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(false)
          }
        }
        this.activity?.finish()
        true
      }

      val splitRatioPreference: SeekBarPreference? = findPreference("split_ratio")
      splitRatioPreference?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue is Int) {
          this.activity?.let { SharePref(it.applicationContext).setSplitRatio(newValue.toFloat()/100) }
        }
        true
      }
    }
  }
}

SettingsActivity.java

package com.example.activity_embedding;

import android.os.Bundle;
import android.view.MenuItem;

import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SeekBarPreference;
import androidx.preference.SwitchPreferenceCompat;

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        if (savedInstanceState == null) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.settings, new SettingsFragment())
                .commit();
        }
        if (getSupportActionBar() != null) {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finishActivity();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void finishActivity() {
        finish();
    }

    public static class SettingsFragment extends PreferenceFragmentCompat {
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey);

            SwitchPreferenceCompat dualPanePreference = findPreference("dual_pane");
            if (dualPanePreference != null) {
                dualPanePreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    boolean isDualPane = (Boolean) newValue;
                    if (getActivity() != null) {
                        SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                        sharePref.setAEFlag(isDualPane);
                        getActivity().finish();
                    }
                    return true;
                });
            }

            SeekBarPreference splitRatioPreference = findPreference("split_ratio");
            if (splitRatioPreference != null) {
                splitRatioPreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    if (newValue instanceof Integer) {
                        float splitRatio = ((Integer) newValue) / 100f;
                        if (getActivity() != null) {
                            SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                            sharePref.setSplitRatio(splitRatio);
                        }
                    }
                    return true;
                });
            }
        }
    }
}

Agrega settings_activity.xml a la carpeta de diseño

settings_activity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <FrameLayout
       android:id="@+id/settings"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />
</LinearLayout>

Agrega SettingsActivity a tu archivo de manifiesto

<activity
   android:name=".SettingsActivity"
   android:exported="false"
   android:label="@string/title_activity_settings" />

Configura reglas de división para SettingsActivity

SplitManager.kt / createSplit()

val settingActivityFilter = ActivityFilter(
   ComponentName(context, SettingsActivity::class.java),
   null
)
val settingActivityFilterSet = setOf(settingActivityFilter)
val settingActivityRule = ActivityRule.Builder(settingActivityFilterSet)
   .setAlwaysExpand(true)
   .build()
ruleController.addRule(settingActivityRule)

SplitManager.java / createSplit()

Set<ActivityFilter> settingActivityFilterSet = new HashSet<>();
ActivityFilter settingActivityFilter = new ActivityFilter(
        new ComponentName(context, SettingsActivity.class),
        null
);
settingActivityFilterSet.add(settingActivityFilter);
ActivityRule settingActivityRule = new ActivityRule.Builder(settingActivityFilterSet)
        .setAlwaysExpand(true).build();
ruleController.addRule(settingActivityRule);

Este es el código para guardar la configuración del usuario en SharedPreferences.

SharedPref.kt

package com.example.activity_embedding

import android.content.Context
import android.content.SharedPreferences

class SharePref(context: Context) {
    private val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("my_app_preferences", Context.MODE_PRIVATE)

    companion object {
        private const val AE_FLAG = "is_activity_embedding_enabled"
        private const val SPLIT_RATIO = "activity_embedding_split_ratio"
        const val DEFAULT_SPLIT_RATIO = 0.3f
    }

    fun setAEFlag(isEnabled: Boolean) {
        sharedPreferences.edit().putBoolean(AE_FLAG, isEnabled).apply()
    }

    fun getAEFlag(): Boolean = sharedPreferences.getBoolean(AE_FLAG, true)

    fun getSplitRatio(): Float = sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO)

    fun setSplitRatio(ratio: Float) {
        sharedPreferences.edit().putFloat(SPLIT_RATIO, ratio).apply()
    }
}

SharedPref.java

package com.example.activity_embedding;

import android.content.Context;
import android.content.SharedPreferences;

public class SharePref {
    private static final String PREF_NAME = "my_app_preferences";
    private static final String AE_FLAG = "is_activity_embedding_enabled";
    private static final String SPLIT_RATIO = "activity_embedding_split_ratio";
    public static final float DEFAULT_SPLIT_RATIO = 0.3f;

    private final SharedPreferences sharedPreferences;

    public SharePref(Context context) {
        this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    public void setAEFlag(boolean isEnabled) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(AE_FLAG, isEnabled);
        editor.apply();
    }

    public boolean getAEFlag() {
        return sharedPreferences.getBoolean(AE_FLAG, true);
    }

    public float getSplitRatio() {
        return sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO);
    }

    public void setSplitRatio(float ratio) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putFloat(SPLIT_RATIO, ratio);
        editor.apply();
    }
}

También necesitas un archivo en formato XML de diseño de pantalla de preferencias. Crea root_preferences.xml en res/xml con el siguiente código.

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:android="http://schemas.android.com/apk/res/android">
   <PreferenceCategory app:title="@string/split_setting_header">

       <SwitchPreferenceCompat
           app:key="dual_pane"
           app:title="@string/dual_pane_title" />

       <SeekBarPreference
           app:key="split_ratio"
           app:title="@string/split_ratio_title"
           android:min="0"
           android:max="100"
           app:defaultValue="50"
           app:showSeekBarValue="true" />
   </PreferenceCategory>
</PreferenceScreen>

Agrega lo siguiente a res/values/strings.xml:

<string name="title_activity_settings">SettingsActivity</string>
<string name="split_setting_header">Dual Pane Display</string>
<string name="dual_pane_title">Dual Pane</string>
<string name="split_ratio_title">Split Ratio</string>

Agrega SettingsActivity al menú

Conectemos nuestro SettingsActivity recién creado a un destino de navegación para que los usuarios puedan acceder a él fácilmente desde la interfaz principal de la app.

  1. En tu archivo ListActivity, declara variables para la barra de navegación inferior y el riel de navegación izquierdo:

ListActivity.kt

 private lateinit var navRail: NavigationRailView private lateinit var bottomNav: BottomNavigationView

ListActivity.java

 private NavigationRailView navRail;  private BottomNavigationView bottomNav;
  1. Dentro del método onCreate() de tu ListActivity, usa findViewById para conectar estas variables a las vistas correspondientes en tu diseño.
  2. Agrega un OnItemSelectedListener a la barra de navegación inferior y al riel de navegación para controlar los eventos de selección de elementos:

ListActivity.kt / onCreate()

navRail  = findViewById(R.id.navigationRailView)
bottomNav = findViewById(R.id.bottomNavigationView)

val menuListener = NavigationBarView.OnItemSelectedListener { item ->
    when (item.itemId) {
        R.id.navigation_home -> {
            true
        }
        R.id.navigation_dashboard -> {
            true
        }
        R.id.navigation_settings -> {
            startActivity(Intent(this, SettingsActivity::class.java))
            true
        }
        else -> false
    }
}

navRail.setOnItemSelectedListener(menuListener)
bottomNav.setOnItemSelectedListener(menuListener)

ListActivity.java / onCreate()

NavigationRailView navRail = findViewById(R.id.navigationRailView);
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);

NavigationBarView.OnItemSelectedListener menuListener = new NavigationBarView.OnItemSelectedListener() {
   @Override
   public boolean onNavigationItemSelected(@NonNull MenuItem item) {
       switch (item.getItemId()) {
           case R.id.navigation_home:
               // Handle navigation_home selection
               return true;
           case R.id.navigation_dashboard:
               // Handle navigation_dashboard selection
               return true;
           case R.id.navigation_settings:
               startActivity(new Intent(ListActivity.this, SettingsActivity.class));
               return true;
           default:
               return false;
       }
   }
};

navRail.setOnItemSelectedListener(menuListener);
bottomNav.setOnItemSelectedListener(menuListener);

La aplicación lee SharedPreferences y renderiza la app en modo dividido o en modo SPLIT_TYPE_EXPAND.

  • Cuando cambia la configuración de la ventana, el programa comprueba si se cumple la restricción de ventana dividida (si el ancho es superior a 840 dp).
  • Además, la app verifica el valor SharedPreferences para ver si el usuario habilitó la ventana dividida para la pantalla; de lo contrario, muestra SplitAttribute con el tipo SPLIT_TYPE_EXPAND.
  • Si la ventana dividida está habilitada, la app lee el valor SharedPreferences para obtener la proporción de división. Esto solo funciona cuando la versión de WindowSDKExtensions es inferior a 6, ya que la versión 6 ya admite la expansión del panel y omite la configuración de la proporción de división. En su lugar, los desarrolladores podrían permitir que los usuarios arrastren el divisor en la IU.

ListActivity.kt / onCreate()

...

SplitController.getInstance(this).setSplitAttributesCalculator{
       params -> params.defaultSplitAttributes
   if (params.areDefaultConstraintsSatisfied) {
       setWiderScreenNavigation(true)

       if (SharePref(this.applicationContext).getAEFlag()) {
           if (WindowSdkExtensions.getInstance().extensionVersion  < 6) {
               // Read a dynamic split ratio from shared preference.
               val currentSplit = SharePref(this.applicationContext).getSplitRatio()
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return@setSplitAttributesCalculator SplitAttributes.Builder()
                       .setSplitType(SplitAttributes.SplitType.ratio(SharePref(this.applicationContext).getSplitRatio()))
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                       .build()
               }
           }
           return@setSplitAttributesCalculator params.defaultSplitAttributes
       } else {
           SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build()
       }
   } else {
       setWiderScreenNavigation(false)
       SplitAttributes.Builder()
           .setSplitType(SPLIT_TYPE_EXPAND)
           .build()
   }
}

...

ListActivity.java / onCreate()

...
SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
   if (params.areDefaultConstraintsSatisfied()) {
       setWiderScreenNavigation(true);

       SharePref sharedPreference = new SharePref(this.getApplicationContext());
       if (sharedPreference.getAEFlag()) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion()  < 6) {
               // Read a dynamic split ratio from shared preference.
               float currentSplit = sharedPreference.getSplitRatio();
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return new SplitAttributes.Builder()
                           .setSplitType(SplitAttributes.SplitType.ratio(sharedPreference.getSplitRatio()))
                           .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                           .build();
               }
           }
           return params.getDefaultSplitAttributes();
       } else {
           return new SplitAttributes.Builder()
                   .setSplitType(SPLIT_TYPE_EXPAND)
                   .build();
       }
   } else {
       setWiderScreenNavigation(false);
       return new SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build();
   }
});

...

Para activar SplitAttributesCalculator después de establecer cambios, debemos invalidar los atributos actuales. Para ello, llamamos a invalidateVisibleActivityStacks() desde ActivityEmbeddingController; antes de WindowManager 1.4, se llama al método

invalidateTopVisibleSplitAttributes.

ListActivity.kt / onResume()

override fun onResume() {
   super.onResume()
   ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
}

ListActivity.java / onResume()

@Override
public void onResume() {
    super.onResume();
    ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks();
}

¡Ejecútalo!

Compila y ejecuta la app de ejemplo.

Explora la configuración:

  • Navega a la pantalla de configuración.
  • Activa o desactiva el interruptor Habilitar ventana dividida.
  • Ajusta el control deslizante de proporción de división (si está disponible en tu dispositivo).

Observa los cambios en el diseño:

  • En dispositivos con Android 14 y versiones anteriores: El diseño debe cambiar entre los modos de panel único y panel doble según el interruptor, y la proporción de división debe cambiar cuando ajustas el control deslizante.
  • En dispositivos con Android 15 y versiones posteriores: La expansión de paneles debería permitirte cambiar el tamaño de los paneles de forma dinámica, independientemente de la configuración del control deslizante.

8. ¡Felicitaciones!

¡Bien hecho! Mejoraste tu app de forma correcta con funciones nuevas y potentes mediante la incorporación de actividades y WindowManager. Ahora, tus usuarios disfrutarán de una experiencia más flexible, intuitiva y atractiva en pantallas grandes, independientemente de la versión de Android que tengan.

9. Más información