Giúp ứng dụng nhận biết chế độ gập

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Màn hình lớn, gập lại và các trạng thái gập độc đáo cho phép người dùng trải nghiệm mới trên các thiết bị có thể gập lại. Để giúp ứng dụng của bạn nhận biết được màn hình đầu tiên, hãy sử dụng thư viện Jetpack WindowManager, thư viện này cung cấp bề mặt API cho các tính năng có thể gập lại của cửa sổ thiết bị như màn hình đầu tiên và bản lề. Khi nhận biết được ứng dụng của bạn trong màn hình đầu tiên, ứng dụng có thể điều chỉnh bố cục để tránh đặt nội dung quan trọng trong màn hình đầu tiên hoặc bản lề, đồng thời sử dụng đường gấp và bản lề làm dấu phân tách tự nhiên.

Thông tin về cửa sổ

Giao diện WindowInfoTracker trong Jetpack WindowManager hiển thị thông tin về bố cục cửa sổ. Phương thức windowLayoutInfo() của giao diện sẽ trả về một luồng dữ liệu WindowLayoutInfo để thông báo cho ứng dụng của bạn về trạng thái của thiết bị có thể gập lại. Phương thức WindowInfoTracker getOrCreate() tạo một bản sao của WindowInfoTracker.

WindowManager hỗ trợ thu thập dữ liệu WindowLayoutInfo bằng cách sử dụng Luồng Kotlin và lệnh gọi lại Java.

Luồng Kotlin

Để bắt đầu và dừng quá trình thu thập dữ liệu WindowLayoutInfo, bạn có thể sử dụng coroutine có thể bắt đầu lại vòng đời, trong đó khối mã repeatOnLifecycle được thực thi khi vòng đời tối thiểu là STARTED và đã dừng khi vòng đời là STOPPED. Việc thực thi khối mã sẽ tự động bắt đầu lại khi vòng đời là STARTED trở lại. Trong ví dụ sau, khối mã thu thập và sử dụng dữ liệu 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.
                    }
            }
        }
    }
}

Các lệnh gọi lại Java

Lớp khả năng tương thích của lệnh gọi lại có trong phần phụ thuộc androidx.window:window-java cho phép bạn thu thập nội dung cập nhật của WindowLayoutInfo mà không cần dùng Luồng Kotlin. Cấu phần phần mềm bao gồm lớp WindowInfoTrackerCallbackAdapter, điều chỉnh WindowInfoTracker để hỗ trợ lệnh gọi lại (và hủy đăng ký) để nhận các bản cập nhật WindowLayoutInfo, ví dụ:

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.
           });
       }
   }
}

Hỗ trợ RxJava

Nếu đã sử dụng RxJava (phiên bản 2 hoặc 3), bạn có thể tận dụng các cấu phần mềm cho phép bạn sử dụng Observable hoặc Flowable để thu thập nội dung cập nhật WindowLayoutInfo mà không cần dùng Luồng Kotlin.

Lớp tương thích do phần phụ thuộc androidx.window:window-rxjava2androidx.window:window-rxjava3 cung cấp bao gồm phương thức WindowInfoTracker#windowLayoutInfoFlowable()WindowInfoTracker#windowLayoutInfoObservable(), cho phép ứng dụng của bạn nhận được thông tin cập nhật WindowLayoutInfo, ví dụ:

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 the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

Tính năng của màn hình có thể gập

Lớp WindowLayoutInfo của Jetpack WindowsManager cung cấp các tính năng của cửa sổ hiển thị dưới dạng danh sách các phần tử DisplayFeature.

FoldingFeature là một loại DisplayFeature cung cấp thông tin về màn hình có thể gập lại, bao gồm các loại sau:

  • state: Trạng thái gập của thiết bị, FLAT hoặc HALF_OPENED
  • orientation: Hướng của màn hình đầu tiên hoặc bản lề, HORIZONTAL hoặc VERTICAL
  • occlusionType: Liệu màn hình gập hay bản lề có che khuất một phần màn hình hay không, NONE hay FULL
  • isSeparating: Góc hoặc bản lề tạo ra hai khu vực hiển thị hợp lý, đúng hay sai

Một thiết bị có thể gập lại được HALF_OPENED luôn báo cáo isSeparating là đúng vì màn hình được tách thành hai khu vực hiển thị. Ngoài ra, isSeparating luôn đúng trên thiết bị màn hình kép khi ứng dụng kéo dài trên cả hai màn hình.

