Usar animações para navegar entre fragmentos

A API Fragment oferece duas maneiras de usar efeitos de movimento e transformações para conectar fragmentos visualmente durante a navegação. Uma delas é o framework de animação, que usa Animation e Animator. A outra é o framework de transição, que inclui transições de elementos compartilhados.

Você pode especificar efeitos personalizados para inserir e remover fragmentos e para transições de elementos compartilhados entre fragmentos.

  • Um efeito de entrada determina como um fragmento entra na tela. Por exemplo, você pode criar um efeito para fazer com que o fragmento apareça deslizando da borda da tela ao navegar até ele.
  • Um efeito de saída determina como um fragmento sai da tela. Por exemplo, você pode criar um efeito para esmaecer o fragmento ao navegar para sair dele.
  • Uma transição de elemento compartilhado determina a forma como uma visualização compartilhada entre dois fragmentos se move entre eles. Por exemplo, uma imagem exibida em uma ImageView no fragmento A passa para o fragmento B quando este se torna visível.

Definir animações

Primeiro, é necessário criar animações para os efeitos de entrada e saída, que são executados ao navegar até um novo fragmento. Você pode definir animações como recursos de animação de interpolação. Esses recursos permitem que você defina como os fragmentos vão girar, expandir, esmaecer e se mover durante a animação. Por exemplo, talvez você queira que o fragmento atual esmaeça e o novo fragmento apareça deslizando do canto direito da tela, conforme mostrado na figura 1.

Animações de entrada e saída. O fragmento atual esmaece à medida que o
            fragmento seguinte aparece deslizando à direita.
Figura 1. Animações de entrada e saída. O fragmento atual esmaece à medida que o fragmento seguinte aparece deslizando à direita.

Essas animações podem ser definidas no diretório res/anim:

<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />
<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

Você também pode especificar animações para os efeitos de entrada e saída executados ao exibir a pilha de retorno, o que pode acontecer quando o usuário toca no botão "Para cima" ou "Voltar". Elas são chamadas de animações popEnter e popExit. Por exemplo, quando um usuário volta para uma tela anterior, é interessante que o fragmento atual deslize para fora da tela à direita e que o fragmento anterior apareça.

Animações popEnter e popExit. O fragmento atual desliza para fora
            da tela à direita à medida que o fragmento anterior aparece.
Figura 2. Animações popEnter e popExit. O fragmento atual desliza para fora da tela à direita, à medida que o fragmento anterior aparece.

Essas animações podem ser definidas da seguinte maneira:

<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />
<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

Depois de definir suas animações, use-as chamando FragmentTransaction.setCustomAnimations(), transmitindo os recursos da animação pelo ID de recurso, conforme mostrado no exemplo a seguir:

Kotlin

supportFragmentManager.commit {
    setCustomAnimations(
        R.anim.slide_in, // enter
        R.anim.fade_out, // exit
        R.anim.fade_in, // popEnter
        R.anim.slide_out // popExit
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Definir transições

Você também pode usar transições para definir efeitos de entrada e saída. Essas transições podem ser definidas em arquivos de recurso XML. Por exemplo, você pode querer que o fragmento atual esmaeça e o novo fragmento apareça deslizando da borda direita da tela. Essas transições podem ser definidas da seguinte forma:

<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>
<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

Depois de definir as transições, aplique-as chamando setEnterTransition() no fragmento de entrada e setExitTransition() no fragmento de saída, passando os recursos de transição inflada pelo ID de recurso, conforme mostrado no exemplo a seguir:

Kotlin

class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

Os fragmentos são compatíveis com transições do AndroidX. Embora os fragmentos também sejam compatíveis com transições de framework, é altamente recomendável usar transições do AndroidX, que são compatíveis com APIs de nível 14 e mais recentes e contêm correções de bugs que não estão presentes em versões anteriores de transições de framework.

Usar transições de elementos compartilhados

As transições de elementos compartilhados fazem parte do framework de transição e determinam como as visualizações correspondentes se movem entre dois fragmentos durante uma transição. Por exemplo, talvez você queira que uma imagem seja exibida em uma ImageView no fragmento A, para fazer a transição para o fragmento B quando este se tornar visível, conforme mostrado na figura 3.

Transição de fragmento com um elemento compartilhado.
Figura 3. Transição de fragmento com um elemento compartilhado.

De modo geral, faça uma transição de fragmentos com elementos compartilhados desta forma:

  1. Atribua um nome de transição exclusivo para cada visualização de elemento compartilhado.
  2. Adicione visualizações de elementos compartilhados e nomes de transição à FragmentTransaction.
  3. Defina uma animação de transição de elemento compartilhado.

Primeiro, é necessário atribuir um nome de transição exclusivo para cada visualização de elemento compartilhado a fim de permitir que as visualizações sejam mapeadas de um fragmento para o próximo. Defina um nome de transição em elementos compartilhados em cada layout de fragmento usando ViewCompat.setTransitionName(), que oferece compatibilidade com as APIs de nível 14 e mais recentes. Como exemplo, o nome de transição de uma ImageView nos fragmentos A e B pode ser atribuído da seguinte maneira:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, item_image)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, hero_image)
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, item_image);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, hero_image);
    }
}

