Cómo diseñar para dispositivos plegables

En la versión 2.1 de ConstraintLayout, se agregaron varias funciones para ayudar a administrar dispositivos plegables, incluidas SharedValues y ReactiveGuide, y la compatibilidad mejorada para animaciones con MotionLayout.

Valores compartidos

Agregamos un nuevo mecanismo para insertar valores del entorno de ejecución en ConstraintLayout, que está diseñado para usarse con valores en todo el sistema, ya que todas las instancias de ConstraintLayout pueden acceder al valor.

En el contexto de los dispositivos plegables, podemos usar este mecanismo para inyectar la posición del pliegue en el tiempo de ejecución:

Kotlin

ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)

Java

ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold);

En un asistente personalizado, puedes acceder a los valores compartidos si agregas un objeto de escucha para cualquier cambio:

Kotlin

val sharedValues: SharedValues = ConstraintLayout.getSharedValues()
sharedValues.addListener(mAttributeId, this)

Java

SharedValues sharedValues = ConstraintLayout.getSharedValues();
sharedValues.addListener(mAttributeId, this);

Puedes observar el ejemplo de FoldableExperiments para ver cómo capturamos la posición del pliegue con la biblioteca Jetpack WindowManager y cómo inyectamos la posición en ConstraintLayout.

Kotlin

inner class StateContainer : Consumer<WindowLayoutInfo> {

    override fun accept(newLayoutInfo: WindowLayoutInfo) {

        // Add views that represent display features
        for (displayFeature in newLayoutInfo.displayFeatures) {
            val foldFeature = displayFeature as? FoldingFeature
            if (foldFeature != null) {
                if (foldFeature.isSeparating &&
                    foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
                ) {
                    // The foldable device is in tabletop mode
                    val fold = foldPosition(motionLayout, foldFeature)
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)
                } else {
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0);
                }
            }
        }
    }
}

Java

class StateContainer implements Consumer<WindowLayoutInfo> {

    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {

        // Add views that represent display features
        for (DisplayFeature displayFeature : newLayoutInfo.getDisplayFeatures()) {
            if (displayFeature instanceof FoldingFeature) {
                FoldingFeature foldFeature = (FoldingFeature)displayFeature;
                if (foldFeature.isSeparating() &&
                    foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL
                ) {
                    // The foldable device is in tabletop mode
                    int fold = foldPosition(motionLayout, foldFeature);
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold);
                } else {
                    ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0);
                }
            }
        }
    }
}

fireNewValue() toma un ID que representa el valor como el primer parámetro y el valor que se insertará como el segundo parámetro.

ReactiveGuide

Una forma de aprovechar un SharedValue en un diseño, sin tener que escribir ningún código, es usar el asistente ReactiveGuide. De esta manera, se posicionará una guía horizontal o vertical según el SharedValue vinculado.

    <androidx.constraintlayout.widget.ReactiveGuide
        android:id="@+id/fold"
        app:reactiveGuide_valueId="@id/fold"
        android:orientation="horizontal" />

Puedes usarlo como lo harías con una guía normal.

MotionLayout para dispositivos plegables

Agregamos varias funciones en la versión 2.1 de MotionLayout que ayudan a transformar el estado, algo particularmente útil para los dispositivos plegables, ya que, en general, debemos controlar la animación entre los diferentes diseños posibles.

Hay dos enfoques disponibles para los dispositivos plegables:

  • Durante el tiempo de ejecución, actualiza tu diseño actual (ConstraintSet) para ocultar o mostrar el pliegue.
  • Usa un ConstraintSet independiente para cada uno de los estados plegables que desees admitir (closed, folded o fully open).

Cómo animar un ConstraintSet

La función updateStateAnimate() en MotionLayout se agregó en la versión 2.1:

Kotlin

fun updateStateAnimate(stateId: Int, set: ConstraintSet, duration: Int)

Java

void updateStateAnimate(int stateId, ConstraintSet set, int duration);

Esta función animará automáticamente los cambios cuando se actualice un ConstraintSet determinado, en lugar de realizar una actualización inmediata (lo que puedes hacer con updateState(stateId, constraintset)). Esto te permite actualizar tu IU sobre la marcha, según los cambios, como el estado plegable en el que te encuentres.

ReactiveGuide dentro de un MotionLayout

ReactiveGuide también admite dos atributos útiles cuando se usa dentro de un MotionLayout:

  • app:reactiveGuide_animateChange="true|false"

  • app:reactiveGuide_applyToAllConstraintSets="true|false"

El primero modificará el ConstraintSet actual y animará el cambio automáticamente. La segunda aplicará el nuevo valor de la posición ReactiveGuide a todos los ConstraintSet en MotionLayout. Un enfoque típico para los dispositivos plegables es usar un ReactiveGuide que represente la posición de plegado y configure tus elementos de diseño en relación con el ReactiveGuide.

Cómo usar varios ConstraintSet para representar el estado plegable

En lugar de actualizar el estado actual de MotionLayout, otra forma de diseñar tu IU para admitir dispositivos plegables es crear estados separados específicos (incluidos closed, folded y fully open).

En esta situación, es posible que aún quieras usar un ReactiveGuide para representar el pliegue, pero tendrías mucho más control (en comparación con la animación automática cuando se actualiza el ConstraintSet actual) sobre la transición de cada estado a otro.

Con este enfoque, en tu objeto de escucha DeviceState, simplemente debes indicar a MotionLayout para que haga la transición a estados específicos a través del método MotionLayout.transitionToState(stateId).