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

在折疊式裝置上,未折疊的大型螢幕和獨特的折疊狀態可以提供新的使用者體驗。如要讓應用程式適用於折疊式裝置,您可以使用 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 Flow。

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 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 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()
                    // 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 物件包含的資訊,應用程式可以支援桌面模式等型態,亦即手機坐在平面上、轉軸處於水平位置,且折疊式螢幕開啟的一半。

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

桌面模式的影片播放器應用程式

使用 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);
}

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

範例

書籍模式

另一種獨特的折疊式型態是書籍模式,其中半部為裝置開啟,轉軸為垂直方向。書籍模式適合用來閱讀電子書。在大螢幕折疊式裝置上採用雙頁版面配置,如同一本繫結書籍,書籍模式可記錄閱讀實際書籍的體驗。

如果你想在不用手持方式拍照時拍攝各種長寬比,可以使用這項功能。

實作書籍模式,採用用於免手持模式的相同技巧。唯一的差別在於程式碼應檢查折疊功能方向是否為垂直,而非水平:

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 所導入的平台 WindowMetrics,WindowManager WindowMetrics 也能提供視窗邊界,但是 API 可以回溯相容到 API 級別 14。

如要瞭解如何支援不同的視窗大小,請參閱「支援不同的螢幕大小」。

其他資源

範例

程式碼研究室