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
- Haber completado el codelab Cómo compilar un diseño de lista-detalles con incorporación de actividades y Material Design
- Experiencia en el trabajo con Android Studio, incluida la configuración de dispositivos virtuales con Android 15
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:
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.
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 unRecyclerView
.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.
- 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.
- 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.
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:
- 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.
- 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.
- ¿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.
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:
- 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.
- Crea
DividerAttributes
: Para aplicar diseño al divisor entre los paneles, crea un objetoDividerAttributes
. 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.
- Agrega a
SplitAttributes
: Una vez que hayas personalizado el divisor, agrégalo a tu objetoDividerAttributes
. - 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.
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.
¿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
:
- Agrega un ID a
ConstraintLayout
android:id="@+id/detailActivity"
- 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"/>
- 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
- 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;
- 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());
});
- Crea un método nuevo llamado
pinActivityStackExample
en tu claseDetailActivity
. 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:
- Solo se puede fijar una actividad a la vez. Deja de fijar la actividad fijada actualmente con
unpinTopActivityStack()
antes de fijar otra.
- 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.
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
.
|
|
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.
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:
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.
- 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;
- Dentro del método
onCreate()
de tuListActivity
, usafindViewById
para conectar estas variables a las vistas correspondientes en tu diseño. - 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, muestraSplitAttribute
con el tipoSPLIT_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 deWindowSDKExtensions
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
- Guía para desarrolladores: Incorporación de actividades
- Documentación de referencia: androidx.window.embedding