Incorporação avançada de atividades

1. Introdução

A incorporação de atividades, apresentada no Android 12L (nível 32 da API), permite que apps baseados em atividade mostrem várias atividades ao mesmo tempo em telas grandes para criar layouts de dois painéis, como detalhes e listas.

O codelab Criar um layout de detalhes e listas com incorporação de atividades e Material Design abordou como usar chamadas de API XML ou do Jetpack WindowManager para criar um layout de detalhes e listas.

Este codelab orienta você sobre alguns recursos recém-lançados para incorporação de atividades, que melhoram ainda mais a experiência no app em dispositivos de tela grande. Os recursos incluem expansão do painel, fixação de atividades e escurecimento da tela cheia para caixas de diálogo.

Pré-requisitos

O que você vai aprender

Como:

  • Ativar a expansão do painel
  • Implementar a fixação de atividades com uma das janelas divididas
  • Usar o escurecimento da tela cheia para caixas de diálogo

O que é necessário

  • Versão recente do Android Studio
  • Smartphone Android ou emulador com o Android 15
  • Tablet grande ou emulador do Android com largura mínima maior que 600 dp

2. Configurar

Baixar o app de exemplo

Etapa 1: clonar o repositório

Clone o repositório Git de codelabs em tela grande:

git clone https://github.com/android/large-screen-codelabs

Ou faça o download e desarquive o arquivo ZIP dos codelabs em tela grande:

Baixar o código-fonte

Etapa 2: inspecionar os arquivos de origem do codelab

Navegue até a pasta activity-embedding-advanced.

Etapa 3: abrir o projeto do codelab

No Android Studio, abra o projeto em Kotlin ou Java.

Lista de arquivos da pasta de atividades no repositório e no arquivo ZIP.

A pasta activity-embedding-advanced no repositório e no arquivo ZIP contém dois projetos do Android Studio: um em Kotlin e outro em Java. Abra o projeto desejado. Os snippets do codelab são fornecidos nas duas linguagens.

Criar dispositivos virtuais

Se você não tiver um smartphone Android, um tablet pequeno ou um tablet grande no nível 35 da API ou mais recente, abra o Gerenciador de dispositivos no Android Studio e crie um dos seguintes dispositivos virtuais necessários:

  • Smartphone: Pixel 8, nível 35 da API ou mais recente
  • Tablet: Pixel Tablet, nível 35 da API ou mais recente

3. Executar o app

O app de exemplo mostra uma lista de itens. Quando o usuário seleciona um item, o app exibe informações sobre o item.

O app consiste em três atividades:

  • ListActivity: contém uma lista de itens em uma RecyclerView.
  • DetailActivity: mostra informações sobre um item da lista quando ele é selecionado.
  • SummaryActivity: mostra um resumo das informações quando o item Resumo é selecionado.

Continuar do codelab anterior

No codelab Criar um layout de detalhes e listas com incorporação de atividades e Material Design, desenvolvemos um aplicativo com uma visualização de detalhes e listas usando a incorporação de atividades com navegação facilitada por uma coluna de navegação e uma barra de navegação na parte de baixo.

  1. Execute o app em um tablet grande ou no emulador do Pixel no modo retrato. Você vai ver a tela de lista principal e uma barra de navegação na parte de baixo.

74906232acad76f.png

  1. Gire o tablet na horizontal (paisagem). A tela vai se dividir, mostrando a lista de um lado e os detalhes do outro. A barra de navegação na parte de baixo precisa ser substituída por uma coluna de navegação vertical.

dc6a7d1c02c49cd4.png

Novos recursos com incorporação de atividades

Tudo pronto para melhorar seu layout de painel duplo? Neste codelab, vamos adicionar alguns recursos novos e interessantes para melhorar a experiência dos usuários. Confira o que vamos criar:

  1. Vamos tornar esses painéis dinâmicos. Vamos implementar a expansão de painéis, permitindo que os usuários redimensionem (ou expandam) os painéis para ter uma visualização personalizada.

2ec5f7fd6df5d8cd.gif

  1. Vamos dar aos usuários o poder de priorizar! Com a fixação de atividades, os usuários podem manter as tarefas mais importantes sempre na tela.

