讓應用程式適用於折疊式裝置

在摺疊式裝置上,未摺疊的大型螢幕和獨特的摺疊狀態可以提供新的使用者體驗。如要為應用程式採用摺疊機制,請使用 Jetpack WindowManager 程式庫,為摺疊和轉軸等摺疊式裝置視窗功能提供 API 介面。採用摺疊機制的應用程式可以調整版面配置,避免在摺疊和轉軸區域放置重要內容,並能利用螢幕摺線和轉軸做為自然區隔機制。

瞭解裝置是否支援桌面型態或書本型態等設定,有助於決定支援不同版面配置或提供特定功能。

視窗資訊

Jetpack WindowManager 的 WindowInfoTracker 介面可以公開視窗版面配置資訊。此介面的 windowLayoutInfo() 方法會傳回一連串的 WindowLayoutInfo 資料,告知應用程式摺疊式裝置的摺疊狀態。WindowInfoTracker#getOrCreate() 方法會建立 WindowInfoTracker 的例項。

WindowManager 支援使用 Kotlin 流程和 Java 回呼收集 WindowLayoutInfo 資料。

Kotlin 資料流

如要開始及停止收集 WindowLayoutInfo 資料,可使用可重新啟動的生命週期感知協同程式,當生命週期至少 STARTED 時就會執行 repeatOnLifecycle 程式碼區塊,並在生命週期為 STOPPED 時停止。當生命週期再度為 STARTED 時,系統會自動重新開始執行程式碼區塊。在以下範例中,程式碼區塊會收集並使用 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.
                    }
            }
        }
    }
}

Java 回呼

有了 androidx.window:window-java 依附元件內附的回呼相容性層,不必使用 Kotlin 資料流就能收集 WindowLayoutInfo 更新資訊。構件包含 WindowInfoTrackerCallbackAdapter 類別,這個類別會自動調整 WindowInfoTracker 以支援註冊 (及取消註冊) 回呼,以接收 WindowLayoutInfo 的更新,例如:

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

RxJava 支援

如果您已在使用 RxJava (版本 23),請善用可讓您使用 ObservableFlowable 的特定構件,以收集 WindowLayoutInfo 更新,無需使用 Kotlin 資料流。

androidx.window:window-rxjava2androidx.window:window-rxjava3 依附元件提供的相容性層包含 WindowInfoTracker#windowLayoutInfoFlowable()WindowInfoTracker#windowLayoutInfoObservable() 方法,可讓應用程式接收 WindowLayoutInfo 更新,例如:

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

折疊式裝置螢幕功能

Jetpack WindowManager 的 WindowLayoutInfo 類別可用 DisplayFeature 元素清單形式提供顯示視窗功能。

FoldingFeature 是一種 DisplayFeature,可以提供摺疊式裝置螢幕相關資訊,包括:

  • state:裝置摺疊狀態,可為 FLATHALF_OPENED

  • orientation:摺疊或轉軸方向,可為 HORIZONTALVERTICAL

  • occlusionType:摺疊或轉軸是否會擋住部分螢幕,可為 NONEFULL

  • isSeparating:摺疊或轉軸是否會建立兩個邏輯顯示區域,可為 true 或 false

處於 HALF_OPENED 狀態的摺疊式裝置一律會將 isSeparating 回報為 true,因為裝置螢幕會分隔成兩個顯示區域。此外,如果應用程式在雙螢幕裝置上橫跨兩個螢幕顯示,則 isSeparating 也會一律為 true。

FoldingFeature bounds 屬性繼承自 DisplayFeature,代表摺疊或轉軸等摺疊功能的矩形界框。此界框可用來將元素放置在螢幕上相對於此功能的位置:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker 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<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }

        }
    }
}

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) {
                // Use information from the feature object.
            }
        }
    }
}

桌面型態

運用 FoldingFeature 物件提供的資訊,應用程式可以支援桌面模式等各種型態,亦即手機穩定放置在平面上、轉軸處於橫向位置,以及摺疊式螢幕半開。

桌面模式方便使用者操作手機,不必拿著手機。桌面模式非常適合用來觀看媒體內容、拍照及進行視訊通話。

圖 1. 桌面模式下的影片播放器應用程式。

使用 FoldingFeature.StateFoldingFeature.Orientation 判斷裝置是否處於桌面型態:

Kotlin

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

Java

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

確認裝置處於桌面型態後,請據此更新應用程式版面配置。以媒體應用程式來說,通常是指將播放內容放在上螢幕,並將控制項和補充內容放在下螢幕,讓使用者不必動手就能欣賞影音內容。

在 Android 15 (API 級別 35) 以上版本中,您可以叫用同步 API,無論裝置目前的狀態為何,都能偵測裝置是否支援桌面模式。

API 提供了裝置支援的防護機制清單。如果清單包含桌面姿勢,您可以分割應用程式版面配置來支援該姿勢,並針對桌面和全螢幕版面配置,在應用程式 UI 上執行 A/B 測試。

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

範例

書本型態

另一種獨特的摺疊型態是書本型態,亦即裝置半開,轉軸為垂直方向。書本型態適合用來閱讀電子書。在大螢幕摺疊式裝置上利用雙頁版面配置閱讀書籍,呈現翻開實體精裝書的感觸。

此外,如果想使用免持方式拍攝不同顯示比例的相片,也可以使用這項功能。

實作書本模式使用的技術與桌面模式相同。唯一的差別在於程式碼應檢查摺疊功能螢幕方向是否為垂直,而非水平:

Kotlin

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

Java

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

視窗大小變化

應用程式的顯示區域可隨著裝置設定而變化,例如裝置處於摺疊、展開或旋轉狀態,或是在多視窗模式下重新調整視窗大小。

您可以利用 Jetpack WindowManager WindowMetricsCalculator 類別擷取目前和最大的視窗指標。如同 API 級別 30 中導入的平台 WindowMetricsWindowManager WindowMetrics 也能提供視窗界框,但這個 API 可以回溯相容至 API 級別 14。

請參閱「使用視窗大小類別」。

其他資源

範例

  • Jetpack WindowManager:Jetpack WindowManager 程式庫的使用範例
  • Jetcaster:使用 Compose 實作桌面型態

程式碼研究��