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

Gdy aplikacja jest używana, na ekranie pojawiają się nowe informacje, a stare są usuwane. Natychmiastowe wprowadzanie zmian w tym, co widać na ekranie, może być irytujące, ponieważ użytkownicy mogą szybko przeoczyć nowe treści, które pojawią się nagle. Animacje spowalniają wprowadzanie zmian i przyciągają uwagę użytkownika ruchem, aby zmiany były bardziej widoczne.

Istnieją 3 typowe animacje, których można używać do pokazywania i ukrywania widoku: wyświetlanie animacji, przenikanie i odwracanie kart.

Utwórz animację przenikania

Animacja przenikania – nazywana też przenikaniem – stopniowo zanika na jednym elemencie View lub ViewGroup i jednocześnie znika. Ta animacja jest przydatna w sytuacjach, gdy chcesz przełączać treści lub widoki w aplikacji. Widoczna tu animacja przenikania korzysta z parametru ViewPropertyAnimator, który jest dostępny na Androidzie 3.1 (poziom API 12) i nowszych.

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

Rysunek 1. Przenikanie.

Tworzenie widoków

Utwórz dwa widoki, które mają się przechodzić. Poniższy przykład pokazuje 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 przenikania

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

  1. Utwórz zmienne członków dla widoków, które mają się pojawiać w różnych miejscach. Odniesienia będą Ci potrzebne później podczas modyfikowania widoków w trakcie animacji.
  2. Ustaw widoczność zanikania widoku na GONE. Zapobiega to wykorzystaniu przestrzeni układu w widoku i nie uwzględnia jej w obliczeniach układu, co przyspiesza przetwarzanie.
  3. Przechowuj właściwość systemową config_shortAnimTime w pamięci podręcznej w zmiennej członkowskiej. Ta właściwość określa standardowy „krótki” czas trwania animacji. Ten czas trwania jest idealny dla subtelnych animacji lub pojawiających się często. Dostępne są też config_longAnimTime i config_mediumAnimTime.

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 między widokami

Po prawidłowym skonfigurowaniu widoków możesz je przenikać, wykonując te czynności:

  1. Dla widoku, który się stopniowo zanika, ustaw wartość alfa na 0, a widoczność na VISIBLE z początkowego ustawienia GONE. Widok jest wtedy widoczny, ale przezroczysty.
  2. Dla widoku, który się stopniowo zanika, animuj jego wartość alfa od 0 do 1. W przypadku zanikającego widoku zastosuj animację wartości alfa od 1 do 0.
  3. Używając właściwości onAnimationEnd() w Animator.AnimatorListener, ustaw widoczność zanikającego widoku na GONE. Mimo że wartość alfa wynosi 0, ustawienie widoczności widoku na GONE uniemożliwia widokowi wykorzystywanie przestrzeni układu i wyklucza ją z obliczeń układu, co przyspiesza przetwarzanie.

Oto przykład, 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 odwrócenia karty

Odwracanie kart pozwala przełączać się między widokami treści, pokazując animację symulującą odwracanie karty. Widoczna tutaj animacja odwracania karty korzysta z parametru FragmentTransaction.

Oto jak wygląda odwrócenie karty:

Rysunek 2. Animacja odwracania karty.

Tworzenie obiektów animatora

Aby utworzyć animację odwrócenia karty, potrzebujesz 4 animatorów. 2 animacje służą do tego, gdy przód karty pojawia się w lewo i w górę, a także gdy pojawia się ona do wewnątrz i z lewej strony. Pozostałe dwa animacje są używane, gdy tył karty pojawia się w górę i z prawej strony oraz gdy ożywa się 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 oddzielny układ, który może zawierać dowolne treści, np. 2 widoki tekstu, 2 obrazy lub dowolną kombinację widoków do przełączania. We fragmentach, które będziesz później animować, używaj dwóch układów. 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>

Następny układ tworzy drugą stronę karty i wyświetla 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" />

Utwórz fragmenty

Utwórz klasy fragmentów dla przedniej i tylnej części karty. W klasach fragmentów zwraca układy utworzone za pomocą metody onCreateView(). Możesz utworzyć wystąpienia tego fragmentu w aktywności nadrzędnej, w której chcesz wyświetlać 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 odwrócenia karty

Wyświetla fragmenty wewnątrz aktywności nadrzędnej. Aby to zrobić, utwórz układ dla aktywności. Poniższy przykład tworzy FrameLayout, do którego można dodawać 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 Ciebie układ. Warto pokazywać fragment domyślny podczas tworzenia aktywności. Ta przykładowa aktywność pokazuje, 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();
        }
    }
    ...
}

Po odsłonięciu przedniej strony karty możesz w odpowiednim momencie pokazać jej tylną część z animacją odwrócenia. Utwórz metodę pokazywania drugiej strony karty, która umożliwia:

  • Ustawia niestandardowe animacje utworzone dla przejść fragmentów.
  • Zastępuje wyświetlany fragment nowym fragmentem i animuje to zdarzenie przy użyciu utworzonych przez Ciebie niestandardowych animacji.
  • Dodaje wyświetlany wcześniej fragment do wstecznego stosu fragmentów, dzięki czemu gdy użytkownik naciśnie przycisk Wstecz, karta zostanie odwrócona.

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

Utwórz animację okrągłego odkrycia

Odkryte animacje zapewniają użytkownikom wizualną ciągłość, gdy pokazujesz lub ukrywasz grupę elementów interfejsu. Metoda ViewAnimationUtils.createCircularReveal() pozwala animować okrąg przycinający, aby pokazać lub ukryć widok. Ta 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, który pokazuje, jak wyświetlić widok, który wcześniej był niewidoczny:

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. Kolejne dwa parametry to współrzędne X i Y środka okręgu przycinającego. Zwykle jest to środek widoku, ale można też wykorzystać punkt klikany przez użytkownika, aby animacja rozpoczynała się w wybranym przez niego miejscu. Czwarty parametr to początkowy promień okręgu przycinającego.

W poprzednim przykładzie początkowy promień jest ustawiony na 0, tak aby wyświetlany widok był ukryty przez okrąg. Ostatni parametr to ostateczny promień okręgu. Podczas wyświetlania widoku ustaw końcowy promień większy od widoku, aby można było go w pełni odsłonić przed zakończeniem animacji.

Aby ukryć wcześniej widoczny widok, wykonaj następujące 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 przycinającego jest ustawiany na tyle, by był tak duży jak widok, aby widok był widoczny przed rozpoczęciem animacji. Ostateczny promień jest ustawiany na zero, dzięki czemu widok zostaje ukryty po zakończeniu animacji. Dodaj odbiornik do animacji, aby po zakończeniu animacji widoczność widoku była ustawiana na INVISIBLE.

Dodatkowe materiały