980d0033972737ed.gif

  1. Precisa se concentrar em uma tarefa específica? Vamos adicionar um recurso de escurecimento da tela cheia para eliminar distrações e permitir que os usuários se concentrem no que é mais importante.

2d3455e0f8901f95.png

4. Expansão do painel

Ao usar um layout de painel duplo em uma tela grande, em muitos casos, os usuários precisam se concentrar em um dos painéis divididos enquanto mantêm o outro na tela. Por exemplo, lendo artigos em um lado e mantendo uma lista de conversas de chat no outro. É comum que os usuários queiram redimensionar os painéis para se concentrar em uma atividade.

Para esse objetivo, a incorporação de atividades adiciona uma nova API que permite aos usuários mudar a proporção de divisão e personalizar a transição de redimensionamento.

Adicionar dependência

Primeiro, adicione o WindowManager 1.4 ao arquivo build.gradle.

Observação: alguns dos recursos desta biblioteca funcionam apenas no Android 15 (nível 35 da API) e em versões mais recentes.

build.gradle

 implementation 'androidx.window:window:1.4.0-alpha02'

Personalizar o divisor de janela

Crie uma instância de DividerAttributes e adicione-a a SplitAttributes. Esse objeto configura o comportamento geral do layout dividido. É possível usar as propriedades de cor, largura e intervalo de arrasto de DividerAttributes para melhorar a experiência do usuário.

Personalizar o divisor:

  1. Confira o nível da API Extensions do WindowManager. Como o recurso de expansão do painel está disponível apenas no nível 6 da API e em versões mais recentes, isso também se aplica ao restante dos novos recursos.
  2. Crie DividerAttributes: para estilizar o divisor entre os painéis, crie um objeto DividerAttributes. Esse objeto permite definir:
  • color: muda a cor do divisor para que ele combine com o tema do app ou crie um contraste visual.
  • widthDp: ajusta a largura do divisor para melhorar a visibilidade ou ter uma aparência mais sutil.
  1. Adicione SplitAttributes: depois de personalizar o divisor, adicione-o ao objeto DividerAttributes.
  2. Defina o intervalo de arrasto (opcional): você também pode controlar até onde os usuários podem arrastar o divisor para redimensionar os painéis.
  • DRAG_RANGE_SYSTEM_DEFAULT: use esse valor especial para permitir que o sistema determine um intervalo de arrasto adequado com base no tamanho da tela e no formato do dispositivo.
  • Valor personalizado (entre 0,33 e 0,66): defina seu próprio intervalo de arrasto para limitar até que ponto os usuários podem redimensionar os painéis. Se o usuário arrastar além desse limite, o layout dividido será desativado.

Substitua splitAttributes pelo código abaixo.

SplitManager.kt

val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
   .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
   .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)

if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
   splitAttributesBuilder.setDividerAttributes(
       DividerAttributes.DraggableDividerAttributes.Builder()
           .setColor(getColor(context, R.color.divider_color))
           .setWidthDp(4)
           .setDragRange(
               DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
           .build()
   )
}
val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

SplitManager.java

SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    splitAttributesBuilder.setDividerAttributes(
            new DividerAttributes.DraggableDividerAttributes.Builder()
                    .setColor(ContextCompat.getColor(context, R.color.divider_color))
                    .setWidthDp(4)
                    .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
                    .build()
    );
}
SplitAttributes splitAttributes = splitAttributesBuilder.build();

Crie divider_color.xml na pasta res/color com o conteúdo abaixo.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:color="#669df6" />
</selector>

Executar

Tudo pronto. Crie e execute o app de exemplo.

Você vai notar a expansão do painel e ele poderá ser arrastado.

2ec5f7fd6df5d8cd.gif

Mudar a proporção de divisão em versões mais antigas

Observação importante sobre compatibilidade: o recurso de expansão de painel está disponível apenas nas Extensões do WindowManager 6 ou mais recentes, o que significa que você precisa do Android 15 (nível 35 da API) ou mais recente.

No entanto, ainda é importante oferecer uma boa experiência para os usuários em versões mais antigas do Android.

