Mostrare o nascondere una visualizzazione utilizzando l'animazione

Prova Compose
Jetpack Compose è il toolkit per la UI consigliato per Android. Scopri come utilizzare le animazioni in Compose.

Mentre l'app è in uso, sullo schermo vengono visualizzate nuove informazioni e quelle precedenti vengono rimosse. Modificare immediatamente ciò che viene visualizzato sullo schermo può essere fastidioso e gli utenti potrebbero perdere i nuovi contenuti che appaiono improvvisamente. Le animazioni rallentano le modifiche e attirano l'attenzione dell'utente con il movimento, in modo che gli aggiornamenti siano più evidenti.

Esistono tre animazioni comuni che puoi utilizzare per mostrare o nascondere una visualizzazione: animazioni di rivelazione, dissolvenza incrociata e capovolgimento della scheda.

Creare un'animazione di dissolvenza incrociata

Un'animazione di dissolvenza incrociata, nota anche come dissolvenza, sfuma gradualmente un View o un ViewGroup mentre un altro appare gradualmente. Questa animazione è utile per le situazioni in cui vuoi cambiare contenuti o visualizzazioni nella tua app. L'animazione di dissolvenza incrociata mostrata qui utilizza ViewPropertyAnimator, disponibile per Android 3.1 (livello API 12) e versioni successive.

Ecco un esempio di dissolvenza incrociata da un indicatore di avanzamento a contenuti di testo:

Figura 1. Animazione con dissolvenza incrociata.

Creare le visualizzazioni

Crea le due visualizzazioni di cui vuoi eseguire la dissolvenza incrociata. L'esempio seguente crea un indicatore di avanzamento e una visualizzazione di testo scorrevole:

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

    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView style="?android:textAppearanceMedium"
            android:lineSpacingMultiplier="1.2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/lorem_ipsum"
            android:padding="16dp" />

    </ScrollView>

    <ProgressBar android:id="@+id/loading_spinner"
        style="?android:progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</FrameLayout>

Configurare l'animazione dissolvenza incrociata

Per configurare l'animazione di dissolvenza incrociata:

  1. Crea variabili membro per le visualizzazioni per cui vuoi eseguire la dissolvenza incrociata. Ti serviranno in un secondo momento per modificare le viste durante l'animazione.
  2. Imposta la visibilità della visualizzazione che viene visualizzata in dissolvenza su GONE. In questo modo, la visualizzazione non utilizza lo spazio del layout e viene omessa dai calcoli del layout, il che velocizza l'elaborazione.
  3. Memorizza nella cache la proprietà di sistema config_shortAnimTime in una variabile membro. Questa proprietà definisce una durata "breve" standard per l'animazione. Questa durata è ideale per animazioni sottili o che si verificano frequentemente. Sono disponibili anche config_longAnimTime e config_mediumAnimTime.

Ecco un esempio che utilizza il layout dello snippet di codice precedente come visualizzazione dei contenuti dell'attività:

Kotlin

class CrossfadeActivity : Activity() {

    private lateinit var contentView: View
    private lateinit var loadingView: View
    private var shortAnimationDuration: Int = 0
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_crossfade)

        contentView = findViewById(R.id.content)
        loadingView = findViewById(R.id.loading_spinner)

        // Initially hide the content view.
        contentView.visibility = View.GONE

        // Retrieve and cache the system's default "short" animation time.
        shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
    }
    ...
}

Java

public class CrossfadeActivity extends Activity {

    private View contentView;
    private View loadingView;
    private int shortAnimationDuration;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_crossfade);

        contentView = findViewById(R.id.content);
        loadingView = findViewById(R.id.loading_spinner);

        // Initially hide the content view.
        contentView.setVisibility(View.GONE);

        // Retrieve and cache the system's default "short" animation time.
        shortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }
    ...
}

Dissolvenza incrociata tra le visualizzazioni

Quando le visualizzazioni sono configurate correttamente, esegui la dissolvenza incrociata nel seguente modo:

  1. Per la visualizzazione che sta apparendo gradualmente, imposta il valore alfa su 0 e la visibilità su VISIBLE dall'impostazione iniziale di GONE. In questo modo la visualizzazione è visibile ma trasparente.
  2. Per la visualizzazione che appare in dissolvenza, anima il suo valore alfa da 0 a 1. Per la visualizzazione che sta scomparendo, anima il valore alfa da 1 a 0.
  3. Utilizzando onAnimationEnd() in un Animator.AnimatorListener, imposta la visibilità della visualizzazione che sta scomparendo su GONE. Anche se il valore alfa è 0, l'impostazione della visibilità della visualizzazione su GONE impedisce alla visualizzazione di utilizzare lo spazio del layout e la esclude dai calcoli del layout, il che velocizza l'elaborazione.

