Pokazywanie i ukrywanie widoku za pomocą animacji

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak korzystać z animacji w Compose

Kiedy aplikacja jest używana, na ekranie pojawiają się nowe informacje, a stare są usuwane. Natychmiastowa zmiana tego, co widać na ekranie, może przykuć uwagę, a użytkownicy mogą przeoczyć nowe treści, które pojawiają się nagle. Animacje spowalniają wprowadzanie zmian i przyciągają uwagę użytkownika ruchem, dzięki czemu zmiany są bardziej widoczne.

Istnieją 3 typowe animacje, których można użyć do pokazania lub ukrycia widoku: ujawnianie, przenikanie animacji i animacje cardflip.

Utwórz animację przenikania

Animacja przenikająca (tzw. dissolve) stopniowo zanika (jeden element View lub ViewGroup), a jednocześnie zanika. Ta animacja przydaje się, gdy chcesz zmieniać treści lub widoki w aplikacji. Pokazana tutaj animacja przenikania korzysta z kodu ViewPropertyAnimator, który jest dostępny na Androidzie 3.1 (poziom interfejsu API 12) i nowszym.

Oto przykład przejścia ze wskaźnika postępu na treść tekstową:

Rys. 1. Przenikanie animacji.

Tworzenie widoków

Utwórz dwa widoki, które będą zanikać. W poniższym przykładzie tworzymy wskaźnik postępu i widok tekstowy, który można przewijać:

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

Aby skonfigurować animację przenikania, wykonaj te czynności:

  1. Utwórz zmienne składowe dla widoków, które chcesz przenikać. Te odwołania będą potrzebne później podczas modyfikowania widoków w trakcie animacji.
  2. Ustaw widoczność zaciemnianego widoku na GONE. Zapobiega to wykorzystywaniu przestrzeni układu przez widok i pomija go w obliczeniach układu, co przyspiesza przetwarzanie
  3. Zapisuj właściwość systemową config_shortAnimTime w pamięci podręcznej w zmiennej uczestnika. 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 często się pojawiają. 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 działania:

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

Przechodzenie między widokami

Gdy widoki są skonfigurowane prawidłowo, możesz je przełączać w ten sposób:

  1. W przypadku zanikającego widoku ustaw wartość alfa na 0, a widoczność na VISIBLE z poziomu początkowego GONE. Dzięki temu widok jest widoczny, ale przezroczysty.
  2. Dla widoku, który zanika, animuj jego wartość alfa od 0 do 1. W przypadku zanikającego widoku animuj wartość alfa od 1 do 0.
  3. Za pomocą parametru onAnimationEnd() w elemencie Animator.AnimatorListener ustaw widoczność widoku, który zanika na GONE. Mimo że wartość alfa wynosi 0, ustawienie widoczności widoku na GONE uniemożliwia widokowi wykorzystanie przestrzeni układu i pomija go w obliczeniach układu, co przyspiesza przetwarzanie.

Oto przykład:

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

Utwórz animację odwrócenia karty

Odwracanie karty przełącza widok między wyświetleniami treści, pokazując animację, która symuluje odwrócenie karty. Animacja odwrócenia karty pokazuje kod FragmentTransaction.

Tak wygląda odwrócenie karty:

Rys. 2. Animacja odwrócenia karty.

Tworzenie obiektów animacji

Aby utworzyć animację odwrócenia karty, potrzebujesz 4 animatorów. Dwóch animatorów używa się, gdy przód karty przesuwa się na zewnątrz i w lewo oraz gdy karta animuje się w lewo i w lewo. Pozostałe dwa animacje pojawiają się, gdy tylna część karty animuje się w prawo i w prawo, a potem na zewnątrz i w prawo.

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 do przełączania się między nimi. Korzystaj z dwóch układów we fragmentach, które będziesz później animować. Ten układ tworzy jedną stronę karty z tekstem:

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

Kolejny układ tworzy drugą stronę karty, w tym 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ć wystąpienia tego fragmentu w aktywności nadrzędnej, w której chcesz wyświetlać kartę.

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

Animuj odwrócenie karty

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

<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 przez siebie układ. Warto pokazywać domyślny fragment podczas tworzenia aktywności. Z przykładu poniżej dowiesz się, jak domyślnie wyświetlać przód 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 przód karty jest widoczny, możesz w odpowiednim momencie pokazać tylną część karty z animacją odwrócenia. Utwórz metodę wyświetlania drugiej strony karty, która:

  • Ustawia niestandardowe animacje utworzone dla przejść fragmentów.
  • Zastępuje wyświetlany fragment nowym fragmentem i animuje to zdarzenie utworzonymi przez Ciebie niestandardowymi animacjami.
  • Dodaje wyświetlony wcześniej fragment do wstecznego stosu fragmentów, dzięki czemu, gdy użytkownik kliknie przycisk Wstecz, karta odwróci się z powrotem.

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 wyświetlania kołowego

Animacje demonstrujące zapewniają użytkownikom ciągłość wizualną, gdy wyświetlasz lub ukrywasz grupę elementów interfejsu. Metoda ViewAnimationUtils.createCircularReveal() umożliwia animowanie okręgu przycinania w celu odkrycia lub ukrycia widoku. Animacja jest udostępniana w klasie ViewAnimationUtils, która jest dostępna na Androidzie 5.0 (poziom interfejsu API 21) i nowszych.

Oto przykład, jak pokazać, jak wyświetlić 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() ma 5 parametrów. Pierwszy parametr to widok, który chcesz ukryć lub pokazać na ekranie. Następne 2 parametry to współrzędne X i Y środka okręgu przycinającego. Zazwyczaj jest to środek widoku, ale możesz też użyć punktu klikniętego przez użytkownika, aby animacja zaczynała się w wybranym miejscu. Czwarty parametr to początkowy promień okręgu przycinającego.

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 od widoku, tak aby widok był widoczny przed zakończeniem animacji.

Aby ukryć widok widoczny wcześniej, wykonaj te czynności:

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 taki sam jak widok, dzięki czemu widok jest widoczny przed rozpoczęciem animacji. Końcowy promień jest ustawiony na zero, by po zakończeniu animacji widok był ukryty. Dodaj do animacji detektor, aby po zakończeniu animacji widoczność widoku była ustawiona na INVISIBLE.

Dodatkowe materiały