Thiết kế cho thiết bị có thể gập lại

Trong bản phát hành ConstraintLayout 2.1, một số tính năng đã được thêm vào để giúp quản lý các thiết bị có thể gập, trong đó có SharedValues, ReactiveGuide và hỗ trợ nâng cao ảnh động với MotionLayout.

Giá trị được chia sẻ

Chúng tôi thêm một cơ chế mới để chèn giá trị thời gian chạy vào ConstraintLayout – mục đích của cơ chế này là sử dụng cho các giá trị trên toàn hệ thống, vì mọi thực thể của ConstraintLayout đều có thể truy cập vào giá trị đó.

Trong bối cảnh của các thiết bị có thể gập lại, chúng ta có thể sử dụng cơ chế này để chèn vị trí của màn hình đầu tiên trong thời gian chạy:

Kotlin

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

Java

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

Trong một trình trợ giúp tuỳ chỉnh, bạn có thể truy cập vào các giá trị dùng chung bằng cách thêm trình nghe đối với mọi thay đổi:

Kotlin

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

Java

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

Bạn có thể xem ví dụ về Thử nghiệm có thể gập lại để xem cách chúng tôi nắm bắt vị trí màn hình đầu tiên bằng cách sử dụng thư viện Jetpack WindowManager và chèn vị trí vào 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() lấy một mã nhận dạng đại diện cho giá trị làm tham số đầu tiên và giá trị cần chèn làm tham số thứ hai.

ReactiveGuide

Một cách để tận dụng SharedValue trong bố cục mà không phải viết bất kỳ mã nào là sử dụng trình trợ giúp ReactiveGuide. Thao tác này sẽ định vị một hướng dẫn theo chiều ngang hoặc chiều dọc theo SharedValue được liên kết.

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

Sau đó, bạn có thể sử dụng đoạn mã này như với hướng dẫn thông thường.

MotionLayout cho thiết bị có thể gập lại

Chúng tôi đã thêm một số tính năng trong MotionLayout ở phiên bản 2.1 để giúp biến đổi trạng thái – một tính năng đặc biệt hữu ích cho thiết bị có thể gập lại, vì chúng ta thường phải xử lý ảnh động giữa nhiều bố cục có thể có.

Có 2 phương pháp dành cho thiết bị có thể gập lại:

  • Trong thời gian chạy, hãy cập nhật bố cục hiện tại (ConstraintSet) để hiện hoặc ẩn đường ranh giới phần hiển thị.
  • Sử dụng một ConstraintSet riêng cho từng trạng thái có thể gập lại mà bạn muốn hỗ trợ (closed, folded hoặc fully open).

Tạo ảnh động cho ConstraintSet

Hàm updateStateAnimate() trong MotionLayout đã được thêm vào bản phát hành 2.1:

Kotlin

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

Java

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

Hàm này sẽ tự động tạo ảnh động cho các thay đổi khi cập nhật một ConstraintSet nhất định thay vì cập nhật ngay (bạn có thể thực hiện với updateState(stateId, constraintset)). Tính năng này cho phép bạn cập nhật giao diện người dùng một cách nhanh chóng, tuỳ thuộc vào các thay đổi, chẳng hạn như trạng thái gập của thiết bị có thể gập lại.

ReactiveGuide bên trong một MotionLayout

ReactiveGuide cũng hỗ trợ 2 thuộc tính hữu ích khi được dùng bên trong một MotionLayout:

  • app:reactiveGuide_animateChange="true|false"

  • app:reactiveGuide_applyToAllConstraintSets="true|false"

Phần đầu tiên sẽ sửa đổi ConstraintSet hiện tại và tự động tạo ảnh động cho thay đổi. Phần tử thứ hai sẽ áp dụng giá trị mới của vị trí ReactiveGuide cho tất cả ConstraintSet trong MotionLayout. Một phương pháp điển hình cho thiết bị có thể gập lại là sử dụng ReactiveGuide đại diện cho vị trí gập, thiết lập các phần tử bố cục tương ứng với ReactiveGuide.

Sử dụng nhiều ConstraintSet để biểu thị trạng thái có thể gập lại

Thay vì cập nhật trạng thái MotionLayout hiện tại, một cách khác để thiết kế giao diện người dùng nhằm hỗ trợ thiết bị có thể gập lại là tạo các trạng thái riêng biệt cụ thể (bao gồm closed, foldedfully open).

Trong trường hợp này, bạn có thể vẫn muốn sử dụng ReactiveGuide để thể hiện màn hình đầu tiên, nhưng bạn sẽ có nhiều quyền kiểm soát hơn (so với ảnh động tự động khi cập nhật ConstraintSet hiện tại) về cách mỗi trạng thái sẽ chuyển đổi thành một trạng thái khác.

Với cách tiếp cận này, trong trình nghe DeviceState, bạn chỉ cần lệnh MotionLayout chuyển đổi sang các trạng thái cụ thể thông qua phương thức MotionLayout.transitionToState(stateId).