Spostarsi tra i frammenti utilizzando le animazioni

L'API Fragment offre due modi per utilizzare gli effetti di movimento e le trasformazioni per connettere visivamente i frammenti durante la navigazione. Uno di questi è il framework dell'animazione, che utilizza sia Animation sia Animator. L'altro è il Framework di transizione, che include le transizioni di elementi condivisi.

Puoi specificare effetti personalizzati per l'ingresso e l'uscita dei frammenti e per le transizioni di elementi condivisi tra i frammenti.

  • Un effetto Invio determina il modo in cui un frammento entra nello schermo. Ad esempio, puoi creare un effetto per far scorrere il frammento dal bordo dello schermo quando lo accedi.
  • Un effetto uscita determina il modo in cui un frammento esce dalla schermata. Ad esempio, puoi creare un effetto per far scomparire il frammento quando lo abbandoni.
  • Una transizione di elementi condivisi determina il modo in cui una vista condivisa tra due frammenti si sposta tra loro. Ad esempio, un'immagine visualizzata in ImageView nelle transizioni del frammento A al frammento B quando B diventa visibile.

Impostare animazioni

Innanzitutto, devi creare animazioni per gli effetti di entrata e uscita, che vengono eseguiti quando si passa a un nuovo frammento. Puoi definire le animazioni come risorse di animazione tra due elementi. Queste risorse consentono di definire la modalità di rotazione, ampliamento, dissolvenza e spostamento dei frammenti durante l'animazione. Ad esempio, potresti voler far scomparire il frammento corrente e far scorrere il nuovo frammento dal bordo destro dello schermo, come mostrato nella figura 1.

Consente di accedere e uscire dalle animazioni. Il frammento corrente sbiadisce in uscita, mentre il frammento successivo scorre verso l'interno da destra.
Figura 1. Consente di accedere e uscire dalle animazioni. Il frammento attuale svanisce, mentre il frammento successivo scorre da destra.

Queste animazioni possono essere definite nella directory 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%" />

Puoi anche specificare animazioni per gli effetti di entrata e uscita eseguiti quando si apre lo stack posteriore, operazione che può essere eseguita quando l'utente tocca il pulsante Su o Indietro. Si tratta delle animazioni popEnter e popExit. Ad esempio, quando un utente torna a una schermata precedente, potresti voler spostare il frammento corrente dal bordo destro dello schermo e la dissolvenza del frammento precedente in entrata.

popInvio e popExit. Il frammento corrente scorre fuori dallo schermo verso destra, mentre il frammento precedente sbiadisce verso l&#39;interno.
Figura 2. Animazioni popEnter e popExit. Il frammento corrente scorre fuori dallo schermo verso destra, mentre il frammento precedente sbiadisce verso l'interno.

Queste animazioni possono essere definite come segue:

<!-- 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" />

Dopo aver definito le animazioni, utilizzale richiamando FragmentTransaction.setCustomAnimations(), trasmettendo le risorse di animazione in base al loro ID risorsa, come mostrato nell'esempio seguente:

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();

Impostare le transizioni

Puoi anche utilizzare le transizioni per definire gli effetti di entrata e uscita. Queste transizioni possono essere definite nei file di risorse XML. Ad esempio, potresti volere che il frammento corrente sbiadisca in uscita e che il nuovo frammento scopra dal bordo destro dello schermo. Queste transizioni possono essere definite come segue:

<!-- 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" />

Dopo aver definito le transizioni, applicale chiamando setEnterTransition() sul frammento di entrata e setExitTransition() sul frammento uscita, passando le risorse di transizione con un aumento artificioso in base al relativo ID risorsa, come mostrato nell'esempio seguente:

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));
    }
}

I frammenti supportano le transizioni ad AndroidX. Sebbene i frammenti supportino anche le transizioni del framework, consigliamo vivamente di utilizzare le transizioni di AndroidX, in quanto sono supportate nei livelli API 14 e successivi e contengono correzioni di bug che non sono presenti nelle versioni precedenti delle transizioni framework.

Utilizzare le transizioni degli elementi condivisi

Parte del framework di transizione, le transizioni degli elementi condivisi determinano in che modo le visualizzazioni corrispondenti si spostano tra due frammenti durante la transizione di un frammento. Ad esempio, potresti volere che un'immagine visualizzata in ImageView sul frammento A passi al frammento B quando diventa visibile la figura 3.

Una transizione di frammento con un elemento condiviso.
Figura 3. Una transizione di frammento con un elemento condiviso.

A livello generale, ecco come eseguire una transizione di frammento con elementi condivisi:

  1. Assegna un nome univoco a ogni vista degli elementi condivisi.
  2. Aggiungi visualizzazioni di elementi condivisi e nomi delle transizioni a FragmentTransaction.
  3. Imposta un'animazione di transizione degli elementi condivisi.

In primo luogo, devi assegnare un nome di transizione univoco a ogni vista degli elementi condivisi per consentire la mappatura delle viste da un frammento all'altro. Imposta un nome di transizione sugli elementi condivisi in ogni layout di frammento utilizzando ViewCompat.setTransitionName(), che garantisce la compatibilità per i livelli API 14 e successivi. Ad esempio, il nome della transizione per un ImageView nei frammenti A e B può essere assegnato come segue:

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”);
    }
}

Per includere gli elementi condivisi nella transizione dei frammenti, FragmentTransaction deve sapere come vengono mappate le visualizzazioni di ogni elemento condiviso da un frammento all'altro. Aggiungi ogni elemento condiviso a FragmentTransaction chiamando FragmentTransaction.addSharedElement(), passando la vista e il nome della transizione della vista corrispondente nel frammento successivo, come mostrato nell'esempio seguente:

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();

