Como desenvolver para dobráveis

Na versão ConstraintLayout 2.1, vários recursos foram adicionados para ajudar a gerenciar dispositivos dobráveis, incluindo SharedValues, ReactiveGuide e suporte avançado a animações com o MotionLayout.

Valores compartilhados

Adicionamos um novo mecanismo para injetar valores de ambiente de execução em ConstraintLayout. Esse recurso foi criado para valores de todo o sistema, já que todas as instâncias de ConstraintLayout podem acessar o valor.

No contexto de dispositivos dobráveis, podemos usar esse mecanismo para injetar a posição da dobra no momento da execução:

Kotlin

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

Java

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

Em um auxiliar personalizado, é possível acessar os valores compartilhados adicionando um listener para qualquer mudança:

Kotlin

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

Java

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

Consulte o exemplo de dispositivos dobráveis para capturar a posição da dobra usando a biblioteca Jetpack WindowManager e injetar a posição em 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() usa um ID que representa o valor como o primeiro parâmetro e o valor a ser injetado como o segundo parâmetro.

ReactiveGuide

Uma maneira de aproveitar um SharedValue em um layout sem precisar programar nenhum código é usar o auxiliar ReactiveGuide. Isso posiciona uma diretriz horizontal ou vertical de acordo com o SharedValue vinculado.

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

Depois disso, você poderá usá-la da mesma forma que faria com uma diretriz normal.

MotionLayout para dispositivos dobráveis

Adicionamos vários recursos ao MotionLayout na versão 2.1 que ajuda a transformar o estado. Isso é particularmente útil para dispositivos dobráveis, já que normalmente precisamos processar a animação entre os diferentes layouts possíveis.

Há duas abordagens disponíveis para dispositivos dobráveis:

  • Durante a execução, atualize o layout atual (ConstraintSet) para mostrar ou ocultar a dobra.
  • Use um ConstraintSet separado para cada um dos estados dobráveis que você quer oferecer suporte (closed, folded ou fully open).

Como animar um ConstraintSet

A função updateStateAnimate() em MotionLayout foi adicionada na versão 2.1:

Kotlin

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

Java

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

Essa função anima automaticamente as mudanças ao atualizar uma determinada ConstraintSet, em vez de fazer uma atualização imediata (o que pode ser feito com updateState(stateId, constraintset)). Isso permite que você atualize a interface em tempo real, dependendo de mudanças, como em qual estado dobrável você está.

ReactiveGuide em um MotionLayout

ReactiveGuide também oferece suporte a dois atributos úteis quando usado dentro de um MotionLayout:

  • app:reactiveGuide_animateChange="true|false"

  • app:reactiveGuide_applyToAllConstraintSets="true|false"

O primeiro modifica a ConstraintSet atual e anima a mudança automaticamente. O segundo aplicará o novo valor da posição ReactiveGuide a todos os ConstraintSets no MotionLayout. Uma abordagem típica para dispositivos dobráveis é usar um ReactiveGuide que representa a posição da dobra, configurando os elementos de layout em relação à ReactiveGuide.

Como usar vários ConstraintSets para representar o estado dobrável

Em vez de atualizar o estado atual da MotionLayout, outra maneira de arquitetar sua interface para oferecer suporte a dispositivos dobráveis é criando estados separados específicos (incluindo closed, folded e fully open).

Nesse cenário, você ainda pode usar um ReactiveGuide para representar a dobra, mas teria muito mais controle (em comparação com a animação automatizada ao atualizar a ConstraintSet atual) sobre como cada estado seria transferido para outro.

Com essa abordagem, no listener DeviceState, você simplesmente direcionaria a MotionLayout para fazer a transição para estados específicos pelo método MotionLayout.transitionToState(stateId).