Animar mudanças de layout com uma transição

O framework de transição do Android permite que você anime todos os tipos de movimento na IU simplesmente fornecendo o layout inicial e o final. Você pode selecionar o tipo de animação desejado (por exemplo, fazer o esmaecimento/exibição gradual das visualizações ou alterar o tamanho delas) e o framework de transição descobre como animar do layout inicial ao final.

O framework de transição inclui os seguintes recursos:

  • Animações em nível de grupo: aplique um ou mais efeitos de animação a todas as visualizações de uma hierarquia.
  • Animações integradas: use animações predefinidas para efeitos comuns, como esmaecimento ou movimento.
  • Compatibilidade com arquivo de recursos: carregue hierarquias de visualizações e animações integradas a partir de arquivos de recursos de layout.
  • Callbacks do ciclo de vida: receba callbacks que oferecem controle sobre o processo de mudança de animação e hierarquia.

Para ver um código de amostra que executa animação entre mudanças de layout, consulte BasicTransition (link em inglês).

O processo básico para animar entre dois layouts é o seguinte:

  1. Crie um objeto Scene para o layout inicial e o final. A cena do layout inicial costuma ser determinada automaticamente a partir do layout atual.
  2. Crie um objeto Transition para definir o tipo de animação que você quer usar.
  3. Chame TransitionManager.go() para o sistema executar a animação de troca de layouts.

O diagrama na Figura 1 ilustra a relação entre seus layouts, as cenas, a transição e a animação final.

Figura 1. Ilustração básica de como o framework de transição cria uma animação.

Criar uma cena

As cenas armazenam o estado de uma hierarquia de visualização, incluindo todas as visualizações e os valores de propriedade delas. O framework de transição pode executar animações entre uma cena inicial e uma final.

Você pode criar suas cenas a partir de um arquivo de recurso de layout ou de um grupo de visualizações no seu código. No entanto, a cena inicial para a transição costuma ser determinada automaticamente a partir da IU atual.

Uma cena também pode definir as próprias ações que são executadas quando você faz uma mudança de cena. Esse recurso é útil, por exemplo, para apagar as configurações de visualização após a transição para uma cena.

Observação: o framework pode animar mudanças em uma única hierarquia de visualização sem usar cenas, como descrito em Aplicar uma transição sem cenas. No entanto, entender as cenas é essencial para trabalhar com transições.

Criar uma cena a partir de um recurso de layout

É possível criar uma instância de Scene diretamente de um arquivo de recurso de layout. Use essa técnica quando a hierarquia de visualização no arquivo for predominantemente estática. A cena resultante representa o estado da hierarquia de visualização no momento em que a instância de Scene foi criada. Se você mudar a hierarquia de visualização, precisará recriar a cena. O framework cria a cena a partir de toda a hierarquia de visualização no arquivo. Não é possível criar uma cena usando parte de um arquivo de layout.

Para criar uma instância de Scene com um arquivo de recurso de layout, recupere a raiz da cena a partir do seu layout como uma instância de ViewGroup e chame a função Scene.getSceneForLayout() com a raiz da cena e o ID do recurso do arquivo de layout que contém a hierarquia de visualização da cena.

Definir layouts para cenas

Os snippets de código no restante desta seção mostram como criar duas cenas diferentes com o mesmo elemento raiz da cena. Os snippets também demonstram que é possível carregar vários objetos Scene não relacionados sem implicar uma relação entre eles.

O exemplo consiste nas seguintes definições de layout:

  • O layout principal de uma atividade com um rótulo de texto e um layout filho.
  • Um layout relativo para a primeira cena com dois campos de texto.
  • Um layout relativo para a segunda cena com os mesmos dois campos de texto em ordem diferente.

O exemplo foi projetado para que toda a animação ocorra no layout filho do layout principal da atividade. O rótulo de texto do layout principal permanece estático.

O layout principal da atividade é definido da seguinte forma:

res/layout/activity_main.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/master_layout">
        <TextView
            android:id="@+id/title"
            ...
            android:text="Title"/>
        <FrameLayout
            android:id="@+id/scene_root">
            <include layout="@layout/a_scene" />
        </FrameLayout>
    </LinearLayout>
    