Thuộc tính FoldingFeature bounds (kế thừa từ DisplayFeature) đại diện cho hình chữ nhật ranh giới của một tính năng gập lại, chẳng hạn như đường gập hoặc bản lề. Bạn có thể dùng các giới hạn để định vị các thành phần trên màn hình so với đối tượng.

Sử dụng FoldingFeature state để xác định xem thiết bị đang ở chế độ trên mặt bàn hay tư thế sách, và tùy chỉnh bố cục ứng dụng của bạn cho phù hợp, chẳng hạn như:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        // The block passed to repeatOnLifecycle is executed when the lifecycle
        // is at least STARTED and is cancelled when the lifecycle is STOPPED.
        // It automatically restarts the block when the lifecycle is STARTED again.
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from windowInfoRepo 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()
                        .firstOrNull()
                    when {
                            isTableTopPosture(foldingFeature) ->
                                enterTabletopMode(foldingFeature)
                            isBookPosture(foldingFeature) ->
                                enterBookMode(foldingFeature)
                            isSeparating(foldingFeature) ->
                            // Dual-screen device
                            if (foldingFeature.orientation == HORIZONTAL) {
                                enterTabletopMode(foldingFeature)
                            } else {
                                enterBookMode(foldingFeature)
                            }
                            else ->
                                enterNormalMode()
                        }
                }

        }
    }
}

@OptIn(ExperimentalContracts::class)
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

@OptIn(ExperimentalContracts::class)
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

@OptIn(ExperimentalContracts::class)
fun isSeparating(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating
}

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) {
                if (isTableTopPosture((FoldingFeature) feature)) {
                    enterTabletopMode(feature);
                } else if (isBookPosture((FoldingFeature) feature)) {
                    enterBookMode(feature);
                } else if (isSeparating((FoldingFeature) feature)) {
                    // Dual-screen device
                    if (((FoldingFeature) feature).getOrientation() ==
                              FoldingFeature.Orientation.HORIZONTAL) {
                        enterTabletopMode(feature);
                    } else {
                        enterBookMode(feature);
                    }
                } else {
                    enterNormalMode();
                }
            }
        }
    }
}

private boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

private boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

private boolean isSeparating(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.FLAT) &&
           (foldFeature.isSeparating() == true);
}

Trên thiết bị có hai màn hình, hãy sử dụng các bố cục được thiết kế cho tư thế trên mặt bàn và sách ngay cả khi trạng thái của FoldingFeatureFLAT.

Không đặt các thành phần điều khiển trên giao diện người dùng quá gần với màn hình đầu tiên hoặc bản lề khi isSeparating đúng vì các thành phần điều khiển có thể khó tiếp cận. Dùng occlusionType để quyết định xem có đặt nội dung trong tính năng gập bounds hay không.

Thay đổi kích thước cửa sổ

Khu vực hiển thị của ứng dụng có thể thay đổi do thay đổi cấu hình thiết bị — ví dụ: khi thiết bị được gập lại hoặc gập lại, xoay hoặc cửa sổ được thay đổi kích thước ở chế độ nhiều cửa sổ.

Lớp Jetpack WindowManager WindowMetricsCalculator cho phép bạn truy xuất các chỉ số thời lượng hiện tại và tối đa. Giống như nền tảng WindowMetrics được giới thiệu trong API cấp 30, WindowManager WindowMetrics cung cấp các giới hạn cửa sổ, nhưng API tương thích ngược với API cấp 14.

Sử dụng WindowMetrics trong phương thức onCreate() hoặc onConfigurationChanged() của một hoạt động để định cấu hình bố cục của ứng dụng cho kích thước cửa sổ hiện tại, ví dụ:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this@MainActivity)
    val bounds = windowMetrics.getBounds()
    ...
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    final WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this);
    final Rect bounds = windowMetrics.getBounds();
    ...
}

Hãy xem thêm về Hỗ trợ nhiều kích thước màn hình.

Tài nguyên khác

Mẫu

  • Jetpack WindowManager: Ví dụ về cách sử dụng thư viện Jetpack WindowsManager
  • Jetcaster: Triển khai chế độ trên mặt bàn bằng Soạn thư

Lớp học lập trình