使用动画显示或隐藏视图

在应用的使用过程中,需要在屏幕上显示新信息,同时移除旧信息。立即切换显示的内容看起来有些突兀,或者导致用户很容易错过屏幕上的新内容。利用动画可以减慢更改的速度,并通过概念吸引用户的注意,以使更新更加明显。

在显示或隐藏视图时,有三种常见的动画可供使用。您可以使用圆形揭露动画、淡入淡出动画或卡片翻转动画。

创建淡入淡出动画

淡入淡出动画(也称为“叠化”)逐渐淡出一个 ViewViewGroup,同时淡入另一个。此动画适用于您希望在应用中切换内容或视图的情况。此处显示的淡入淡出动画使用 ViewPropertyAnimator,适用于 Android 3.1(API 级别 12)及更高版本。

以下是从进度指示器切换到某些文字内容的淡入淡出示例。

淡入淡出动画
 

创建视图

首先,您需要创建两个要淡入淡出的视图。以下示例创建了一个进度指示器和一个可滚动的文本视图:

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

设置淡入淡出动画

如需设置淡入淡出动画,请执行以下操作:

  1. 为想要淡入淡出的视图创建成员变量。之后在显示动画期间修改视图时,您需要引用这些变量。
  2. 对于淡入的视图,请将其可见性设置为 GONE。 这样可以避免视图占用布局空间,并在进行布局计算时省去对视图的计算,从而加快处理速度。
  3. config_shortAnimTime 系统属性缓存到成员变量中。此属性用于定义动画的标准“短”持续时间。该持续时间非常适用于巧妙的动画效果或经常显示的动画。如果愿意,您也可以使用 config_longAnimTimeconfig_mediumAnimTime

下面的示例使用前一个代码段中的布局作为 Activity 内容视图:

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

    

淡入淡出视图

现在视图已正确设置,可通过执行以下操作来实现淡入淡出:

  1. 对于淡入的视图,请将 Alpha 值设置为 0,将可见性设置为 VISIBLE。(请记住,初始设置为 GONE。)这会使视图可见但完全透明。
  2. 对于淡入的视图,通过动画将其 Alpha 值从 0 过渡到 1。对于淡出的视图,通过动画将其 Alpha 值从 1 过渡到 0
  3. Animator.AnimatorListener 中使用 onAnimationEnd(),将淡出视图的可见性设置为 GONE。即使 Alpha 值为 0,将视图的可见性设置为 GONE 也会阻止视图占用布局空间,并在进行布局计算时省去对视图的计算,从而加快处理速度。

以下方法通过示例说明了如何执行此操作:

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 (it won't
            // participate in layout passes, etc.)
            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 (it won't
            // participate in layout passes, etc.)
            loadingView.animate()
                    .alpha(0f)
                    .setDuration(shortAnimationDuration)
                    .setListener(new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(Animator animation) {
                            loadingView.setVisibility(View.GONE);
                        }
                    });
        }
    }

    

创建卡片翻转动画

卡片翻转通过显示模拟卡片翻转的动画,在内容视图之间添加动画。此处显示的卡片翻转动画使用 FragmentTransaction,它在 Android 3.0(API 级别 11)及更高版本中可用。

卡片翻转的效果如下:

卡片翻转动画
 

创建 Animator 对象

要创建卡片翻转动画,总共需要四个 Animator。 两个 Animator 用于卡片正面向左侧淡出以及从左侧淡入的动画。您还需要两个 Animator,用于卡片背面从右侧淡入以及向右侧淡出的动画。

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

        <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
        <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" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
        <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" />

        <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
        <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" />

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

创建视图

“卡片”的每一面都是一个独立的布局,可以包含您所需的任何内容,例如用于翻转切换的两个文本视图、两张图片或任意视图组合。然后,您将在 Fragment 中使用两个布局供您稍后添加动画。以下布局会创建显示文本的卡片一面:

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

卡片 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" />
    

创建 Fragment

为卡片的正面和背面创建 Fragment 类。这些类将返回您之前在每个 Fragment 的 onCreateView() 方法中创建的布局。然后,您可以在要显示该卡片的父 Activity 中创建此 Fragment 的实例。以下示例展示了父 Activity 内使用的嵌套 Fragment 类:

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

    

为卡片翻转添加动画

现在,您需要显示父 Activity 内的 Fragment。为此,请先为您的 Activity 创建布局。以下示例创建了一个 FrameLayout,您可以在运行时向其中添加 Fragment:

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

在 Activity 代码中,将内容视图设置为您刚刚创建的布局。也可以在创建 Activity 时显示默认 Fragment,以下示例 Activity 向您展示了如何默认显示卡片的正面:

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

    

现在显示的是卡片正面,您可以在适当的时间通过翻转动画显示卡片背面。创建一种方法来显示卡片的另一面,该方法需要完成以下操作:

  • 设置您之前为 Fragment 转换创建的自定义动画。
  • 使用新 Fragment 替换当前显示的 Fragment,并使用您创建的自定义动画为该事件添加动画。
  • 将之前显示的 Fragment 添加到 Fragment 返回堆栈中,以便当用户按下“返回”按钮时,卡片会翻转回来。

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 (e.g. 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 currently 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, allowing users to press
                    // Back 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 (e.g. 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 currently 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, allowing users to press
                    // Back to get to the front of the card.
                    .addToBackStack(null)

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

    

创建圆形揭露动画

当您显示或隐藏一组界面元素时,揭露动画可为用户提供视觉连续性。ViewAnimationUtils.createCircularReveal() 方法让您能够为裁剪圆形添加动画以揭露或隐藏视图。此动画在 ViewAnimationUtils 类中提供,适用于 Android 5.0(API 级别 21)及更高版本。

以下示例展示了如何揭露之前不可见的视图:

Kotlin

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

    // Check if the runtime version is at least Lollipop
    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 zero)
        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 Lollipop
        myView.visibility = View.INVISIBLE
    }

    

Java

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

    // Check if the runtime version is at least Lollipop
    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 zero)
        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 Lollipop
        myView.setVisibility(View.INVISIBLE);
    }

    

ViewAnimationUtils.createCircularReveal() 动画采用五个参数。第一个参数是要在屏幕上隐藏或显示的视图。接下来的两个参数是裁剪圆形圆心的 x 和 y 坐标。通常,这将成为视图的中心,但您也可以使用用户轻触的点作为中心,以便动画从用户选择的位置开始。第四个参数是剪裁圆形的起始半径。

在上述示例中,初始半径设置为 0,因此圆形会隐藏要显示的视图。最后一个参数是圆形的最终半径。在显示视图时,请确保最终半径大于视图本身,以便在动画播放完毕之前完全显示视图。

隐藏之前可见的视图:

Kotlin

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

    // Check if the runtime version is at least Lollipop
    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 zero)
        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 Lollipop
        myView.visibility = View.VISIBLE
    }

    

Java

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

    // Check if the runtime version is at least Lollipop
    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 zero)
        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 Lollipop
        myView.setVisibility(View.VISIBLE);
    }

    

在本例中,裁剪圆形的初始半径设置为与视图一样大,因此视图在动画开始播放前可见。最终半径设置为 0,因此视图在动画播放完毕后将被隐藏。一定要在动画中添加监听器,这样可在动画播放完毕时将视图的可见性设置为 INVISIBLE