在折疊式裝置上,未折疊的大型螢幕和獨特的折疊狀態可以提供新的使用者體驗。如要讓應用程式適用於折疊式裝置,您可以使用 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
(版本 2
或 3
),您可以利用可讓您使用 Observable
或 Flowable
的特定構件以收集 WindowLayoutInfo
更新,無需使用 Kotlin Flow。
androidx.window:window-rxjava2
和 androidx.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
:裝置折疊狀態,可為FLAT
或HALF_OPENED
orientation
:折疊或轉軸方向,可為HORIZONTAL
或VERTICAL
occlusionType
:折疊或轉軸是否會擋住部分螢幕,可為NONE
或FULL
isSeparating
:折疊或轉軸是否會建立兩個邏輯顯示區域,可為 true 或 false
HALF_OPENED
的折疊式裝置會一律回報 isSeparating
為 true,因為裝置螢幕會分割成兩個顯示區域。如果雙螢幕裝置的應用程式橫跨兩個螢幕,則 isSeparating
也一律為 true。
FoldingFeature
bounds
屬性繼承自 DisplayFeature
,代表折疊或轉軸等折疊功能的矩形界框。此界框可用來按照這項功能在螢幕上置放元素。
使用 FoldingFeature
state
判斷裝置處於桌面型態或書本型態,再據此自訂應用程式版面配置,範例如下:
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); }
在雙螢幕裝置上,即使 FoldingFeature
狀態為 FLAT
,也請使用為桌面及書本型態設計的版面配置。
如果 isSeparating
為 true,請勿在折疊或轉軸附近置放 UI 控制項,因為這樣無法輕鬆使用控制項。請使用 occlusionType
,決定是否要把內容置放在折疊功能 bounds
範圍內。
視窗大小變化
應用程式的顯示區域可隨著裝置設定而變化,例如裝置處於摺疊、未摺疊或旋轉狀態,或某個視窗在多視窗模式下重新調整了大小。
您可以利用 Jetpack WindowManager WindowMetricsCalculator
類別擷取目前和最大的視窗指標。如同 API 級別 30 所導入的平台 WindowMetrics
,WindowManager WindowMetrics
也能提供視窗界框,但是 API 可以回溯相容到 API 級別 14。
以活動的 onCreate()
或 onConfigurationChanged()
方法使用 WindowMetrics
,以便用目前的視窗大小設定應用程式版面配置,範例如下:
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(); ... }
也請您參閱支援不同的螢幕大小。
其他資源
範例
- Jetpack WindowManager:Jetpack WindowManager 程式庫的使用方式範例
- Jetcaster:使用 Compose 實作桌面模式