Les grands écrans déployés et les positions pliées uniques offrent de nouvelles expériences utilisateur sur les appareils pliables. Pour que votre application devienne pliable, utilisez la bibliothèque Jetpack WindowManager, qui fournit une surface d'API pour les caractéristiques des fenêtres des appareils pliables telles que les plis et les charnières. Une application pliable peut adapter sa mise en page pour éviter de placer du contenu important dans les zones des plis ou des charnières et les utiliser comme séparateurs naturels.
Informations sur la fenêtre
L'interface WindowInfoTracker
de Jetpack WindowManager présente des informations sur la mise en page de la fenêtre. La méthode windowLayoutInfo()
de l'interface renvoie un flux de données WindowLayoutInfo
qui informe votre application de la position pliée d'un appareil pliable. La méthode WindowInfoTracker
getOrCreate()
crée une instance de WindowInfoTracker
.
WindowManager contribue à la collecte des données WindowLayoutInfo
à l'aide de flux Kotlin et de rappels Java.
Flux Kotlin
Pour démarrer et arrêter la collecte de données WindowLayoutInfo
, vous pouvez utiliser une coroutine redémarrable sensible au cycle de vie, dans laquelle le bloc de code repeatOnLifecycle
est exécuté lorsque le cycle de vie présente au moins la valeur STARTED
et arrêté lorsque le cycle de vie présente l'état STOPPED
. L'exécution du bloc de code redémarre automatiquement lorsque l'état du cycle de vie est à nouveau STARTED
. Dans l'exemple suivant, le bloc de code collecte et utilise les données 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.
}
}
}
}
}
Rappels Java
La couche de compatibilité de rappel incluse dans la dépendance androidx.window:window-java
vous permet de collecter des mises à jour WindowLayoutInfo
sans utiliser de flux Kotlin. L'artefact inclut la classe WindowInfoTrackerCallbackAdapter
, qui adapte un WindowInfoTracker
pour accepter l'abonnement (et le désabonnement) à des rappels de mises à jour WindowLayoutInfo
, par exemple :
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.
});
}
}
}
Compatibilité avec RxJava
Si vous utilisez déjà RxJava
(version 2
ou 3
), vous pouvez utiliser les artefacts qui vous permettent d'utiliser un Observable
ou Flowable
pour collecter les mises à jour WindowLayoutInfo
sans utiliser de flux Kotlin.
La couche de compatibilité fournie par les dépendances androidx.window:window-rxjava2
et androidx.window:window-rxjava3
inclut les méthodes WindowInfoTracker#windowLayoutInfoFlowable()
et WindowInfoTracker#windowLayoutInfoObservable()
, qui permettent à votre application de recevoir des mises à jour WindowLayoutInfo
, par exemple :
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()
}
}
Caractéristiques des écrans pliables
La classe WindowLayoutInfo
de Jetpack WindowManager rend les caractéristiques d'une fenêtre d'affichage disponibles sous la forme d'une liste d'éléments DisplayFeature
.
Un FoldingFeature
est un type de DisplayFeature
qui fournit des informations sur les écrans pliables, par exemple :
state
: position pliée de l'appareil (FLAT
ouHALF_OPENED
).orientation
: orientation du pli ou de la charnière (HORIZONTAL
ouVERTICAL
).occlusionType
: indique si le pli ou la charnière masque une partie de l'écran (NONE
ouFULL
).isSeparating
: indique si le pli ou la charnière crée deux zones d'affichage logiques (vrai ou faux).
Un appareil pliable HALF_OPENED
signale toujours isSeparating
comme vrai, car l'écran est divisé en deux zones d'affichage. De plus, isSeparating
est toujours vrai sur un appareil à double écran lorsque l'application recouvre les deux écrans.
La propriété bounds
FoldingFeature
(héritée de DisplayFeature
) représente le rectangle de délimitation d'une caractéristique de pliage telle qu'un pli ou une charnière. Les limites peuvent être utilisées pour positionner des éléments à l'écran par rapport à la caractéristique.
Utilisez state
FoldingFeature
pour déterminer si l'appareil est posé à plat ou debout, puis personnalisez en conséquence la mise en page de votre application, par exemple :
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); }
Sur les appareils à double écran, utilisez des mises en page qui s'adaptent aux positions à plat et debout, même si l'état FoldingFeature
est FLAT
.
Ne placez pas les commandes d'interface utilisateur trop près d'un pli ou d'une charnière lorsque isSeparating
est défini sur "vrai", car les commandes peuvent être difficiles à atteindre. Utilisez occlusionType
pour déterminer si le contenu doit se trouver dans la caractéristique de pliage bounds
.
Changements de taille de la fenêtre
La zone d'affichage d'une application peut changer en raison d'une modification de la configuration de l'appareil, par exemple lorsque l'appareil est plié ou déployé, qu'il pivote ou lorsqu'une fenêtre est redimensionnée en mode multifenêtre.
La classe Jetpack WindowManager WindowMetricsCalculator
vous permet de récupérer les métriques de la fenêtre actuelle et maximale. Comme les WindowMetrics
de la plate-forme introduites dans le niveau d'API 30, les WindowMetrics
de WindowManager fournissent les limites de fenêtre, mais l'API est rétrocompatible jusqu'au niveau d'API 14.
Utilisez les WindowMetrics
dans la méthode onCreate()
ou onConfigurationChanged()
d'une activité pour configurer la mise en page de votre application pour la taille de fenêtre actuelle, par exemple :
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(); ... }
Consultez également la section Compatibilité avec différentes tailles d'écran.
Ressources supplémentaires
Exemples
- Jetpack WindowManager : Exemple d'utilisation de la bibliothèque Jetpack WindowManager
- Jetcaster : Intégration d'une position à plat avec Compose