Дизайн для складных устройств

В версии ConstraintLayout 2.1 появилось несколько функций для управления складными устройствами, в том числе SharedValues, ReactiveGuide и улучшенная поддержка анимации с помощью MotionLayout.

Общие ценности

Мы добавили новый механизм для внедрения значений времени выполнения в ConstraintLayout. Он предназначен для значений на уровне системы, поскольку все экземпляры ConstraintLayout могут получить доступ к значению.

В случае со складными устройствами мы можем использовать этот механизм, чтобы внедрить положение сгиба во время выполнения:

Kotlin

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

Java

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

В специальном помощнике вы можете получить доступ к общим значениям, добавив прослушиватель для любых изменений:

Kotlin

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

Java

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

В примере FoldableExperiments показано, как мы определяем положение сгиба с помощью библиотеки Jetpack WindowManager и передаем его в 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() принимает идентификатор, представляющий значение, в качестве первого параметра и значение, которое нужно вставить, в качестве второго параметра.

ReactiveGuide

Один из способов использовать SharedValue в макете без написания кода – использовать вспомогательный класс ReactiveGuide. Это позволит разместить горизонтальную или вертикальную направляющую в соответствии со связанным SharedValue.

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

После этого ее можно использовать как обычную направляющую.

MotionLayout для складных устройств

В MotionLayout 2.1 мы добавили несколько функций, которые помогают изменять состояние. Это особенно полезно для складных устройств, поскольку обычно нам приходится обрабатывать анимацию между различными возможными макетами.

Для складных устройств есть два подхода:

  • Во время выполнения обновите текущий макет (ConstraintSet), чтобы показать или скрыть сгиб.
  • Используйте отдельный ConstraintSet для каждого из поддерживаемых состояний складного устройства (closed, folded или fully open).

Анимация: ConstraintSet

Функция updateStateAnimate() в MotionLayout была добавлена в версии 2.1:

Kotlin

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

Java

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

Эта функция автоматически анимирует изменения при обновлении заданного ConstraintSet вместо немедленного обновления (которое можно выполнить с помощью updateState(stateId, constraintset)). Это позволяет обновлять пользовательский интерфейс на лету в зависимости от изменений, например от того, в каком состоянии находится складное устройство.

ReactiveGuide в MotionLayout

ReactiveGuide также поддерживает два полезных атрибута, если используется внутри MotionLayout:

  • app:reactiveGuide_animateChange="true|false"

  • app:reactiveGuide_applyToAllConstraintSets="true|false"

Первый изменит текущий ConstraintSet и автоматически анимирует изменение. Второй вариант позволяет применить новое значение позиции ReactiveGuide ко всем ConstraintSet в MotionLayout. Типичный подход для складных устройств – использовать ReactiveGuide, представляющий положение сгиба, и настраивать элементы макета относительно ReactiveGuide.

Использование нескольких ConstraintSet для представления состояния складного устройства

Вместо того чтобы обновлять текущее состояние MotionLayout, вы можете создать отдельные состояния (включая closed, folded и fully open).

В этом случае вы можете использовать ReactiveGuide, чтобы представить сгиб, но у вас будет гораздо больше контроля (по сравнению с автоматической анимацией при обновлении текущего ConstraintSet) над тем, как каждое состояние будет переходить в другое.

При таком подходе в прослушивателе DeviceState вы просто указываете MotionLayout перейти в определенные состояния с помощью метода MotionLayout.transitionToState(stateId).