No Android 14 (nível 34 da API) e em versões anteriores, ainda é possível fornecer ajustes dinâmicos de proporção de divisão usando a classe SplitAttributesCalculator. Isso oferece uma maneira de manter algum nível de controle do usuário sobre o layout, mesmo sem a expansão do painel.

a36f8ba4226353c5.gif

Quer saber como usar esses recursos da melhor forma? Vamos abordar todas as práticas recomendadas e dicas na seção "Práticas recomendadas".

5. Fixação de atividades

Você já quis manter uma parte da tela dividida fixa enquanto navegava livremente na outra? Imagine ler um artigo longo em um lado e ainda poder interagir com o conteúdo de outro app na outra metade.

É aí que entra a fixação de atividades. Ela permite fixar uma das janelas divididas para que ela permaneça na tela mesmo enquanto você navega na outra. Isso oferece uma experiência de multitarefas mais focada e produtiva para os usuários.

Adicionar o botão de fixação

Primeiro, vamos adicionar um botão em DetailActivity.. O aplicativo fixa esse DetailActivity quando os usuários clicam no botão.

Faça as seguintes mudanças em activity_detail.xml:

  1. Adicione um ID ao ConstraintLayout
android:id="@+id/detailActivity"
  1. Adicione um botão na parte de baixo do layout
<androidx.appcompat.widget.AppCompatButton
      android:id="@+id/pinButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/pin_this_activity"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
  1. Restrinja a parte de baixo da TextView à parte de cima do botão
app:layout_constraintBottom_toTopOf="@id/pinButton"

Remova esta linha em TextView.

app:layout_constraintBottom_toBottomOf="parent"

Confira o código XML completo do arquivo de layout activity_detail.xml, incluindo o botão PIN THIS ACTIVITY (FIXAR ESTA ATIVIDADE) que acabamos de adicionar:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/detailActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".DetailActivity">

  <TextView
      android:id="@+id/textViewItemDetail"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="36sp"
      android:textColor="@color/obsidian"
      app:layout_constraintBottom_toTopOf="@id/pinButton"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <androidx.appcompat.widget.AppCompatButton
      android:id="@+id/pinButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/pin_this_activity"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Adicione a string pin_this_activity ao arquivo res/values/strings.xml.

<string name="pin_this_activity">PIN THIS ACTIVITY</string>

Conectar o botão de fixação

  1. Declarar a variável: no arquivo DetailActivity.kt, declare uma variável para manter uma referência ao botão PIN THIS ACTIVITY:

DetailActivity.kt

private lateinit var pinButton: Button

DetailActivity.java

private Button pinButton;
  1. Encontre o botão no layout e adicione um callback setOnClickListener().

DetailActivity.kt / onCreate

pinButton = findViewById(R.id.pinButton)
pinButton.setOnClickListener {
 pinActivityStackExample(taskId)
}

DetailActivity.java / onCreate()

Button pinButton = findViewById(R.id.pinButton);
pinButton.setOnClickListener( (view) => {
        pinActivityStack(getTaskId());

});
  1. Crie um novo método com o nome pinActivityStackExample na classe DetailActivity. Vamos implementar a lógica de fixação real aqui.

DetailActivity.kt

private fun pinActivityStackExample(taskId: Int) {

 val splitAttributes: SplitAttributes = SplitAttributes.Builder()
   .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
   .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
   .build()

 val pinSplitRule = SplitPinRule.Builder()
   .setSticky(true)
   .setDefaultSplitAttributes(splitAttributes)
   .build()

 SplitController.getInstance(applicationContext).pinTopActivityStack(taskId, pinSplitRule)
}

DetailActivity.java

private void pinActivityStackExample(int taskId) {
    SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();

    SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();

    SplitController.getInstance(getApplicationContext()).pinTopActivityStack(taskId, pinSplitRule);
}

Observação:

  1. Só é possível fixar uma atividade por vez. Libere a atividade fixada com
unpinTopActivityStack()

antes de fixar outra.

  1. Para ativar a expansão do painel ao fixar a atividade, chame
setDividerAttributes()

para a atividade recém-criada

SplitAttributes

também.

Mudanças na navegação de retorno

