Pokazywanie i ukrywanie widoku za pomocą animacji

Wypróbuj Compose
Jetpack Compose to zalecany zestaw narzędzi do tworzenia interfejsu na Androidzie. Dowiedz się, jak używać animacji w Compose.

Gdy użytkownik korzysta z aplikacji, na ekranie pojawiają się nowe informacje, a stare są usuwane. Natychmiastowa zmiana tego, co jest wyświetlane na ekranie, może być irytująca, a użytkownicy mogą przegapić nowe treści, które pojawiają się nagle. Animacje spowalniają zmiany i przyciągają wzrok użytkownika ruchem, dzięki czemu aktualizacje są bardziej widoczne.

Do wyświetlania lub ukrywania widoku możesz użyć 3 popularnych animacji: animacji ujawniania, przenikania i odwracania karty.

Tworzenie animacji przenikania

Animacja przenikania, zwana też rozpuszczaniem, stopniowo zanika jeden element View lub ViewGroup, jednocześnie stopniowo pojawiając się w innym. Ta animacja jest przydatna w sytuacjach, gdy chcesz przełączać treści lub widoki w aplikacji. Pokazana tutaj animacja przenikania korzysta z ViewPropertyAnimator, który jest dostępny w Androidzie 3.1 (API na poziomie 12) i nowszym.

Oto przykład przenikania od wskaźnika postępu do treści tekstowej:

Rysunek 1. Animacja przenikania.

Tworzenie widoków

Utwórz 2 widoki, które chcesz przenikać. Poniższy przykład tworzy wskaźnik postępu i przewijany widok tekstu:

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

Konfigurowanie animacji przenikania

Aby skonfigurować animację przenikania:

  1. Utwórz zmienne składowe dla widoków, które chcesz przenikać. Te odwołania będą Ci potrzebne później, gdy będziesz modyfikować widoki podczas animacji.
  2. Ustaw widoczność widoku, który ma się pojawić, na GONE. Zapobiega to używaniu przez widok miejsca na układzie i pomija go w obliczeniach układu, co przyspiesza przetwarzanie.
  3. Zapisz w pamięci podręcznej właściwość config_shortAnimTime systemową w zmiennej składowej. Ta właściwość określa standardowy „krótki” czas trwania animacji. Ten czas trwania jest idealny w przypadku subtelnych animacji lub animacji, które występują często. config_longAnimTime i config_mediumAnimTime są również dostępne.

Oto przykład użycia układu z poprzedniego fragmentu kodu jako widoku treści aktywności:

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

Przenikanie widoków

Gdy widoki są prawidłowo skonfigurowane, możesz je przenikać, wykonując te czynności:

  1. W przypadku widoku, który ma się pojawić, ustaw wartość alfa na 0, a widoczność na VISIBLE (z początkowego ustawienia GONE). Dzięki temu widok będzie widoczny, ale przezroczysty.
  2. W przypadku widoku, który ma się pojawić, animuj wartość alfa od 0 do 1. W przypadku widoku, który ma zniknąć, animuj wartość alfa od 1 do 0.
  3. Używając onAnimationEnd() w Animator.AnimatorListener, ustaw widoczność widoku, który ma zniknąć, na GONE. Nawet jeśli wartość alfa wynosi 0, ustawienie widoczności widoku na GONE zapobiega używaniu przez widok miejsca na układzie i pomija go w obliczeniach układu, co przyspiesza przetwarzanie.

Poniższa metoda pokazuje, jak to zrobić:

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

Tworzenie animacji odwracania karty

Odwracanie karty przełącza widoki treści, wyświetlając animację, która naśladuje odwracanie karty. Pokazana tutaj animacja odwracania karty korzysta z FragmentTransaction.

Oto jak wygląda odwracanie karty:

Rysunek 2. Animacja odwracania karty.

Tworzenie obiektów animatora

Aby utworzyć animację odwracania karty, potrzebujesz 4 animatorów. 2 animatory są używane, gdy przednia strona karty animuje się w lewo i znika oraz gdy animuje się w prawo i pojawia. Pozostałe 2 animatory są używane, gdy tylna strona karty animuje się w prawo i pojawia oraz gdy animuje się w lewo i znika.

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>