Essa definição de layout contém um campo de texto e um layout filho para a raiz da cena. O layout da primeira cena é incluído no arquivo de layout principal. Isso permite que o aplicativo o exiba como parte da interface do usuário inicial e o carregue em uma cena, considerando que o framework pode carregar apenas um arquivo de layout inteiro em uma cena.

O layout da primeira cena é definido da seguinte maneira:

res/layout/a_scene.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view1"
            android:text="Text Line 1" />
        <TextView
            android:id="@+id/text_view2"
            android:text="Text Line 2" />
    </RelativeLayout>
    

O layout da segunda cena contém os mesmos dois campos de texto (com os mesmos IDs) colocados em uma ordem diferente e é definido da seguinte maneira:

res/layout/another_scene.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/scene_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <TextView
            android:id="@+id/text_view2"
            android:text="Text Line 2" />
        <TextView
            android:id="@+id/text_view1"
            android:text="Text Line 1" />
    </RelativeLayout>
    

Gerar cenas a partir de layouts

Depois de criar definições para os dois layouts relativos, você pode conseguir uma cena para cada um deles. Isso permite que você faça a transição posterior entre as duas configurações da IU. Para conseguir uma cena, você precisa de uma referência à raiz dela e ao código do recurso de layout.

O snippet de código a seguir mostra como acessar uma referência à raiz da cena e criar dois objetos Scene a partir dos arquivos de layout:

Kotlin

    val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
    val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this)
    val anotherScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this)

    

Java

    Scene aScene;
    Scene anotherScene;

    // Create the scene root for the scenes in this app
    sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

    // Create the scenes
    aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
    anotherScene =
        Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

    

No app, agora há dois objetos Scene baseados nas hierarquias de visualização. Ambas as cenas usam a raiz da cena definida pelo elemento FrameLayout em res/layout/activity_main.xml.

Criar uma cena no código

Também é possível criar uma instância de Scene no código a partir de um objeto ViewGroup. Use essa técnica quando modificar as hierarquias de visualização diretamente no código ou quando gerá-las dinamicamente.

Para criar uma cena a partir de uma hierarquia de visualização no código, use o construtor Scene(sceneRoot, viewHierarchy). Chamar esse construtor é equivalente a chamar a função Scene.getSceneForLayout() quando você já inflou um arquivo de layout.

O snippet de código a seguir demonstra como criar uma instância de Scene a partir do elemento raiz da cena e da hierarquia de visualização da cena no código:

Kotlin

    val sceneRoot = someLayoutElement as ViewGroup
    val viewHierarchy = someOtherLayoutElement as ViewGroup
    val scene: Scene = Scene(sceneRoot, mViewHierarchy)

    

Java

    Scene mScene;

    // Obtain the scene root element
    sceneRoot = (ViewGroup) someLayoutElement;

    // Obtain the view hierarchy to add as a child of
    // the scene root when this scene is entered
    viewHierarchy = (ViewGroup) someOtherLayoutElement;

    // Create a scene
    mScene = new Scene(sceneRoot, mViewHierarchy);

    

Criar ações de cena

O framework permite que você defina ações de cena personalizadas para o sistema executar ao entrar ou sair de uma cena. Em muitos casos, a definição de ações de cena personalizadas não é necessária, porque o framework anima a mudança entre as cenas automaticamente.

As ações de cena são úteis para lidar com estes casos:

  • Animar visualizações que não estão na mesma hierarquia. Você pode animar as visualizações das cenas inicial e final usando ações de cena de saída e entrada.
  • Animar visualizações que o framework de transições não pode animar automaticamente, como objetos ListView. Para ver mais informações, consulte Limitações.

Para oferecer ações de cena personalizadas, defina suas ações como objetos Runnable e as transmita para as funções Scene.setExitAction() ou Scene.setEnterAction(). O framework chama a função setExitAction() na cena inicial antes de executar a animação de transição, e a função setEnterAction() na cena final depois de executar a animação de transição.