Per specificare la modalità di transizione degli elementi condivisi da un frammento all'altro, devi impostare una transizione di tipo Invio per il frammento di destinazione. Richiama Fragment.setSharedElementEnterTransition() nel metodo onCreate() del frammento, come mostrato nell'esempio seguente:

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);
    }
}

La transizione shared_image è definita come segue:

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

Tutte le sottoclassi di Transition sono supportate come transizioni di elementi condivisi. Se vuoi creare un elemento Transition personalizzato, consulta Creare un'animazione di transizione personalizzata. changeImageTransform, utilizzata nell'esempio precedente, è una delle traduzioni predefinite disponibili che puoi usare. Puoi trovare ulteriori sottoclassi Transition nel riferimento API per la classe Transition.

Per impostazione predefinita, la transizione di invio dell'elemento condiviso viene utilizzata anche come transizione di ritorno per gli elementi condivisi. La transizione di ritorno determina in che modo gli elementi condivisi ritornano al frammento precedente quando la transazione del frammento viene scollegata dallo stack posteriore. Se vuoi specificare una transizione di ritorno diversa, puoi farlo utilizzando Fragment.setSharedElementReturnTransition() nel metodo onCreate() del frammento.

Compatibilità posteriore predittiva

Puoi utilizzare il dorso predittivo con molte animazioni tra frammenti, ma non tutte. Quando implementi la risposta predittiva, tieni presente i seguenti aspetti:

  • Importa Transitions 1.5.0 o una versione successiva e Fragments 1.7.0 o una versione successiva.
  • Sono supportate le classi e le sottoclassi Animator e la libreria AndroidX Transizione.
  • La libreria Animation e il framework Transition non sono supportati.
  • Le animazioni predittive con frammenti funzionano solo su dispositivi con Android 14 o versioni successive.
  • setCustomAnimations, setEnterTransition, setExitTransition, setReenterTransition, setReturnTransition, setSharedElementEnterTransition e setSharedElementReturnTransition sono supportati con il valore indietro predittivo.

Per scoprire di più, consulta Aggiungere il supporto per le animazioni predittive Indietro.

Rinvio delle transizioni

In alcuni casi, potrebbe essere necessario posticipare la transizione del frammento per un breve periodo di tempo. Ad esempio, potrebbe essere necessario attendere che tutte le visualizzazioni nel frammento iniziale siano state misurate e strutturate in modo che Android possa acquisire accuratamente gli stati di inizio e di fine della transizione.

Inoltre, potrebbe essere necessario posticipare la transizione fino al caricamento di alcuni dati necessari. Ad esempio, potresti dover attendere il caricamento delle immagini per gli elementi condivisi. In caso contrario, la transizione potrebbe essere scioccante se il caricamento di un'immagine termina durante o dopo la transizione.

Per posticipare una transizione, devi prima assicurarti che la transazione di frammento consenta il riordinamento delle modifiche dello stato del frammento. Per consentire il riordinamento delle modifiche dello stato dei frammenti, chiama FragmentTransaction.setReorderingAllowed(), come mostrato nell'esempio seguente:

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();

Per posticipare la transizione di invio, chiama Fragment.postponeEnterTransition() nel metodo onViewCreated() del frammento di entrata:

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();
    }
}

Una volta caricati i dati e pronto per iniziare la transizione, chiama Fragment.startPostponedEnterTransition(). L'esempio seguente utilizza la libreria Glide per caricare un'immagine in un ImageView condiviso, posticipando la transizione corrispondente fino al completamento del caricamento dell'immagine.

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)
    }
}

In caso di problemi come la connessione a internet lenta di un utente, potrebbe essere necessario che la transizione posticipata inizi dopo un determinato periodo di tempo anziché attendere il caricamento di tutti i dati. In questi casi, puoi chiamare Fragment.postponeEnterTransition(long, TimeUnit) nel metodo onViewCreated() del frammento di input, passando la durata e l'unità di tempo. Il posticipo quindi inizia automaticamente una volta trascorso il tempo specificato.

Utilizza le transizioni di elementi condivisi con un RecyclerView

Le transizioni di immissione posticipate non devono iniziare finché non sono state misurate e definite tutte le visualizzazioni nel frammento di entrata. Quando utilizzi una RecyclerView, devi attendere che i dati vengano caricati e che gli elementi RecyclerView siano pronti per essere disegnati prima di iniziare la transizione. Ecco un esempio:

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;
                            }
                    });
                }
        });
    }
}

Nota che è impostato un elemento ViewTreeObserver.OnPreDrawListener sulla parte padre della visualizzazione frammento. Questo serve a garantire che tutte le visualizzazioni del frammento siano state misurate e disposte e siano quindi pronte per essere tracciate prima di iniziare la transizione di inserimento posticipata.

Un altro aspetto da considerare quando si utilizzano le transizioni di elementi condivisi con RecyclerView è che non è possibile impostare il nome della transizione nel layout XML dell'elemento RecyclerView perché questo layout è condiviso da un numero arbitrario di elementi. Devi assegnare un nome di transizione univoco affinché l'animazione di transizione utilizzi la visualizzazione corretta.

Puoi assegnare un nome univoco all'elemento condiviso per la transizione assegnando loro un nome quando ViewHolder è associato. Ad esempio, se i dati di ogni elemento includono un ID univoco, questo potrebbe essere utilizzato come nome della transizione, come mostrato nell'esempio seguente:

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);
        ...
    }
}

Risorse aggiuntive

Per saperne di più sulle transizioni dei frammenti, consulta le seguenti risorse aggiuntive.

Campioni

Post sui blog