Com o WindowManager 1.4, o comportamento da navegação de retorno mudou. O evento de retorno é enviado para a última atividade em foco ao usar a navegação por botões.

Navegação com botões:

  • Com a navegação com botões, o evento de retorno agora é enviado de forma consistente para a última atividade em foco. Isso simplifica o comportamento de navegação de retorno, tornando-o mais previsível para os usuários.

Navegação por gestos:

  • Android 14 (nível 34 da API) e versões anteriores: o gesto de retorno envia o evento para a atividade em que ele ocorreu, o que pode levar a um comportamento inesperado em cenários de tela dividida.
  • Android 15 (nível 35 da API) e versões mais recentes:
  • Atividades no mesmo app: o gesto de retorno sempre encerra a atividade de cima, independente da direção do deslizar, oferecendo uma experiência mais unificada.
  • Atividades de apps diferentes (sobreposição): o evento de retorno vai para a última atividade em foco, semelhante ao comportamento da navegação com botões.

Executar

Crie e execute o app de exemplo.

Fixar a atividade

  • Navegue até a tela DetailActivity.
  • Toque no botão PIN THIS ACTIVITY (FIXAR ATIVIDADE).

980d0033972737ed.gif

6. Escurecimento da tela cheia para caixas de diálogo

Embora a incorporação de atividades facilite os layouts de tela dividida, as caixas de diálogo nas versões anteriores escureciam apenas o contêiner da própria atividade. Isso pode criar uma experiência visual desconexa, especialmente quando você quer que a caixa de diálogo seja o foco.

A solução: WindowManager 1.4

  • Temos recomendações para você. Com o WindowManager 1.4, as caixas de diálogo agora escurecem toda a janela do app por padrão (DimAreaBehavior.Companion.ON_TASK), proporcionando uma sensação mais imersiva e focada.
  • Precisa do comportamento antigo de volta? Sem problemas. Você ainda pode optar por escurecer apenas o contêiner da atividade usando ON_ACTIVITY_STACK.

ON_ACTIVITY_STACK

ON_TASK

Confira como usar o ActivityEmbeddingController para gerenciar o comportamento de escurecimento da tela cheia:

Observação: o escurecimento da caixa de diálogo em tela cheia está disponível com a versão 5 ou mais recente das extensões do WindowManager.

SplitManager.kt / createSplit()

with(ActivityEmbeddingController.getInstance(context)) {
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 5) {
       setEmbeddingConfiguration(
           EmbeddingConfiguration.Builder()
               .setDimAreaBehavior(ON_TASK)
               .build()
       )
   }
}

SplitManager.java / createSplit()

ActivityEmbeddingController controller = ActivityEmbeddingController.getInstance(context);
if (WindowSdkExtensions.getInstance().getExtensionVersion()  >= 5) {
    controller.setEmbeddingConfiguration(
        new EmbeddingConfiguration.Builder()
            .setDimAreaBehavior(EmbeddingConfiguration.DimAreaBehavior.ON_TASK)
            .build()
    );
}

Para mostrar o recurso de escurecimento da tela cheia, vamos usar uma caixa de diálogo de alerta que pede a confirmação do usuário antes de fixar a atividade. Ao aparecer, essa caixa de diálogo escurece toda a janela do aplicativo, não apenas o contêiner em que a atividade está.

DetailActivity.kt

pinButton.setOnClickListener {
 showAlertDialog(taskId)
}

...
private fun showAlertDialog(taskId: Int) {
 val builder = AlertDialog.Builder(this)
 builder.setTitle(getString(R.string.dialog_title))
 builder.setMessage(getString(R.string.dialog_message))
 builder.setPositiveButton(getString(R.string.button_yes)) { _, _ ->
   if (WindowSdkExtensions.getInstance().extensionVersion  >= 6) {
     pinActivityStackExample(taskId)
   }
 }
 builder.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
   // Cancel
 }
 val dialog: AlertDialog = builder.create()
 dialog.show()
}

DetailActivity.java

pinButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       showAlertDialog(getTaskId());
   }
});

...