Il seguente metodo mostra un esempio di come farlo:

Kotlin

class CrossfadeActivity : Activity() {

    private lateinit var contentView: View
    private lateinit var loadingView: View
    private var shortAnimationDuration: Int = 0
    ...
    private fun crossfade() {
        contentView.apply {
            // Set the content view to 0% opacity but visible, so that it is
            // visible but fully transparent during the animation.
            alpha = 0f
            visibility = View.VISIBLE

            // Animate the content view to 100% opacity and clear any animation
            // listener set on the view.
            animate()
                    .alpha(1f)
                    .setDuration(shortAnimationDuration.toLong())
                    .setListener(null)
        }
        // Animate the loading view to 0% opacity. After the animation ends,
        // set its visibility to GONE as an optimization step so it doesn't
        // participate in layout passes.
        loadingView.animate()
                .alpha(0f)
                .setDuration(shortAnimationDuration.toLong())
                .setListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) {
                        loadingView.visibility = View.GONE
                    }
                })
    }
}

Java

public class CrossfadeActivity extends Activity {

    private View contentView;
    private View loadingView;
    private int shortAnimationDuration;
    ...
    private void crossfade() {

        // Set the content view to 0% opacity but visible, so that it is
        // visible but fully transparent during the animation.
        contentView.setAlpha(0f);
        contentView.setVisibility(View.VISIBLE);

        // Animate the content view to 100% opacity and clear any animation
        // listener set on the view.
        contentView.animate()
                .alpha(1f)
                .setDuration(shortAnimationDuration)
                .setListener(null);

        // Animate the loading view to 0% opacity. After the animation ends,
        // set its visibility to GONE as an optimization step so it doesn't
        // participate in layout passes.
        loadingView.animate()
                .alpha(0f)
                .setDuration(shortAnimationDuration)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        loadingView.setVisibility(View.GONE);
                    }
                });
    }
}

Creare un'animazione di capovolgimento della carta

Le schede cambiano la visualizzazione dei contenuti mostrando un'animazione che simula il ribaltamento di una scheda. L'animazione di rotazione della scheda mostrata qui utilizza FragmentTransaction.

Ecco come appare il ribaltamento di una carta:

Figura 2. Animazione di capovolgimento della scheda.

Crea gli oggetti dell'animatore

Per creare l'animazione del cambio di carta, hai bisogno di quattro animatori. Due animatori sono per quando la parte anteriore della scheda si anima verso l'esterno e verso sinistra e quando si anima verso l'interno e da sinistra. Gli altri due animatori servono per quando il retro della carta entra in animazione da destra e quando esce in animazione verso destra.

card_flip_left_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="-180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_left_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Halfway through the rotation, set the alpha to 1. See startOffset. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Halfway through the rotation, set the alpha to 0. See startOffset. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

Creare le visualizzazioni

Ogni lato della scheda è un layout separato che può contenere qualsiasi contenuto tu voglia, ad esempio due visualizzazioni di testo, due immagini o qualsiasi combinazione di visualizzazioni tra cui alternare. Utilizza i due layout nei frammenti che animerai in seguito. Il seguente layout crea un lato di una scheda, che mostra il testo:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#a6c"
    android:padding="16dp"
    android:gravity="bottom">

    <TextView android:id="@android:id/text1"
        style="?android:textAppearanceLarge"
        android:textStyle="bold"
        android:textColor="#fff"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/card_back_title" />

    <TextView style="?android:textAppearanceSmall"
        android:textAllCaps="true"
        android:textColor="#80ffffff"
        android:textStyle="bold"
        android:lineSpacingMultiplier="1.2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/card_back_description" />

</LinearLayout>

Il layout successivo crea l'altro lato della carta, che mostra un ImageView:

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image1"
    android:scaleType="centerCrop"
    android:contentDescription="@string/description_image_1" />

Crea i frammenti

Crea classi di frammenti per il fronte e il retro della carta. Nelle classi dei fragment, restituisci i layout che hai creato dal metodo onCreateView(). Puoi quindi creare istanze di questo frammento nell'attività principale in cui vuoi mostrare la scheda.

L'esempio seguente mostra le classi di frammenti nidificate all'interno dell'attività principale che le utilizza:

Kotlin

class CardFlipActivity : FragmentActivity() {
    ...
    /**

                    *   A fragment representing the front of the card.
     */
    class CardFrontFragment : Fragment() {