Observação: não use ações de cena para transmitir dados entre as visualizações nas cenas inicial e final. Para ver mais informações, consulte Definir callbacks do ciclo de vida da transição.

Aplicar uma transição

O framework de transição representa o estilo de animação entre as cenas com um objeto Transition. Você pode instanciar uma Transition usando várias subclasses integradas, como AutoTransition e Fade, ou definir sua própria transição. Em seguida, execute a animação entre as cenas transmitindo a Scene final e a Transition para TransitionManager.go().

O ciclo de vida da transição é semelhante ao ciclo de vida da atividade e representa os estados de transição que o framework monitora entre o início e o fim de uma animação. Em estados importantes do ciclo de vida, o framework invoca funções de retorno de chamada que você pode implementar para fazer ajustes na interface do usuário em diferentes fases da transição.

Criar uma transição

Na seção anterior, você aprendeu a criar cenas que representam o estado de diferentes hierarquias de visualização. Depois de definir a cena inicial e a final entre as quais você quer alternar, crie um objeto Transition que defina uma animação. O framework permite que você especifique uma transição integrada em um arquivo de recurso e a infle no seu código ou crie uma instância de uma transição integrada diretamente no seu código.

Tabela 1. Tipos de transição integrada.

Classe Tag Atributos Efeito
AutoTransition <autoTransition/> - Transição padrão. Esmaecer, mover, redimensionar e exibir gradualmente as visualizações, nessa ordem.
Fade <fade/> android:fadingMode="[fade_in |
fade_out |
fade_in_out]"
fade_in faz as visualizações aparecerem gradualmente
fade_out faz as visualizações esmaecerem
fade_in_out (padrão) faz um fade_out seguido por um fade_in.
ChangeBounds <changeBounds/> - Move e redimensiona as visualizações.

Criar uma instância de transição a partir de um arquivo de recurso

Essa técnica permite que você modifique sua definição de transição sem precisar mudar o código da sua atividade. Ela também é útil para separar definições de transição complexas do código do seu aplicativo, conforme mostrado em Especificar várias transições.

Para especificar uma transição integrada em um arquivo de recurso, siga estas etapas:

  1. Adicione o diretório res/transition/ ao projeto.
  2. Crie um novo arquivo de recurso XML dentro desse diretório.
  3. Adicione um nó XML para uma das transições integradas.

Por exemplo, o arquivo de recurso a seguir especifica a transição Fade:

res/transition/fade_transition.xml

    <fade xmlns:android="http://schemas.android.com/apk/res/android" />
    

O snippet de código a seguir mostra como inflar uma instância de Transition dentro da atividade a partir de um arquivo de recurso:

Kotlin

    var fadeTransition: Transition =
        TransitionInflater.from(this)
                          .inflateTransition(R.transition.fade_transition)

    

Java

    Transition fadeTransition =
            TransitionInflater.from(this).
            inflateTransition(R.transition.fade_transition);

    

Criar uma instância de transição no seu código

Essa técnica é útil para criar objetos de transição dinamicamente se você modificar a interface do usuário no código, bem como para criar instâncias de transição integrada simples com poucos ou nenhum parâmetro.

Para criar uma instância de uma transição integrada, invoque um dos construtores públicos nas subclasses da classe Transition. Por exemplo, o snippet de código a seguir cria uma instância da transição Fade:

Kotlin

    var fadeTransition: Transition = Fade()

    

Java

    Transition fadeTransition = new Fade();

    

Aplicar uma transição

Normalmente, você aplica uma transição para alternar entre diferentes hierarquias de visualização em resposta a um evento, como uma ação do usuário. Por exemplo, considere um app de pesquisa: quando o usuário insere um termo de pesquisa e clica no botão "Pesquisar", o app muda para a cena que representa o layout dos resultados, aplicando uma transição que esmaece o botão de pesquisa e faz os resultados serem exibidos.

Para fazer uma mudança de cena ao aplicar uma transição em resposta a algum evento na atividade, chame a função de classe TransitionManager.go() com a cena final e a instância de transição a ser usada para a animação, como mostrado no snippet a seguir:

Kotlin

    TransitionManager.go(endingScene, fadeTransition)

    