private void showAlertDialog(int taskId) {
   AlertDialog.Builder builder = new AlertDialog.Builder(this);
   builder.setTitle(getString(R.string.dialog_title));
   builder.setMessage(getString(R.string.dialog_message));

   builder.setPositiveButton(getString(R.string.button_yes), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
               pinActivityStackExample(taskId);
           }
       }
   });
   builder.setNegativeButton(getString(R.string.button_cancel), new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           // Cancel
       }
   });
   AlertDialog dialog = builder.create();
   dialog.show();
}

Adicione as strings abaixo ao arquivo res/values/strings.xml.

<!-- Dialog information -->
<string name="dialog_title">Activity Pinning</string>
<string name="dialog_message">Confirm to pin this activity</string>
<string name="button_yes">Yes</string>
<string name="button_cancel">Cancel</string>

Executar

Crie e execute o app de exemplo.

Clique no botão de fixação da atividade:

  • Uma caixa de diálogo de alerta vai aparecer, pedindo que você confirme a ação de fixação.
  • Observe como toda a tela, incluindo os dois painéis divididos, fica escurecida, dando foco à caixa de diálogo.

2d3455e0f8901f95.png

7. Práticas recomendadas

Permitir que os usuários desativem o layout de painel duplo

Para facilitar a transição para os novos layouts, vamos permitir que os usuários mudem entre as visualizações de painel duplo e de coluna única. Isso pode ser feito usando a SplitAttributesCalculator e SharedPreferences para armazenar as preferências do usuário.

Mudar a proporção de divisão no Android 14 e em versões anteriores

Analisamos a expansão do painel, que é uma ótima maneira de ajustar a proporção de divisão no Android 15 e em versões mais recentes. Mas como podemos oferecer um nível semelhante de flexibilidade aos usuários de versões mais antigas do Android?

Vamos entender como a SplitAttributesCalculator pode ajudar a fazer isso e garantir uma experiência consistente em uma variedade maior de dispositivos.

Confira abaixo um exemplo:

a87452341434c86d.gif

Criar a tela de configurações

Para começar, vamos criar uma tela de configurações dedicada para a configuração do usuário.

Nesta tela de configurações, vamos incorporar uma chave para ativar ou desativar o recurso de incorporação de atividades para todo o aplicativo. Além disso, vamos incluir uma barra de progresso que permite aos usuários ajustar a proporção de divisão do layout de painel duplo. O valor da proporção de divisão só será aplicado se a chave de incorporação de atividades estiver ativada.

Depois que o usuário define valores em SettingsActivity, eles são salvos em SharedPreferences para serem usados mais tarde em outros lugares do aplicativo.

build.gradle

Adicione a dependência de preferência.

implementation 'androidx.preference:preference-ktx:1.2.1' // Kotlin

Ou

implementation 'androidx.preference:preference:1.2.1' // Java

SettingsActivity.kt

package com.example.activity_embedding

import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import androidx.preference.SwitchPreferenceCompat

class SettingsActivity : AppCompatActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.settings_activity)
    if (savedInstanceState == null) {
      supportFragmentManager
        .beginTransaction()
        .replace(R.id.settings, SettingsFragment())
        .commit()
    }
    supportActionBar?.setDisplayHomeAsUpEnabled(true)
  }

  override fun onOptionsItemSelected(item: MenuItem): Boolean {
    if (item.itemId == android.R.id.home) finishActivity()
    return super.onOptionsItemSelected(item)
  }

  private fun finishActivity() { finish() }

  class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
      setPreferencesFromResource(R.xml.root_preferences, rootKey)
findPreference<SwitchPreferenceCompat>("dual_pane")?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue as Boolean) {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(true)
          }
        } else {
          this.activity?.let {
            SharePref(it.applicationContext).setAEFlag(false)
          }
        }
        this.activity?.finish()
        true
      }

      val splitRatioPreference: SeekBarPreference? = findPreference("split_ratio")
      splitRatioPreference?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue is Int) {
          this.activity?.let { SharePref(it.applicationContext).setSplitRatio(newValue.toFloat()/100) }
        }
        true
      }
    }
  }
}

SettingsActivity.java

package com.example.activity_embedding;

import android.os.Bundle;
import android.view.MenuItem;