    override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
    ): View = inflater.inflate(R.layout.fragment_card_front, container, false)
    }

    /**
    *   A fragment representing the back of the card.
    */
    class CardBackFragment : Fragment() {

    override fun onCreateView(
                inflater: LayoutInflater,
                container: ViewGroup?,
                savedInstanceState: Bundle?
    ): View = inflater.inflate(R.layout.fragment_card_back, container, false)
    }
}

Java

public class CardFlipActivity extends FragmentActivity {
    ...
    /**
    *   A fragment representing the front of the card.
    */
    public class CardFrontFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_card_front, container, false);
    }
    }

    /**
    *   A fragment representing the back of the card.
    */
    public class CardBackFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_card_back, container, false);
    }
    }
}

Animare il capovolgimento della scheda

Visualizza i fragment all'interno di un'attività principale. Per farlo, crea il layout per la tua attività. L'esempio seguente crea un FrameLayout a cui puoi aggiungere frammenti in fase di runtime:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Nel codice dell'attività, imposta la visualizzazione dei contenuti in modo che corrisponda al layout che crei. È buona norma mostrare un fragment predefinito quando viene creata l'attività. L'attività di esempio seguente mostra come visualizzare la parte anteriore della carta per impostazione predefinita:

Kotlin

class CardFlipActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_activity_card_flip)
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                    .add(R.id.container, CardFrontFragment())
                    .commit()
        }
    }
    ...
}

Java

public class CardFlipActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_activity_card_flip);

        if (savedInstanceState == null) {
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.container, new CardFrontFragment())
                    .commit();
        }
    }
    ...
}

Con la parte anteriore della carta in mostra, puoi mostrare il retro della carta con l'animazione di rotazione al momento opportuno. Crea un metodo per mostrare l'altro lato della carta che esegue le seguenti operazioni:

  • Imposta le animazioni personalizzate che hai creato per le transizioni dei fragment.
  • Sostituisce il frammento visualizzato con un nuovo frammento e anima questo evento con le animazioni personalizzate che hai creato.
  • Aggiunge il frammento visualizzato in precedenza al back stack dei frammenti, in modo che quando l'utente tocca il pulsante Indietro, la scheda si capovolga.

Kotlin

class CardFlipActivity : FragmentActivity() {
    ...
    private fun flipCard() {
        if (showingBack) {
            supportFragmentManager.popBackStack()
            return
        }

        // Flip to the back.

        showingBack = true

        // Create and commit a new fragment transaction that adds the fragment
        // for the back of the card, uses custom animations, and is part of the
        // fragment manager's back stack.

        supportFragmentManager.beginTransaction()

                // Replace the default fragment animations with animator
                // resources representing rotations when switching to the back
                // of the card, as well as animator resources representing
                // rotations when flipping back to the front, such as when the
                // system Back button is tapped.
                .setCustomAnimations(
                        R.animator.card_flip_right_in,
                        R.animator.card_flip_right_out,
                        R.animator.card_flip_left_in,
                        R.animator.card_flip_left_out
                )

                // Replace any fragments in the container view with a fragment
                // representing the next page, indicated by the just-incremented
                // currentPage variable.
                .replace(R.id.container, CardBackFragment())

                // Add this transaction to the back stack, letting users press
                // the Back button to get to the front of the card.
                .addToBackStack(null)

                // Commit the transaction.
                .commit()
    }
}

Java

public class CardFlipActivity extends FragmentActivity {
    ...
    private void flipCard() {
        if (showingBack) {
            getSupportFragmentManager().popBackStack();
            return;
        }

        // Flip to the back.

        showingBack = true;

        // Create and commit a new fragment transaction that adds the fragment
        // for the back of the card, uses custom animations, and is part of the
        // fragment manager's back stack.

        getSupportFragmentManager()
                .beginTransaction()

                // Replace the default fragment animations with animator
                // resources representing rotations when switching to the back
                // of the card, as well as animator resources representing
                // rotations when flipping back to the front, such as when the
                // system Back button is pressed.
                .setCustomAnimations(
                        R.animator.card_flip_right_in,
                        R.animator.card_flip_right_out,
                        R.animator.card_flip_left_in,
                        R.animator.card_flip_left_out)

                // Replace any fragments in the container view with a fragment
                // representing the next page, indicated by the just-incremented
                // currentPage variable.
                .replace(R.id.container, new CardBackFragment())

                // Add this transaction to the back stack, letting users press
                // Back to get to the front of the card.
                .addToBackStack(null)

                // Commit the transaction.
                .commit();
    }
}

Creare un'animazione di rivelazione circolare

