Telas grandes desdobradas e estados dobrados exclusivos permitem novas experiências do usuário em dispositivos dobráveis. Para que o app reconheça um dispositivo dobrável, use a biblioteca Jetpack WindowManager, que tem uma superfície de API para recursos de janela de dispositivos dobráveis, como articulações e dobras. Quando o app reconhece dobras, ele pode adaptar o layout para evitar colocar conteúdo importante na área delas ou de articulações, além de poder usar as dobras e articulações como separadores naturais.
Informações da janela
A interface WindowInfoTracker
na Jetpack WindowManager expõe informações de layout de janelas. O método windowLayoutInfo()
da interface retorna um fluxo de dados do WindowLayoutInfo
que informa ao app sobre o estado da dobra de um dispositivo dobrável. O método getOrCreate()
do WindowInfoTracker
cria uma instância do WindowInfoTracker
.
A WindowManager permite coletar dados WindowLayoutInfo
usando fluxos do Kotlin e callbacks do Java.
Fluxos do Kotlin
Para iniciar e interromper a coleta de dados de WindowLayoutInfo
, use uma corrotina reiniciável que reconhece o ciclo de vida, em que o bloco de código repeatOnLifecycle
é executado quando o ciclo de vida é de pelo menos STARTED
(iniciado) e interrompido quando o ciclo de vida é STOPPED
(parado). A execução do bloco de código é reiniciada automaticamente quando o ciclo de vida é STARTED
(iniciado) novamente. No exemplo abaixo, o bloco de código coleta e usa dados de 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.
}
}
}
}
}
Callbacks do Java
A camada de compatibilidade de callback incluída na dependência androidx.window:window-java
permite coletar atualizações de WindowLayoutInfo
sem usar um fluxo Kotlin. O artefato inclui a classe WindowInfoTrackerCallbackAdapter
, que adapta um WindowInfoTracker
para oferecer suporte ao registro (e ao cancelamento) de callbacks para receber atualizações de WindowLayoutInfo
, por exemplo:
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.
});
}
}
}
Suporte ao RxJava
Se você já usa o RxJava
(versão 2
ou 3
), pode aproveitar os artefatos que permitem usar Observable
ou Flowable
para coletar atualizações de WindowLayoutInfo
sem usar um fluxo Kotlin (links em inglês).
A camada de compatibilidade fornecida pelas dependências de androidx.window:window-rxjava2
e androidx.window:window-rxjava3
inclui os métodos WindowInfoTracker#windowLayoutInfoFlowable()
e WindowInfoTracker#windowLayoutInfoObservable()
, que permitem que o app receba atualizações de WindowLayoutInfo
, por exemplo:
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()
}
}
Recursos de telas dobráveis
A classe WindowLayoutInfo
da Jetpack WindowManager disponibiliza os recursos de uma janela de exibição como uma lista de elementos DisplayFeature
.
Um FoldingFeature
é um tipo de DisplayFeature
que fornece informações sobre telas dobráveis, incluindo o seguinte:
state
: o estado dobrado do dispositivo,FLAT
ouHALF_OPENED
.orientation
: a orientação da dobra ou articulação,HORIZONTAL
ouVERTICAL
.occlusionType
: indica se a dobra ou articulação oculta parte da tela,NONE
ouFULL
.isSeparating
: se a dobra ou articulação cria duas áreas de exibição lógicas ou não, "true" ou "false".
Um dispositivo dobrável que está HALF_OPENED
sempre informa isSeparating
como "true" porque a tela é separada em duas áreas de exibição. Além disso, isSeparating
é sempre "true" em um dispositivo de tela dupla quando o app abrange as duas telas.
A propriedade FoldingFeature
bounds
(herdada de DisplayFeature
) representa o retângulo delimitador de um recurso dobrável, como uma dobra ou articulação. Os limites podem ser usados para posicionar elementos na tela em relação ao recurso.
Use FoldingFeature
state
para determinar se o dispositivo está na posição de mesa ou livro e personalize o layout do app de acordo, por exemplo:
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); }
Em dispositivos de tela dupla, use layouts projetados para posturas de mesa e livro mesmo que o estado FoldingFeature
seja FLAT
.
Não coloque os controles da IU muito perto de uma dobra ou articulação quando isSeparating
for "true" porque os controles podem ser difíceis de alcançar. Use occlusionType
para decidir se você quer inserir conteúdo no recurso dobrável bounds
.
Mudanças no tamanho das janelas
A área de exibição de um app pode mudar devido a uma modificação na configuração do dispositivo. Por exemplo, quando o dispositivo é dobrado, desdobrado, girado ou uma janela é redimensionada no modo de várias janelas.
A classe WindowMetricsCalculator
da Jetpack WindowManager permite extrair as métricas atuais e máximas da janela. Semelhante à plataforma WindowMetrics
introduzida no nível 30 da API, a WindowMetrics
da WindowManager fornece os limites de janela, mas a API é compatível com versões anteriores até o nível 14 da API.
Use WindowMetrics
no método onCreate()
ou onConfigurationChanged()
de uma atividade para configurar o layout do app como o tamanho atual da janela, por exemplo:
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(); ... }
Consulte também Suporte a tamanhos de tela diferentes.
Outros recursos
Exemplos (em inglês)
- Jetpack WindowManager: exemplo de como usar a biblioteca WindowManager do Jetpack.
- Jetcaster: implementação da postura de mesa com o Compose.