import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SeekBarPreference;
import androidx.preference.SwitchPreferenceCompat;

public class SettingsActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings_activity);
        if (savedInstanceState == null) {
            getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.settings, new SettingsFragment())
                .commit();
        }
        if (getSupportActionBar() != null) {
            getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == android.R.id.home) {
            finishActivity();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void finishActivity() {
        finish();
    }

    public static class SettingsFragment extends PreferenceFragmentCompat {
        @Override
        public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
            setPreferencesFromResource(R.xml.root_preferences, rootKey);

            SwitchPreferenceCompat dualPanePreference = findPreference("dual_pane");
            if (dualPanePreference != null) {
                dualPanePreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    boolean isDualPane = (Boolean) newValue;
                    if (getActivity() != null) {
                        SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                        sharePref.setAEFlag(isDualPane);
                        getActivity().finish();
                    }
                    return true;
                });
            }

            SeekBarPreference splitRatioPreference = findPreference("split_ratio");
            if (splitRatioPreference != null) {
                splitRatioPreference.setOnPreferenceChangeListener((preference, newValue) -> {
                    if (newValue instanceof Integer) {
                        float splitRatio = ((Integer) newValue) / 100f;
                        if (getActivity() != null) {
                            SharePref sharePref = new SharePref(getActivity().getApplicationContext());
                            sharePref.setSplitRatio(splitRatio);
                        }
                    }
                    return true;
                });
            }
        }
    }
}

Adicionar settings_activity.xml na pasta de layout

settings_activity.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <FrameLayout
       android:id="@+id/settings"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />
</LinearLayout>

Adicione SettingsActivity ao arquivo de manifesto.

<activity
   android:name=".SettingsActivity"
   android:exported="false"
   android:label="@string/title_activity_settings" />

Configure regras de divisão para SettingsActivity.

SplitManager.kt / createSplit()

val settingActivityFilter = ActivityFilter(
   ComponentName(context, SettingsActivity::class.java),
   null
)
val settingActivityFilterSet = setOf(settingActivityFilter)
val settingActivityRule = ActivityRule.Builder(settingActivityFilterSet)
   .setAlwaysExpand(true)
   .build()
ruleController.addRule(settingActivityRule)

SplitManager.java / createSplit()

Set<ActivityFilter> settingActivityFilterSet = new HashSet<>();
ActivityFilter settingActivityFilter = new ActivityFilter(
        new ComponentName(context, SettingsActivity.class),
        null
);
settingActivityFilterSet.add(settingActivityFilter);
ActivityRule settingActivityRule = new ActivityRule.Builder(settingActivityFilterSet)
        .setAlwaysExpand(true).build();
ruleController.addRule(settingActivityRule);

Este é o código para salvar as configurações do usuário em SharedPreferences.

SharedPref.kt

package com.example.activity_embedding

import android.content.Context
import android.content.SharedPreferences

class SharePref(context: Context) {
    private val sharedPreferences: SharedPreferences =
        context.getSharedPreferences("my_app_preferences", Context.MODE_PRIVATE)

    companion object {
        private const val AE_FLAG = "is_activity_embedding_enabled"
        private const val SPLIT_RATIO = "activity_embedding_split_ratio"
        const val DEFAULT_SPLIT_RATIO = 0.3f
    }

    fun setAEFlag(isEnabled: Boolean) {
        sharedPreferences.edit().putBoolean(AE_FLAG, isEnabled).apply()
    }

    fun getAEFlag(): Boolean = sharedPreferences.getBoolean(AE_FLAG, true)

    fun getSplitRatio(): Float = sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO)

    fun setSplitRatio(ratio: Float) {
        sharedPreferences.edit().putFloat(SPLIT_RATIO, ratio).apply()
    }
}

SharedPref.java

package com.example.activity_embedding;

import android.content.Context;
import android.content.SharedPreferences;

public class SharePref {
    private static final String PREF_NAME = "my_app_preferences";
    private static final String AE_FLAG = "is_activity_embedding_enabled";
    private static final String SPLIT_RATIO = "activity_embedding_split_ratio";
    public static final float DEFAULT_SPLIT_RATIO = 0.3f;

    private final SharedPreferences sharedPreferences;