Le animazioni di visualizzazione offrono agli utenti una continuità visiva quando mostri o nascondi un gruppo di elementi dell'interfaccia utente. Il metodo ViewAnimationUtils.createCircularReveal() consente di animare un cerchio di ritaglio per mostrare o nascondere una visualizzazione. Questa animazione è fornita nella classe ViewAnimationUtils, che è disponibile per Android 5.0 (livello API 21) e versioni successive.

Ecco un esempio che mostra come visualizzare una visualizzazione precedentemente invisibile:

Kotlin

// A previously invisible view.
val myView: View = findViewById(R.id.my_view)

// Check whether the runtime version is at least Android 5.0.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Get the center for the clipping circle.
    val cx = myView.width / 2
    val cy = myView.height / 2

    // Get the final radius for the clipping circle.
    val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()

    // Create the animator for this view. The start radius is 0.
    val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius)
    // Make the view visible and start the animation.
    myView.visibility = View.VISIBLE
    anim.start()
} else {
    // Set the view to invisible without a circular reveal animation below
    // Android 5.0.
    myView.visibility = View.INVISIBLE
}

Java

// A previously invisible view.
View myView = findViewById(R.id.my_view);

// Check whether the runtime version is at least Android 5.0.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Get the center for the clipping circle.
    int cx = myView.getWidth() / 2;
    int cy = myView.getHeight() / 2;

    // Get the final radius for the clipping circle.
    float finalRadius = (float) Math.hypot(cx, cy);

    // Create the animator for this view. The start radius is 0.
    Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0f, finalRadius);

    // Make the view visible and start the animation.
    myView.setVisibility(View.VISIBLE);
    anim.start();
} else {
    // Set the view to invisible without a circular reveal animation below
    // Android 5.0.
    myView.setVisibility(View.INVISIBLE);
}

L'animazione ViewAnimationUtils.createCircularReveal() accetta cinque parametri. Il primo parametro è la visualizzazione che vuoi nascondere o mostrare sullo schermo. I due parametri successivi sono le coordinate X e Y per il centro del cerchio di ritaglio. In genere, si tratta del centro della visualizzazione, ma puoi anche utilizzare il punto in cui l'utente tocca in modo che l'animazione inizi nel punto selezionato. Il quarto parametro è il raggio iniziale del cerchio di ritaglio.

Nell'esempio precedente, il raggio iniziale è impostato su zero in modo che la visualizzazione mostrata sia nascosta dal cerchio. L'ultimo parametro è il raggio finale del cerchio. Quando visualizzi una vista, rendi il raggio finale più grande della vista in modo che possa essere rivelata completamente prima che l'animazione termini.

Per nascondere una visualizzazione precedentemente visibile:

Kotlin

// A previously visible view.
val myView: View = findViewById(R.id.my_view)

// Check whether the runtime version is at least Android 5.0.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Get the center for the clipping circle.
    val cx = myView.width / 2
    val cy = myView.height / 2

    // Get the initial radius for the clipping circle.
    val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()

    // Create the animation. The final radius is 0.
    val anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f)

    // Make the view invisible when the animation is done.
    anim.addListener(object : AnimatorListenerAdapter() {

        override fun onAnimationEnd(animation: Animator) {
            super.onAnimationEnd(animation)
            myView.visibility = View.INVISIBLE
        }
    })

    // Start the animation.
    anim.start()
} else {
    // Set the view to visible without a circular reveal animation below
    // Android 5.0.
    myView.visibility = View.VISIBLE
}

Java

// A previously visible view.
final View myView = findViewById(R.id.my_view);

// Check whether the runtime version is at least Android 5.0.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Get the center for the clipping circle.
    int cx = myView.getWidth() / 2;
    int cy = myView.getHeight() / 2;

    // Get the initial radius for the clipping circle.
    float initialRadius = (float) Math.hypot(cx, cy);

    // Create the animation. The final radius is 0.
    Animator anim = ViewAnimationUtils.createCircularReveal(myView, cx, cy, initialRadius, 0f);

    // Make the view invisible when the animation is done.
    anim.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            myView.setVisibility(View.INVISIBLE);
        }
    });

    // Start the animation.
    anim.start();
} else {
    // Set the view to visible without a circular reveal animation below Android
    // 5.0.
    myView.setVisibility(View.VISIBLE);
}

In questo caso, il raggio iniziale del cerchio di ritaglio è impostato in modo che sia grande quanto la visualizzazione, in modo che la visualizzazione sia visibile prima dell'inizio dell'animazione. Il raggio finale è impostato su zero in modo che la visualizzazione sia nascosta al termine dell'animazione. Aggiungi un listener all'animazione in modo che la visibilità della visualizzazione possa essere impostata su INVISIBLE al termine dell'animazione.

Risorse aggiuntive