Las pantallas grandes desplegadas y los estados plegados únicos permiten crear nuevas experiencias del usuario en dispositivos plegables. Para que tu app funcione en dispositivos plegables, usa la biblioteca de Jetpack WindowManager, que proporciona una plataforma de API para las funciones de ventanas de dispositivos plegables, como los pliegues y las bisagras. Cuando tu app funciona en dispositivos plegables, puede adaptar su diseño para evitar colocar contenido importante en el área de pliegues o bisagras y usar esos elementos como separadores naturales.
Comprender si un dispositivo admite configuraciones como la posición de mesa o de libro puede guiar las decisiones sobre la compatibilidad con diferentes diseños o la implementación de funciones específicas.
Información de la ventana
La interfaz WindowInfoTracker
de Jetpack WindowManager expone la información de diseño de la ventana. El método windowLayoutInfo()
de la interfaz muestra un flujo de datos WindowLayoutInfo
que informa a la app sobre el estado de plegado de un dispositivo plegable. El método WindowInfoTracker#getOrCreate()
crea una instancia de WindowInfoTracker
.
WindowManager brinda compatibilidad para recopilar datos de WindowLayoutInfo
mediante flujos de Kotlin y devoluciones de llamada de Java.
Flujos de Kotlin
Para iniciar y detener la recopilación de datos de WindowLayoutInfo
, puedes usar una corrutina que se pueda reiniciar y que tenga en cuenta el ciclo de vida, en la que se ejecuta el bloque de código repeatOnLifecycle
cuando el ciclo de vida tiene al menos STARTED
y se detiene cuando el ciclo de vida tiene el estado STOPPED
. La ejecución del bloque de código se reinicia automáticamente cuando el ciclo de vida vuelve a tener el estado STARTED
. En el siguiente ejemplo, el bloque de código recopila y usa datos de WindowLayoutInfo
:
class DisplayFeaturesActivity : AppCompatActivity() {
private lateinit var binding: ActivityDisplayFeaturesBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
Devoluciones de llamada de Java
La capa de compatibilidad de devolución de llamada incluida en la dependencia androidx.window:window-java
te permite recopilar actualizaciones de WindowLayoutInfo
sin usar un flujo de Kotlin. El artefacto incluye la clase WindowInfoTrackerCallbackAdapter
, que adapta un WindowInfoTracker
para admitir el registro (y la cancelación del registro) de las devoluciones de llamada para recibir actualizaciones de WindowLayoutInfo
, por ejemplo:
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
Compatibilidad con RxJava
Si ya usas RxJava
(versión 2
o 3
), puedes aprovechar los artefactos que te permiten usar un Observable
o Flowable
para recopilar actualizaciones WindowLayoutInfo
sin usar un flujo de Kotlin.
La capa de compatibilidad proporcionada por las dependencias androidx.window:window-rxjava2
y androidx.window:window-rxjava3
incluye los métodos WindowInfoTracker#windowLayoutInfoFlowable()
y WindowInfoTracker#windowLayoutInfoObservable()
, que permiten que tu app reciba actualizaciones de WindowLayoutInfo
, por ejemplo:
class RxActivity: AppCompatActivity {
private lateinit var binding: ActivityRxBinding
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Create a new observable.
observable = WindowInfoTracker.getOrCreate(this@RxActivity)
.windowLayoutInfoObservable(this@RxActivity)
}
@Override
protected void onStart() {
super.onStart();
// Subscribe to receive WindowLayoutInfo updates.
disposable?.dispose()
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose of the WindowLayoutInfo observable.
disposable?.dispose()
}
}
Funciones de las pantallas plegables
La clase WindowLayoutInfo
de Jetpack WindowManager pone a disposición las funciones de una ventana de visualización como una lista de elementos DisplayFeature
.
FoldingFeature
es un tipo de DisplayFeature
que proporciona información sobre pantallas plegables, lo que incluye lo siguiente:
state
: Es el estado plegado del dispositivo,FLAT
oHALF_OPENED
.orientation
: Es la orientación del pliegue o la bisagra,HORIZONTAL
oVERTICAL
.occlusionType
: Indica si el pliegue o la bisagra oculta parte de la pantallaNONE
oFULL
.isSeparating
: Indica si el pliegue o la bisagra crea dos áreas lógicas de visualización, verdadero o falso.
Un dispositivo plegable que tenga el estado HALF_OPENED
siempre informa isSeparating
como verdadero porque la pantalla está separada en dos áreas de visualización. Además, isSeparating
siempre es verdadero en un dispositivo con pantalla doble cuando la aplicación abarca ambas pantallas.
La propiedad FoldingFeature
bounds
(heredada de DisplayFeature
) representa el rectángulo delimitador de un componente de plegado, como un pliegue o una bisagra.
Se pueden usar los límites para posicionar elementos en la pantalla en relación con el componente:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... lifecycleScope.launch(Dispatchers.Main) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { // Safely collects from WindowInfoTracker when the lifecycle is // STARTED and stops collection when the lifecycle is STOPPED. WindowInfoTracker.getOrCreate(this@MainActivity) .windowLayoutInfo(this@MainActivity) .collect { layoutInfo -> // New posture information. val foldingFeature = layoutInfo.displayFeatures .filterIsInstance<FoldingFeature>() .firstOrNull() // Use information from the foldingFeature object. } } } }
Java
private WindowInfoTrackerCallbackAdapter windowInfoTracker; private final LayoutStateChangeCallback layoutStateChangeCallback = new LayoutStateChangeCallback(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ... windowInfoTracker = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this)); } @Override protected void onStart() { super.onStart(); windowInfoTracker.addWindowLayoutInfoListener( this, Runnable::run, layoutStateChangeCallback); } @Override protected void onStop() { super.onStop(); windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback); } class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> { @Override public void accept(WindowLayoutInfo newLayoutInfo) { // Use newLayoutInfo to update the Layout. List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures(); for (DisplayFeature feature : displayFeatures) { if (feature instanceof FoldingFeature) { // Use information from the feature object. } } } }
Posición de mesa
Con la información incluida en el objeto FoldingFeature
, tu app puede admitir posiciones como la de mesa, en la que el teléfono se ubica sobre una superficie, la bisagra está en posición horizontal y la pantalla plegable está semiabierta.
La postura de mesa ofrece a los usuarios la comodidad de operar el teléfono sin tenerlo en la mano. La posición de mesa es ideal para mirar contenido multimedia, tomar fotos y hacer videollamadas.
Usa FoldingFeature.State
y FoldingFeature.Orientation
para determinar si el dispositivo está en posición de mesa:
Kotlin
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL }
Java
boolean isTableTopPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL); }
Una vez que sepas que el dispositivo está en posición de mesa, actualiza el diseño de tu app según corresponda. En el caso de las apps de música, por lo general, eso significa que debes colocar la reproducción en la mitad superior de la página y los controles de posicionamiento, así como el contenido complementario, justo debajo para obtener una experiencia de reproducción sin usar las manos.
En Android 15 (nivel de API 35) y versiones posteriores, puedes invocar una API síncrona para detectar si un dispositivo admite la posición de mesa, independientemente del estado actual del dispositivo.
La API proporciona una lista de las posiciones que admite el dispositivo. Si la lista contiene la posición de mesa, puedes dividir el diseño de tu app para admitir la posición y ejecutar pruebas A/B en la IU de tu app para diseños de mesa y de pantalla completa.
Kotlin
if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) { val postures = WindowInfoTracker.getOrCreate(context).supportedPostures if (postures.contains(TABLE_TOP)) { // Device supports tabletop posture. } }
Java
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) { List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures(); if (postures.contains(SupportedPosture.TABLETOP)) { // Device supports tabletop posture. } }
Ejemplos
App de
MediaPlayerActivity
: Descubre cómo usar Media3 Exoplayer y WindowManager para crear un reproductor de video apto para dispositivos plegables.Codelab Cómo optimizar tu app de cámara en dispositivos plegables con Jetpack WindowManager: Obtén información para implementar la posición de mesa para apps de fotografía. Muestra el visor en la mitad superior de la pantalla (por encima del pliegue) y los controles en la mitad inferior (por debajo del pliegue).
Postura del libro
Otra característica plegable única es la posición de libro, en la que el dispositivo está parcialmente abierto y la bisagra es vertical. La postura de libro es ideal para leer libros electrónicos. Con un diseño de dos páginas en una pantalla plegable grande y abierta como un libro, la postura de libro captura la experiencia de leer un libro real.
También se puede utilizar para fotografías si quieres capturar una relación de aspecto diferente mientras tomas fotos con el modo manos libres.
Implementa la posición de libro con las mismas técnicas que se usan para la posición de mesa. La única diferencia es que el código debe verificar que la orientación del componente de plegado sea vertical en lugar de horizontal:
Kotlin
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.VERTICAL }
Java
boolean isBookPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL); }
Cambios en el tamaño de la ventana
El área de visualización de una app puede cambiar como resultado de un cambio en la configuración del dispositivo. Por ejemplo, cuando se pliega o se despliega el dispositivo, se rota o se cambia el tamaño de una ventana en el modo multiventana.
La clase WindowMetricsCalculator
de Jetpack WindowManager te permite recuperar las métricas actuales y máximas de la ventana. Al igual que la plataforma WindowMetrics
, que se introdujo en el nivel de API 30, WindowManager WindowMetrics
proporciona los límites de la ventana, pero la API es retrocompatible hasta el nivel de API 14.
Consulta Cómo usar clases de tamaño de ventana.
Recursos adicionales
Ejemplos
- WindowManager de Jetpack: Ejemplo de uso de la biblioteca de WindowManager de Jetpack
- Jetcaster : Implementación de posición de mesa con Compose
Codelabs
- Cómo admitir dispositivos plegables y con pantalla doble con Jetpack WindowManager
- Cómo optimizar tu app de cámara en dispositivos plegables con Jetpack WindowManager