Para incluir seus elementos compartilhados na transição de fragmento, sua FragmentTransaction precisa saber como as visualizações de cada elemento compartilhado são mapeadas de um fragmento para o próximo. Adicione cada um dos elementos compartilhados à FragmentTransaction chamando FragmentTransaction.addSharedElement() e transmitindo a visualização e o nome da transição da visualização correspondente no próximo fragmento, conforme mostrado no exemplo a seguir:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, hero_image)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, hero_image)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Para especificar como seus elementos compartilhados fazem a transição de um fragmento para o outro, é necessário definir uma transição de entrada no fragmento de destino. Chame Fragment.setSharedElementEnterTransition() no método onCreate() do fragmento, conforme mostrado no exemplo a seguir:

Kotlin

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

A transição shared_image é definida da seguinte maneira:

<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

Todas as subclasses de Transition são compatíveis como transições de elementos compartilhados. Caso você queira criar uma Transition personalizada, consulte Criar uma animação de transição personalizada. changeImageTransform, usada no exemplo anterior, é uma das traduções pré-criadas disponíveis para uso. Você pode encontrar outras subclasses Transition na referência da API para a classe Transition.

Por padrão, a transição de entrada do elemento compartilhado também é usada como a transição de retorno dos elementos compartilhados. A transição de retorno determina como os elementos compartilhados voltam para o fragmento anterior quando a transação do fragmento é retirada da pilha de retorno. Caso queira especificar uma transição de retorno diferente, faça isso usando Fragment.setSharedElementReturnTransition() no método onCreate() do fragmento.

Compatibilidade com volta preditiva

Você pode usar a volta preditiva com muitas animações entre fragmentos, mas não todas. Ao implementar a volta preditiva, tenha em mente as seguintes considerações:

  • Importe Transitions 1.5.0 ou uma versão mais recente e Fragments 1.7.0 ou uma mais recente.
  • A classe Animator, as subclasses e a biblioteca AndroidX Transition estão suporte.
  • Não há suporte para a classe Animation e a biblioteca Transition do framework.
  • Animações de fragmentos preditivos só funcionam em dispositivos com o Android 14 ou mais alto.
  • setCustomAnimations, setEnterTransition e setExitTransition. setReenterTransition, setReturnTransition. setSharedElementEnterTransition e setSharedElementReturnTransition são é compatível com a volta preditiva.

Para saber mais, consulte Suporte a animações de volta preditiva

Adiamento de transições

Em alguns casos, pode ser necessário adiar a transição de fragmento por um curto período. Por exemplo, pode ser necessário aguardar até que todas as visualizações no fragmento de entrada sejam medidas e definidas para que o Android possa capturar com precisão os estados inicial e final da transição.