Tworzenie widoków

Każda strona karty to osobny układ, który może zawierać dowolną treść, np. 2 widoki tekstu, 2 obrazy lub dowolną kombinację widoków, między którymi można się przełączać. Użyj 2 układów we fragmentach, które będziesz później animować. Poniższy układ tworzy jedną stronę karty, na której wyświetla się tekst:

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

A następny układ tworzy drugą stronę karty, na której wyświetla się 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" />

Tworzenie fragmentów

Utwórz klasy fragmentów dla przedniej i tylnej strony karty. W klasach fragmentów zwróć układy utworzone za pomocą metody.onCreateView() Następnie możesz utworzyć instancje tego fragmentu w aktywności nadrzędnej, w której chcesz wyświetlić kartę.

Poniższy przykład pokazuje zagnieżdżone klasy fragmentów w aktywności nadrzędnej, która ich używa:

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

Animowanie odwracania karty

Wyświetl fragmenty w aktywności nadrzędnej. Aby to zrobić, utwórz układ dla aktywności. Poniższy przykład tworzy FrameLayout, do którego możesz dodawać fragmenty w czasie działania programu:

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

W kodzie aktywności ustaw widok treści na utworzony układ. Dobrym rozwiązaniem jest wyświetlanie domyślnego fragmentu podczas tworzenia aktywności. Poniższy przykład aktywności pokazuje, jak domyślnie wyświetlać przednią stronę karty:

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

Gdy wyświetla się przednia strona karty, możesz w odpowiednim momencie wyświetlić tylną stronę za pomocą animacji odwracania. Utwórz metodę wyświetlania drugiej strony karty, która wykonuje te czynności:

  • Ustawia niestandardowe animacje utworzone na potrzeby przejść między fragmentami.
  • Zastępuje wyświetlany fragment nowym fragmentem i animuje to zdarzenie za pomocą utworzonych przez Ciebie animacji niestandardowych.
  • Dodaje wcześniej wyświetlany fragment do stosu wstecznego fragmentów, dzięki czemu gdy użytkownik kliknie przycisk Wstecz, karta się odwróci.

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

Tworzenie animacji ujawniania okrężnego

Animacje ujawniania zapewniają użytkownikom wizualną ciągłość, gdy wyświetlasz lub ukrywasz grupę elementów interfejsu. The ViewAnimationUtils.createCircularReveal() metoda umożliwia animowanie okręgu przycinania w celu ujawnienia lub ukrycia widoku. Ta animacja jest dostępna w ViewAnimationUtils klasie, która jest dostępna w Androidzie 5.0 (API na poziomie 21) i nowszym.

Oto przykład pokazujący, jak ujawnić wcześniej niewidoczny widok:

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

Animacja ViewAnimationUtils.createCircularReveal() przyjmuje 5 parametrów. Pierwszy parametr to widok, który chcesz ukryć lub wyświetlić na ekranie. Następne 2 parametry to współrzędne X i Y środka okręgu przycinania. Zwykle jest to środek widoku, ale możesz też użyć punktu, w którym użytkownik dotknie ekranu, aby animacja zaczęła się w wybranym przez niego miejscu. Czwarty parametr to początkowy promień okręgu przycinania.

W poprzednim przykładzie początkowy promień jest ustawiony na zero, dzięki czemu wyświetlany widok jest ukryty przez okrąg. Ostatni parametr to końcowy promień okręgu. Podczas wyświetlania widoku ustaw końcowy promień większy niż widok, aby widok mógł się w pełni ujawnić przed zakończeniem animacji.

Aby ukryć wcześniej widoczny widok:

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

W tym przypadku początkowy promień okręgu przycinania jest ustawiony na taką samą wartość jak widok, dzięki czemu widok jest widoczny przed rozpoczęciem animacji. Końcowy promień jest ustawiony na zero, dzięki czemu widok jest ukryty po zakończeniu animacji. Dodaj do animacji słuchacza, aby po zakończeniu animacji można było ustawić widoczność widoku na INVISIBLE gdy animacja zakończy się.

Dodatkowe materiały