折りたたみ式デバイスでは、開いた状態の大型ディスプレイと独自の折りたたみ状態により、新しいユーザー エクスペリエンスを実現します。アプリを折りたたみ対応にするには、Jetpack WindowManager ライブラリを使用します。このライブラリは、折りたたみ式デバイスの折り目やヒンジなどのウィンドウ機能用の API サーフェスを提供します。アプリを折りたたみ対応にすると、レイアウトを調整して、重要なコンテンツが折り目またはヒンジの領域に配置されることを回避したり、折り目またはヒンジを自然な区切り要素として使用したりすることができます。
ウィンドウ情報
Jetpack WindowManager の WindowInfoTracker
インターフェースは、ウィンドウ レイアウト情報を公開します。このインターフェースの windowLayoutInfo()
メソッドは、折りたたみ式デバイスの折りたたみ状態をアプリに通知する WindowLayoutInfo
データのストリームを返します。WindowInfoTracker
の getOrCreate()
メソッドは、WindowInfoTracker
のインスタンスを作成します。
WindowManager は、Kotlin Flow と Java コールバックを使用した WindowLayoutInfo
データの収集をサポートしています。
Kotlin Flow
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 Flow を使用せずに 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
を使用して、Kotlin Flow を使用せずに WindowLayoutInfo
の更新を収集するアーティファクトを利用できます。
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
: 折り目またはヒンジが 2 つの論理ディスプレイ領域を生成するかどうか(true または false)
HALF_OPENED
状態の折りたたみ式デバイスは、画面が 2 つのディスプレイ領域に分割されているため、常に 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 によるテーブルトップ形状の実装