Java

    TransitionManager.go(endingScene, fadeTransition);

    

O framework troca a hierarquia de visualização dentro da raiz da cena pela hierarquia de visualização da cena final ao executar a animação especificada pela instância de transição. A cena inicial é a cena final da última transição. Se não houver uma transição anterior, a cena inicial será determinada automaticamente a partir do estado atual da interface do usuário.

Se você não especificar uma instância de transição, o gerenciador de transição poderá aplicar uma transição automática que faça algo aceitável para a maioria das situações. Para ver mais informações, consulte a referência da API para a classe TransitionManager.

Escolher visualizações de destino específicas

O framework aplica transições a todas as visualizações nas cenas inicial e final por padrão. Em alguns casos, talvez você só queira aplicar uma animação a um subconjunto de visualizações em uma cena. Por exemplo, o framework não é compatível com a animação de mudanças em objetos ListView, então não tente animá-los durante a transição. O framework permite que você selecione visualizações específicas que quer animar.

Cada visualização que a transição anima é chamada de destino. Você só pode selecionar destinos que façam parte da hierarquia de visualização associada a uma cena.

Para remover uma ou mais visualizações da lista de destinos, chame o método removeTarget() antes de iniciar a transição. Para adicionar apenas as visualizações especificadas à lista de destinos, chame a função addTarget(). Para ver mais informações, consulte a referência da API para a classe Transition.

Especificar várias transições

Para conseguir o maior impacto em uma animação, você precisa associá-la ao tipo de alteração que ocorre entre as cenas. Por exemplo, se você estiver removendo algumas visualizações e adicionando outras entre as cenas, uma animação de esmaecimento/exibição gradual proporciona uma indicação perceptível de que algumas visualizações não estão mais disponíveis. Se você estiver movendo visualizações para pontos diferentes na tela, uma opção melhor será animar o movimento para que os usuários notem o novo local das visualizações.

Você não precisa escolher apenas uma animação, porque o framework de transições permite combinar efeitos de animação em um conjunto de transições integradas ou personalizadas.

Para definir um conjunto de transições a partir de uma coleção de transições em XML, crie um arquivo de recursos no diretório res/transitions/ e liste as transições no elemento transitionSet. Por exemplo, o snippet a seguir mostra como especificar um conjunto de transições que tem o mesmo comportamento que a classe AutoTransition:

    <transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
        android:transitionOrdering="sequential">
        <fade android:fadingMode="fade_out" />
        <changeBounds />
        <fade android:fadingMode="fade_in" />
    </transitionSet>
    

Para inflar o conjunto de transições em um objeto TransitionSet no código, chame a função TransitionInflater.from() na atividade. A classe TransitionSet se estende da classe Transition e pode ser usada com um gerenciador de transição da mesma forma que qualquer outra instância de Transition.

Aplicar uma transição sem cenas

Alterar as hierarquias de visualização não é a única maneira de modificar sua interface do usuário. Você também pode fazer alterações adicionando, modificando e removendo visualizações filhas na hierarquia atual. Por exemplo, você pode implementar uma interação de pesquisa com apenas um layout. Comece pelo layout que mostra um campo de entrada de pesquisa e um ícone de pesquisa. Para mudar a interface do usuário de forma que ela mostre os resultados, remova o botão de pesquisa quando o usuário clicar nele chamando a função ViewGroup.removeView() e adicione os resultados da pesquisa chamando a função ViewGroup.addView().

Essa abordagem é recomendável se a alternativa for ter duas hierarquias quase idênticas. Em vez de criar e manter dois arquivos de layout separados para uma pequena diferença na interface do usuário, você pode criar um arquivo de layout contendo uma hierarquia de visualização que possa ser modificada no código.

Se você fizer alterações na hierarquia de visualização dessa maneira, não precisará criar uma cena. Em vez disso, você poderá criar e aplicar uma transição entre dois estados de uma hierarquia de visualização usando uma transição atrasada. Esse recurso do framework de transição começa pelo estado atual da hierarquia de visualização, registra as alterações feitas nas visualizações e aplica uma transição que anima as alterações quando o sistema redesenha a interface do usuário.