Além disso, a transição pode precisar ser adiada até que alguns dados necessários sejam carregados. Por exemplo, talvez seja necessário aguardar até que as imagens tenham sido carregadas para os elementos compartilhados. Caso contrário, a transição poderá ficar instável se uma imagem terminar de ser carregada durante ou após a transição.

Para adiar uma transição, é necessário garantir que a transação do fragmento permita reordenar as mudanças de estado do fragmento. Para permitir reordenar as mudanças de estado do fragmento, chame FragmentTransaction.setReorderingAllowed(), conforme mostrado no exemplo a seguir:

Kotlin

val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

Java

Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .setCustomAnimations(...)
    .addSharedElement(view, view.getTransitionName())
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

Para adiar a transição de entrada, chame Fragment.postponeEnterTransition() no método onViewCreated() do fragmento de entrada:

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        postponeEnterTransition()
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        postponeEnterTransition();
    }
}

Depois que você carregar os dados e estiver tudo pronto para iniciar a transição, chame Fragment.startPostponedEnterTransition(). O exemplo a seguir usa a biblioteca Glide (link em inglês) para carregar uma imagem em uma ImageView compartilhada, adiando a transição correspondente até que o carregamento da imagem seja concluído.

Kotlin

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        Glide.with(this)
            .load(url)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}

Java

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        Glide.with(this)
            .load(url)
            .listener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(...) {
                    startPostponedEnterTransition();
                    return false;
                }

                @Override
                public boolean onResourceReady(...) {
                    startPostponedEnterTransition();
                    return false;
                }
            })
            .into(headerImage)
    }
}

Ao lidar com casos como conexão de Internet lenta do usuário, pode ser necessário que a transição adiada seja iniciada após determinado período, em vez de esperar que todos os dados sejam carregados. Para essas situações, você pode chamar Fragment.postponeEnterTransition(long, TimeUnit) no método onViewCreated() do fragmento de entrada, transmitindo a duração e a unidade de tempo. A transição adiada será iniciada automaticamente após o tempo especificado.

Usar transições de elementos compartilhados com uma RecyclerView

As transições de entrada adiadas não serão iniciadas até que todas as visualizações no fragmento de entrada sejam medidas e definidas. Ao usar uma RecyclerView, é necessário esperar que todos os dados sejam carregados e que os itens RecyclerView estejam prontos para serem desenhados antes de iniciar a transição. Veja um exemplo:

Kotlin

class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // Wait for the data to load
        viewModel.data.observe(viewLifecycleOwner) {
            // Set the data on the RecyclerView adapter
            adapter.setData(it)
            // Start the transition once all views have been
            // measured and laid out
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

Java

public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // Wait for the data to load
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // Set the data on the RecyclerView adapter
                    adapter.setData(it);
                    // Start the transition once all views have been
                    // measured and laid out
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            @Override
                            public boolean onPreDraw(){
                                parentView.getViewTreeObserver()
                                        .removeOnPreDrawListener(this);
                                startPostponedEnterTransition();
                                return true;
                            }
                    });
                }
        });
    }
}

Um ViewTreeObserver.OnPreDrawListener é definido no pai da visualização de fragmento. Isso serve para garantir que todas as visualizações do fragmento tenham sido medidas e definidas e estejam prontas para serem desenhadas antes de iniciar a transição de entrada adiada.

Outro ponto a ser considerado ao usar transições de elementos compartilhados com uma RecyclerView é que não é possível definir o nome de transição no layout XML do item RecyclerView, porque um número arbitrário de itens compartilha esse layout. Um nome de transição exclusivo precisa ser atribuído para que a animação de transição use a visualização correta.

Você pode dar um nome de transição exclusivo ao elemento compartilhado de cada item, atribuindo o nome quando ViewHolder estiver vinculado. Por exemplo, se os dados de cada item incluem um ID exclusivo, esse ID pode ser usado como o nome da transição, conforme mostrado no exemplo a seguir:

Kotlin

class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

Java

public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

Outros recursos

Para saber mais sobre transições de fragmentos, consulte os recursos a seguir.

Amostras

Postagens do blog