    public SharePref(Context context) {
        this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    public void setAEFlag(boolean isEnabled) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(AE_FLAG, isEnabled);
        editor.apply();
    }

    public boolean getAEFlag() {
        return sharedPreferences.getBoolean(AE_FLAG, true);
    }

    public float getSplitRatio() {
        return sharedPreferences.getFloat(SPLIT_RATIO, DEFAULT_SPLIT_RATIO);
    }

    public void setSplitRatio(float ratio) {
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putFloat(SPLIT_RATIO, ratio);
        editor.apply();
    }
}

Você também precisa de um layout de tela de preferências em XML. Crie root_preferences.xml em res/xml com o código abaixo.

<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:android="http://schemas.android.com/apk/res/android">
   <PreferenceCategory app:title="@string/split_setting_header">

       <SwitchPreferenceCompat
           app:key="dual_pane"
           app:title="@string/dual_pane_title" />

       <SeekBarPreference
           app:key="split_ratio"
           app:title="@string/split_ratio_title"
           android:min="0"
           android:max="100"
           app:defaultValue="50"
           app:showSeekBarValue="true" />
   </PreferenceCategory>
</PreferenceScreen>

E adicione o código abaixo ao arquivo res/values/strings.xml.

<string name="title_activity_settings">SettingsActivity</string>
<string name="split_setting_header">Dual Pane Display</string>
<string name="dual_pane_title">Dual Pane</string>
<string name="split_ratio_title">Split Ratio</string>

Adicionar a SettingsActivity ao menu

Vamos conectar a SettingsActivity recém-criada a um destino de navegação para que possa ser facilmente acessada na interface principal do app pelo usuário.

  1. No arquivo ListActivity, declare variáveis para a barra de navegação de baixo e a barra de navegação à esquerda:

ListActivity.kt

 private lateinit var navRail: NavigationRailView private lateinit var bottomNav: BottomNavigationView

ListActivity.java

 private NavigationRailView navRail;  private BottomNavigationView bottomNav;
  1. No método onCreate() da ListActivity, use findViewById para conectar essas variáveis às visualizações correspondentes no layout.
  2. Adicione um OnItemSelectedListener à barra de navegação de baixo e à coluna de navegação para processar eventos de seleção de itens:

ListActivity.kt / onCreate()

navRail  = findViewById(R.id.navigationRailView)
bottomNav = findViewById(R.id.bottomNavigationView)

val menuListener = NavigationBarView.OnItemSelectedListener { item ->
    when (item.itemId) {
        R.id.navigation_home -> {
            true
        }
        R.id.navigation_dashboard -> {
            true
        }
        R.id.navigation_settings -> {
            startActivity(Intent(this, SettingsActivity::class.java))
            true
        }
        else -> false
    }
}

navRail.setOnItemSelectedListener(menuListener)
bottomNav.setOnItemSelectedListener(menuListener)

ListActivity.java / onCreate()

NavigationRailView navRail = findViewById(R.id.navigationRailView);
BottomNavigationView bottomNav = findViewById(R.id.bottomNavigationView);

NavigationBarView.OnItemSelectedListener menuListener = new NavigationBarView.OnItemSelectedListener() {
   @Override
   public boolean onNavigationItemSelected(@NonNull MenuItem item) {
       switch (item.getItemId()) {
           case R.id.navigation_home:
               // Handle navigation_home selection
               return true;
           case R.id.navigation_dashboard:
               // Handle navigation_dashboard selection
               return true;
           case R.id.navigation_settings:
               startActivity(new Intent(ListActivity.this, SettingsActivity.class));
               return true;
           default:
               return false;
       }
   }
};

navRail.setOnItemSelectedListener(menuListener);
bottomNav.setOnItemSelectedListener(menuListener);