Para criar uma transição atrasada em uma única hierarquia de visualização, siga estas etapas:

  1. Quando o evento que aciona a transição ocorrer, chame a função TransitionManager.beginDelayedTransition() fornecendo a visualização mãe de todas as visualizações que você quer mudar e a transição a ser usada. O framework armazena o estado atual das visualizações filhas e os respectivos valores de propriedade.
  2. Faça alterações nas visualizações filhas conforme necessário para seu caso de uso. O framework registra as alterações feitas nas visualizações filhas e nas propriedades delas.
  3. Quando o sistema redesenhar a interface do usuário de acordo com suas alterações, o framework animará as alterações entre o estado original e o novo.

O exemplo a seguir mostra como animar o acréscimo de uma visualização de texto a uma hierarquia de visualização usando uma transição atrasada. O primeiro snippet mostra o arquivo de definição de layout:

res/layout/activity_main.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/mainLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <EditText
            android:id="@+id/inputText"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        ...
    </RelativeLayout>
    

O próximo snippet mostra o código que anima o acréscimo da visualização de texto:

MainActivity

Kotlin

    setContentView(R.layout.activity_main)
    val labelText = TextView(this).apply {
        text = "Label"
        id = R.id.text
    }
    val rootView: ViewGroup = findViewById(R.id.mainLayout)
    val fade: Fade = Fade(Fade.IN)
    TransitionManager.beginDelayedTransition(rootView, mFade)
    rootView.addView(labelText)

    

Java

    private TextView labelText;
    private Fade mFade;
    private ViewGroup rootView;
    ...

    // Load the layout
    setContentView(R.layout.activity_main);
    ...

    // Create a new TextView and set some View properties
    labelText = new TextView(this);
    labelText.setText("Label");
    labelText.setId(R.id.text);

    // Get the root view and create a transition
    rootView = (ViewGroup) findViewById(R.id.mainLayout);
    mFade = new Fade(Fade.IN);

    // Start recording changes to the view hierarchy
    TransitionManager.beginDelayedTransition(rootView, mFade);

    // Add the new TextView to the view hierarchy
    rootView.addView(labelText);

    // When the system redraws the screen to show this update,
    // the framework will animate the addition as a fade in

    

Definir callbacks do ciclo de vida da transição

O ciclo de vida da transição é semelhante ao da atividade. Ele representa os estados de transição que o framework monitora durante o tempo entre uma chamada para a função TransitionManager.go() e o término da animação. Em estados importantes do ciclo de vida, o framework invoca callbacks definidos pela interface TransitionListener.

Os callbacks do ciclo de vida da transição são úteis, por exemplo, para copiar um valor de propriedade de visualização da hierarquia de visualização inicial para a final durante uma mudança de cena. Você não pode simplesmente copiar o valor da visualização inicial para a visualização na hierarquia final, porque essa hierarquia não é inflada até que a transição seja concluída. Em vez disso, você precisa armazenar o valor em uma variável e depois copiá-lo para a hierarquia de visualização final quando o framework tiver terminado a transição. Para receber uma notificação quando a transição for concluída, implemente a função TransitionListener.onTransitionEnd() na atividade.

Para mais informações, consulte a referência da API para a classe TransitionListener.

Limitações

Esta seção lista algumas limitações conhecidas do framework de transições:

  • As animações aplicadas a uma SurfaceView podem não ser exibidas corretamente. As instâncias de SurfaceView são atualizadas em uma linha de execução que não é da IU, de modo que as atualizações podem ficar fora de sincronia com as animações de outras visualizações.
  • Alguns tipos específicos de transição podem não produzir o efeito de animação esperado quando aplicados a uma TextureView.
  • Classes que estendem AdapterView, como ListView, gerenciam as visualizações filhas de maneiras incompatíveis com o framework de transições. Se você tentar animar uma visualização com base em AdapterView, a tela do dispositivo poderá travar.
  • Se você tentar redimensionar uma TextView com uma animação, o texto aparecerá em um novo local antes de o objeto ser completamente redimensionado. Para evitar esse problema, não anime o redimensionamento de visualizações que contenham texto.