O aplicativo lê as SharedPreferences e renderiza o app no modo de tela dividida ou SPLIT_TYPE_EXPAND.

  • Quando a configuração da janela muda, o programa confere se a restrição de janela dividida é atendida (se a largura for maior que 840 dp).
  • O app verifica o valor de SharedPreferences para saber se o usuário ativou a janela dividida para exibição. Caso contrário, ele retorna SplitAttribute com o tipo SPLIT_TYPE_EXPAND.
  • Se a janela dividida estiver ativada, o app vai ler o valor de SharedPreferences para receber a proporção da divisão. Isso só funciona quando a versão de WindowSDKExtensions é anterior a 6, porque a versão 6 já é compatível com a expansão do painel e ignora a configuração da proporção de divisão. Em vez disso, os desenvolvedores podem permitir que os usuários arrastem o divisor na interface.

ListActivity.kt / onCreate()

...

SplitController.getInstance(this).setSplitAttributesCalculator{
       params -> params.defaultSplitAttributes
   if (params.areDefaultConstraintsSatisfied) {
       setWiderScreenNavigation(true)

       if (SharePref(this.applicationContext).getAEFlag()) {
           if (WindowSdkExtensions.getInstance().extensionVersion  < 6) {
               // Read a dynamic split ratio from shared preference.
               val currentSplit = SharePref(this.applicationContext).getSplitRatio()
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return@setSplitAttributesCalculator SplitAttributes.Builder()
                       .setSplitType(SplitAttributes.SplitType.ratio(SharePref(this.applicationContext).getSplitRatio()))
                     .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                       .build()
               }
           }
           return@setSplitAttributesCalculator params.defaultSplitAttributes
       } else {
           SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build()
       }
   } else {
       setWiderScreenNavigation(false)
       SplitAttributes.Builder()
           .setSplitType(SPLIT_TYPE_EXPAND)
           .build()
   }
}

...

ListActivity.java / onCreate()

...
SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
   if (params.areDefaultConstraintsSatisfied()) {
       setWiderScreenNavigation(true);

       SharePref sharedPreference = new SharePref(this.getApplicationContext());
       if (sharedPreference.getAEFlag()) {
           if (WindowSdkExtensions.getInstance().getExtensionVersion()  < 6) {
               // Read a dynamic split ratio from shared preference.
               float currentSplit = sharedPreference.getSplitRatio();
               if (currentSplit != SharePref.DEFAULT_SPLIT_RATIO) {
                   return new SplitAttributes.Builder()
                           .setSplitType(SplitAttributes.SplitType.ratio(sharedPreference.getSplitRatio()))
                           .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
                           .build();
               }
           }
           return params.getDefaultSplitAttributes();
       } else {
           return new SplitAttributes.Builder()
                   .setSplitType(SPLIT_TYPE_EXPAND)
                   .build();
       }
   } else {
       setWiderScreenNavigation(false);
       return new SplitAttributes.Builder()
               .setSplitType(SPLIT_TYPE_EXPAND)
               .build();
   }
});

...

Para acionar a SplitAttributesCalculator após as mudanças de configuração, precisamos invalidar os atributos atuais. Para isso, chamamos invalidateVisibleActivityStacks() no ActivityEmbeddingController; antes do WindowManager 1.4. O método é chamado

invalidateTopVisibleSplitAttributes.

ListActivity.kt / onResume()

override fun onResume() {
   super.onResume()
   ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks()
}

ListActivity.java / onResume()

@Override
public void onResume() {
    super.onResume();
    ActivityEmbeddingController.getInstance(this).invalidateVisibleActivityStacks();
}

Executar

Crie e execute o app de exemplo.

Conheça as configurações:

  • Navegue até a tela de configurações.
  • Ative e desative a opção Ativar janela dividida.
  • Ajuste o controle deslizante da proporção de divisão (se disponível no seu dispositivo).

Observe as mudanças no layout:

  • Em dispositivos com o Android 14 e versões anteriores: o layout precisa mudar entre os modos de painel único e duplo com base na troca, e a proporção de divisão precisa mudar quando você ajustar o controle deslizante.
  • Em dispositivos com o Android 15 e versões mais recentes: a expansão de painéis permite redimensionar os painéis de forma dinâmica, independente da configuração do controle deslizante.

8. Parabéns!

Muito bem! Você melhorou seu app com novos recursos usando a incorporação de atividades e o WindowManager. Agora, os usuários vão ter uma experiência mais flexível, intuitiva e envolvente em telas grandes, independente da versão do Android.

9. Saiba mais