【翻译】Android 中的动画(Animations )和过渡(Transitions)

`动画概述

动画可以添加视觉效果,通知用户应用中发生了什么。当 UI 的状态发生改变的时候,例如新内容加载或者新功能可用时,它有为有用。动画还可以让应用外观看起来更好看,从而提升应用的质感。

Android 包含不同的动画 API,具体取决于你想要的动画类型,因此本页面概述了你可以向 UI 添加动画的不同方式。

要更好的了解合适应该使用动画,请看 material design guide to motion

位图动画(以前叫帧动画)

如果要为 Bitmap(例如图标或者插图)设置动画,应使用 drawable 动画 API。通常这些动画是用 drawable 资源定义的,但你也可以在运行时自定义动画的行为。

例如,点击动画播放按钮转换为暂停按钮是向用户传达这两个动作相关的一种很好的方式,而按下一个动作使得另一个动作变得可见。

`

更多内容,请看 Animate Drawable Graphics

UI 的可见性动画和动作动画

当你需要更改布局中 View 的可见性或位置时,你应该包含微妙的动画,以帮助用户了解 UI 的更改方式。

要在当前布局中移动、显示或者隐藏 View,你可以使用 Android 3.0(API 11)或更高版本中提供的 android.animation 包提供的属性动画。这些 API 会在一段时间内更新 View 对象的属性,并在属性更改时不断的重绘 View。例如,但给你更改位置属性时,View 会在屏幕上移动;当你更改透明度属性时,View 会淡入淡出。

你可以直接在布局上启用动画,这样当你只是更改 View 的可见性的时候,动画会自动应用。

动画应该基于物理特性

你的动画要遵守真实世界的物理特性,这样它们看起来会自然一些。例如,它们应该在目标变化时保持惯性,并在变化期间进行平滑过渡。

为了提供这些行为,Android Support 库包含基于物理的动画 API,它以来物理定律来控制动画的发生方式。

两种常见的基于物理的动画如下:

不基于物理特性的动画(例如使用 ObjectAnimator API 构建的动画)是相当静态并且有固定持续时间的。如果目标值更改,则需要在目标值更改时取消动画,使用新值作为起始值重新配置动画,并添加新的目标值。在视觉上,这个过程在动画中图片停止,然后是一个杂乱的运动,就像下图:

然而,使用基于物理特性的动画 API(例如 DyncmicAnimation)构建的动画是由动力驱动的。目标值的变化导致动力的变化。新的动力适用于现有的速度,从而不断向新目标过度。该过程会产生更自然的动画,如下图所示:

布局更改动画

在 Android 4.4(API 19)及更高版本中,你可以使用过渡框架在当前 Activity 或者 Fragment 中变换布局时创建动画。你需要做的就是指定开始和结束布局,以及要使用的动画类型。然后系统在两个布局之间找出并执行动画。你可以使用它开过渡整个 UI 或是移动/替换一些 View。

例如,当用户点击项目以查看更多信息时,你可以使用项目详情信息替换布局,如下图所示

起始布局和结束布局均存储在 Scene 中,但其实场景通常是当前布局自动确定的。然后创建一个 Transition 对象来过塑系统你想要什么类型的动画,然后调用 TransitionManager.go()) 通知系统运行动画来替换布局。

更多有关信息,可以查看: Animate Between Layouts Using a Transition,示例代码请查看: BasicTransition

Activity 之间的动画

在 Android 5.0(API 21)及更高版本上,还可以创建 Activity 之间的切换动画。是基于上面的用于布局更改动画的转换框架,但它允许我们在不同的 Activity 的布局之间创建动画。

你可以使用简单的动画,例如从侧面滑动到新的 Activity,或者淡入到新的 Activity,你也可以创建在每个 Activity 中的共享 View 之间转换的动画。例如当用户点击某个项目以查看更多信息时,你可以转换为带有动画的新 Activity,该动画可以无缝的增长该项目以填充屏幕,就像上面那张图片一样。

调用 startActivity() 启动新的 Activity,同时将 ActivityOptions.makeSceneTransitionAnimation()) 提供的一系列选项传递给它,这些选项包括在 Activity 之间可以共享哪些 View,因此转换框架可以在动画期间连接这些 View。

更多细节,请看:Start an Activity with an Animation;示例代码,请看:ActivitySceneTransitionBasic

属性动画概述

属性动画是一个强大的框架,允许你在几乎任何东西上创建动画。你可以定义动画以随着时间更改任意对象的属性,无论它是否在屏幕上绘制。属性动画在指定的时间长度内更改属性(对象中的字段)值。要为某些内容设置动画,请指定要设置动画的对象的属性,例如对象在屏幕上的位置,要为其设置动画的时长以及要在期间设置动画的值。(这一段翻译的很别扭,大意就是你可以为任意对象设置动画,只需要设置动画的执行时长以及动画的范围值)。

属性动画允许你定义动画的以下特征:

  • 持续时间:你可以指定动画的持续时间,默认是 300 毫秒
  • 插值时间:你可以指定函数,用于指定当前动画的已用时间
  • 重复次数和重复模式:你可以指定是否在动画结束时重复动画,以及重复次数。你还可以指定是否反向播放动画。
  • 动画设置:你可以将动画分为一组按照顺序执行或者指定延时执行
  • 帧刷新延迟:你可以设置刷新动画帧的频率。默认为 10 毫秒刷新一次,但最终速度取决于系统。

属性动画的完整示例,请看 GitHub 上 android-CustomTransition 示例中的 ChangeColor 类。

属性动画是怎么工作的

首先,我们通过一个例子来了解动画的动作原理,下图假设了一个对象,该对象使用它的 X 属性进行动画处理,该属性标识它在屏幕上的水平位置。动画持续时间设置为 40 毫秒,距离为 40 像素。默认的刷新频率是每 10 毫秒刷新一次,对象水平移动 10 像素。在 40 毫秒结束时,动画停止,意味着对象在水平 40 像素的位置。这是具有线性插值器的动画示例,意味着对象以恒定的速度移动。

你还可以指定动画使用非线性插值器,下图假设了一个对象,它在动画开始的时候加速,在动画结束的时候减速。动画仍然在 40 毫秒内移动 40 个像素,但是速度是非线性的。在开始时,动画加速到中间点,然后从中间点减速直到动画结束。下图中,动画开始和结束时的距离小于中间。

我们详细了解属性动画系统的重要组件是如何计算动画,下图描述了主类之间是如何相互协作的。

ValueAnimator 对象追踪(?)动画的执行时间以及当前时间下的属性值。

ValueAnimator 封装了一个插值器 TimeInterpolator 和一个 TypeEvaluator,由他们来定义属性的值,例如在上上张图中,使用的 TimeInterpolator 是 AccelerateDecelerateInterpolator,TypeEvaluator 是 IntEvaluator。

要启动动画,请创建 ValueAnimator 并为其设置属性的起始值和结束值,以及动画的持续时间。当你调用 start() 方法时,动画开始。在整个动画运行期间,ValueAniamtor 根据动画的总时长和已完成的时长计算出一个 0 到 1 之间的分数,这个分数表示动画完成的时间百分比,0 表示 0%,1表示 100%。例如在本节第一章图中,t = 10ms 的时候,分数为 2.5,因为总持续时间 t=40ms。

当 ValueAnimator 计算完已经完成的动画分数时,它会调用当前设置的 TimeInterpolator 来得到一个插值分数。在计算过程中,已完成动画百分比会被加入到新的插值计算中。如上图本节图 2 非线性动画中,因为动画的运动是缓慢加速的, 它的插值分数大约是 0.15,小于 t = 10ms 时的已完成动画分数 0.25。而在本节图 1 中,这个插值分数一直和已完成动画分数是相同的。

在计算插值分数时,ValueAnimator 会调用相应的 TypeEvaluator,根据插值分数,起始值和动画的结束值来设置动画的属性值。例如在本节图 2 中,插值分数在 t = 10ms 时为 15,因此此时属性的值为 所以属性值:0+0.15*(40-0)= 6。

属性动画和视图动画的区别

视图动画系统仅为 View 对象提供动画动能,因此如果要为非 View 对象设置动画,则必须实现自己的代码才能执行。视图动画系统也有限制,因为它只是将 View 对象的一些特征暴露给动画效果,例如 View 对象的缩放和旋转而背景颜色却不可以。

视图动画系统的另一个缺点是它只修改了绘制 View 的位置,为不是实际的 View 本身。假如你设定值了一个按钮在屏幕上移动,该按钮可以正常绘制,但出发点击事件的位置却不会更改,因此你必须实现自己的逻辑来处理这个问题。

使用属性动画系统,可以完全摆脱这些约束,可以为任何对象(View 和非 View)的任何属性设置的动画,并且实际修改对象本身。属性动画系统在执行动画方面也很强大。更深层次的讲,你还可以给需要动画的属性分配动画执行器,例如颜色、位置、尺寸以及能够定义的动画特性(例如插值和多个动画同步等等)。

但是视图动画系统创建耗时段,代码量也较少。如果视图动画能够满足需求,或者既存的代码已经完成了想要的动画效果,就无须再使用属性动画了。我们应该针对不同的情况来选择使用这两种不同的动画系统。

API 概述

你可以在 android.animation 包中找到大多数属性动画的 API。因为视图动画系统已经在 android.view.animation 中定义了许多插值器,所以你也可以在属性动画中使用这些插值器。下表列出了属性动画系统的主要组件。

Animator 类提供了创建动画的基本结构。通常不直接使用该类,因为它只提供必须扩展以完全支持动画值的最小功能。 下面的子类扩展Animator。

描述
ValueAnimator用于属性动画的主要计时引擎,它还为被动画化的属性计算值。它拥有计算动画值的所有核心功能并且包含每个动画的计时细节,关于一个动画是否重复的信息,接收更新事件的监听器,以及设置要求值的自定义类型的能力。对于正在动画的属性有两块问题:计算被动画的值,以及在正在被动画化的对象和属性上设置那些值。ValueAnimator 不解决第二块问题,所以你必须监听被 ValueAnimator 计算的值的更新,并且使用你自己的逻辑来修改你希望动画化的对象。参见关于 Animating with ValueAnimator 的章节以获得更多信息。
ObjectAnimatorValueAnimator 的一个子类,它允许你设置一个目标对象和要动画化的对象属性。当它为动画计算一个新值时此类相应地更新属性。推荐你尽量使用 ObjectAnimator,因为它使得修改动画化目标对象上的值的过程变得更为简单。然而,你有时希望直接地使用 ValueAnimator,因为 ObjectAnimator 有较多一点的限制,譬如需要修改一些在目标对象上特殊存在的属性。
AnimatorSet提供一种将动画分组的机制,以便他们可以关联运行。你可以将动画设置为一起播放、按顺序播放或者在延时播放。有关详细信息,请参阅:Choreographing multiple animations with Animator Sets

Evaluators 告诉属性动画系统该如何计算特定属性的值。他们获取 Animator 类提供的计时数据、动画开始和结束值,并根据此数据计算属性的值。属性动画系统提供以下 Evaluators:

Class/InterfaceDescription
IntEvaluator默认用于计算 int 型属性值的求值器。
FloatEvaluator默认用于计算 float 型属性值的求值器。
ArgbEvaluator默认用于计算表现为十六进制值的颜色属性值的求值器。
TypeEvaluator创建自定义 Evaluator 的接口。如果你正在动画化一个对象属性,它不是 int 型,float 型,或颜色,你必须实现 TypeEvaluator 接口以指定如何计算对象属性的被动画化的值。你也可以为 int 型,float 型,和颜色值指定一个自定义 TypeEvaluator。有关如何编写自定义求值程序的更多信息,请参阅 Using a TypeEvaluator

时间插值器定义如何把一个动画中的特定值计算为一个时间函数。例如,你可以指定动画在整个动画过程中线性地发生,意味着动画在整个时间内均匀地移动,或者你可以指定动画使用非线性时间,例如,在动画开始时加速和在动画结束时减速。下表描述在 android.view.animation 中包含的插值器。如果提供的插值器都不适合你的需要,请实现 TimeInterpolator 接口并创建你自己的插值器。参见 Using interpolators 以获得关于如何编写一个自定义插值器的更多信息。

Class/InterfaceDescription
AccelerateDecelerateInterpolator改变速率缓慢地开始和结束但通过中间时加速
AccelerateInterpolator改变速率缓慢地出发然后加速
AnticipateInterpolator刚开始向后。然后向前滑动。
AnticipateOvershootInterpolator刚开始向后,然后超过原来的值,最后返回到最后的值。
BounceInterpolator值到最后会回弹。
CycleInterpolator重复指定的次数。
DecelerateInterpolator速率开始快,然后减速。
LinearInterpolator速率不变,匀速。
OvershootInterpolator变化将超过起始值和最后值,然后回来。
TimeInterpolator允许你实现你自己的插补值的接口

使用 ValueAnimator 处理动画

ValueAnimator 类允许你通过指定一组 intfloat,或色彩值来创建动画。你调用它的一个工厂方法:ofint()offloat(),或 ofobject() 来得到一个 ValueAnimator。例如:

ValueAnimator animation = ValueAnimator.ofFloat(0f, 100f);
animation.setDuration(1000);
animation.start();

在这段代码中,start() 方法执行之后的 1000 毫秒期间,ValueAnimator 开始在 0 和 100 之间计算动画的值。

您还可以通过以下方式指定一个自定义类型来动画:

ValueAnimator animation = ValueAnimator.ofObject(new MyTypeEvaluator(), startPropertyValue, endPropertyValue);
animation.setDuration(1000);
animation.start();

在这段代码中,start() 方法执行后,ValueAnimator 开始使用 MyTypeEvaluator 提供的逻辑在 startPropertyValue 和 endPropertyValue 之间计算动画的值,持续时间为 1000 毫秒。

你可以通过 ValueAnimator 对象添加 AnimatorUpdateLinstener 来使用动画的值,如下所示:

animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator updatedAnimation) {
        // You can use the animated value in a property that uses the
        // same type as the animation. In this case, you can use the
        // float value in the translationX property.
        float animatedValue = (float)updatedAnimation.getAnimatedValue();
        textView.setTranslationX(animatedValue);
    }
});

onAnimationUpdate() 方法中,你可以访问更新的动画之,并在其中一个 View 的属性中使用它。有关监听器的更多内容,请参阅:Animation listeners

使用 ObjectAnimator 处理动画

ObjectAnimator 是 ValueAnimator 的子类,它将 ValueAnimator 的计时引擎和值计算与目标对象的属性结合在一起。这使得设置任何对象的动画都变得更容易,因为你无须实现 ValueAnimator.AnimatorUpdateLinstener,因为栋数属性会自动更新。

实例化 ObjectAnimator 和实例化 ValueAnimator 相似,但你还可以指定动画目标和目标属性的名称以及要动画的范围值。

ObjectAnimator animation = ObjectAnimator.ofFloat(textView, "translationX", 100f);
animation.setDuration(1000);
animation.start();

要正确的更新 ObjectAnimator 属性,必须执行以下操作:

  • 你要设置动画的属性必须具有 set<PropertyName>() 形式的 setter 函数。因为 ObjectAnimator 在动画期间自动更新属性值,所以它必须能过使用此 setter 方法来访问该属性。例如属性名称为 foo,则需要 setFoo() 方法。如果该 setter 方法不存在,则有如下选项:

    • 如果可能的话,添加 setter 方法
    • 使用一个你有权改变的封装器类,并且让那个封装器用一个可用的 set 方法接收值并且定向它至原来的对象。
    • 改为使用 ValueAnimator。
  • 如果仅仅为 ObjectAnimator 工厂方法之一中 的 value... 参数指定一个只,则默认该值为动画的结束值。因此,你还需要设置动画的对象属性必须有一个用于获取动画起始值的 getter 方法。getter 方法必须采用 get<PropertyName>() 的形式。例如,如果属性名为 foo,则需要使用 getFoo() 方法。
  • 动画属性的 setter 和 getter 方法必须和 ObjectAnimator 中的开始值和结束值类型相同。例如,如果构造以下 ObjectAnimator,则必须具有 targetObject.setPropNametargetObject.getPropName(float)

    ObjectAnimator.ofFloat(targetObject, "propName", 1f)
  • 根据你的动画化的属性或者对象,有时候你可能需要在 View 上调用 invalidate() 方法以强制屏幕使用新的属性值重绘自身。你可以在 onAnimationUpdate 会调用执行该操作。例如,设置 Drawable 对象的 color 属性的动画仅在该对象重绘自身时才会更新屏幕。在View上所有属性set方法,诸如 setAlpha() 和 setTranslationX() 可以正确地无效化 View,所以当使用新值调用这些方法时你不需要无效化 View。关于监听器的更多信息,参见 Animation listeners

使用 AnimatorSet 编写多个动画

很多时候,你想要播放一个动画,取决于另一个动画开始或者技术。Android 系统允许将多个动画捆绑成为一个 AnimatorSet,你可以指定是否同时启动动画、播放顺序、延时播放等。你还可以互相嵌套 AnimatorSet 对象。

下列示例代码用以下方式播放 Animator 对象:

  • 先播放 bounceAnim
  • squashAnim1squashAnim2stretchAnim1,和 stretchAnim2同时播放。
  • 播放bounceBackAnim
  • 播放fadeAnim
AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(bouncer).before(fadeAnim);
animatorSet.start();

动画监听器

您可以在动画持续时间期间使用下面列出监听器监听重要事件。

  • Animator.AnimatorListener

  • ValueAnimator.AnimatorUpdateListener

    • onAnimationUpdate()),在动画的每一帧上调用。监听该时间以使用每个动画期间由 ValueAnimator 计算的值。 要使用该值,使用 getAnimatedValue() 方法查询被传递进事件的 ValueAnimator 以获得当前被动画化的值。 如果使用 ValueAnimator,则需要实现该监听器。

    根据您动画的属性或对象,您可能需要调用 View 的 invalidate() 以强制屏幕的该区域使用新的动画值重绘自身。 例如,为 Drawable 对象的 color 属性设置动画只会在该对象重绘自身时导致对屏幕的更新。 View 上的所有属性设置器(例如setAlpha() 和 setTranslationX() 都会使 View无效,因此在使用新值调用这些方法时,无需使 View 无效。

如果你不想实现 Animator.AnimatorListener 接口的所有方法,则可以扩展 AnimatorListenerAdapter 类,而不是实现 Animator.AnimatorListener 接口。AnimatorListenerAdapter 类提供了可以选择覆盖的方法的空实现。

例如,下面的代码仅为 onAnimationEnd() 回调创建 AnimatorListenerAdapter:

ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
fadeAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
    balls.remove(((ObjectAnimator)animation).getTarget());
}

ViewGroup布局更改动画

属性动画系统提供了对 ViewGroup 对象进行动画处理的功能,并提供了一中简单的方法来为 View 对象设置动画。

你可以使用 LayoutTransition 类为 ViewGroup 中的布局更改设置动画。当你将 ViewGroup 中个的 View 添加到 ViewGroup 或者从 ViewGroup 中删除它们抑或是调用 View.setVisibility() 方法设置 VISIBLE、INVISIBLE 或者 GONE 时,ViewGroup 中的 View 可以展示显示或消失动画。在添加或者删除 View 时,ViewGroup 中的其余 View 也可以动画到新位置。通过调用 setAnimator() 并使用以下 layoutTransition 常量之一传入 Animator 对象,可以在 layoutTransition 对象中定义以下动画:

  • APPEARING - 指示在容器中出现的项目上运行的动画的标志。
  • CHANGE_APPEARING - 指示由于容器中出现新项目而正在更改的项目上运行的动画的标志。
  • DISAPPEARING - 指示在从容器中消失的项目上运行的动画的标志。
  • CHANGE_DISAPPEARING - 指示由于项目从容器中消失而正在更改的项目上运行的动画的标志。

你可以为这四种类型的时间定义自己的自定义动画,以自定义布局过度的外观,或者只是使用默认动画。

API Demos 中的 LayoutAnimations 示例显示了如何为布局转换定义动画,然后在要动画化的 View 对象上设置动画。

LayoutAnimationsByDefault 及其对应的 layout_animations_by_default.xml 布局资源文件向您展示如何在 XML 中为 ViewGroup 启用默认布局转换。你唯一需要做的就是为 ViewGroup 设置 android:animateLayoutchanges 属性为 true。例如:

<LinearLayout
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:id="@+id/verticalContainer"
    android:animateLayoutChanges="true" />

将此属性设置为 true,当从 ViewGroup 中添加或删除 View 时,会自动为添加/删除的 View 以及其余 View 设置动画。

使用 StateListAnimator 创建视图状态变更动画

StateListAnimator 类允许你定义在 View 状态改变时的的动画。该对象充当动画对象的包装器,每当指定的视图状态(如“按下”或“获取焦点”)更改时调用该动画。

StateListAnimator 可以在 XML 资源中定义,其中包含 <selector> 元素为根元素,包裹着 <item> 元素,每个元素都指定由 StateListAnimator 类定义的不同视图状态。 每个 <item> 包含属性动画集的定义。

例如,以下文件创建一个状态列表动画,它在按下时更改 View 的 x 和 y 比例:

res/xml/animate_scale.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- the pressed state; increase x and y size to 150% -->
    <item android:state_pressed="true">
        <set>
            <objectAnimator android:propertyName="scaleX"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1.5"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="scaleY"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1.5"
                android:valueType="floatType"/>
        </set>
    </item>
    <!-- the default, non-pressed state; set x and y size to 100% -->
    <item android:state_pressed="false">
        <set>
            <objectAnimator android:propertyName="scaleX"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1"
                android:valueType="floatType"/>
            <objectAnimator android:propertyName="scaleY"
                android:duration="@android:integer/config_shortAnimTime"
                android:valueTo="1"
                android:valueType="floatType"/>
        </set>
    </item>
</selector>

要将状态列表动画器设置到 View,请添加 android:stateListAnimator 属性,如下所示:

<Button android:stateListAnimator="@xml/animate_scale"
        ... />

现在,当该按钮的状态发生变化时,将使用 animate_scale.xml 中定义的动画。

为了讲状态列表动画器设置给代码中的 View,请使用 AnimatorInflater.loadStateListAnimator() 方法,并使用 View.setStateListAnimator() 方法将动画设置到 View 上。

你也可以使用 AnimatedStateListDrawable 在状态更改之间播放 drawable 动画。Android 5.0 中的某些系统小部件默认使用这些动画。一下示例显示如何将 AnimatedStateListDrawable 定义为 XML 资源:

<!-- res/drawable/myanimstatedrawable.xml -->
<animated-selector
    xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- provide a different drawable for each state-->
    <item android:id="@+id/pressed" android:drawable="@drawable/drawableP"
        android:state_pressed="true"/>
    <item android:id="@+id/focused" android:drawable="@drawable/drawableF"
        android:state_focused="true"/>
    <item android:id="@id/default"
        android:drawable="@drawable/drawableD"/>

    <!-- specify a transition -->
    <transition android:fromId="@+id/default" android:toId="@+id/pressed">
        <animation-list>
            <item android:duration="15" android:drawable="@drawable/dt1"/>
            <item android:duration="15" android:drawable="@drawable/dt2"/>
            ...
        </animation-list>
    </transition>
    ...
</animated-selector>

使用 TypeEvaluator

如果要为 Android 系统未知的类型设置动画,可以通过实现 TypeEvaluator 接口来创建自己的求值器。Android 系统已知的类型为 int、float 或 color,分别对应 IntEvaluatorFloatEvaluatorArgbEvaluator

TypeEvaluator 接口只需要实现一个方法 evaluate(),这允许你正在使用的动画在动画当前点为你的动画属性返回适当的值。FloatEvaluator 类演示了如何执行此操作:

public class FloatEvaluator implements TypeEvaluator {

    public Object evaluate(float fraction, Object startValue, Object endValue) {
        float startFloat = ((Number) startValue).floatValue();
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
    }
}

当 ValueAnimator(或 ObjectAnimator)运行时,它会计算动画当前流逝的部分(介于 0 和 1 之间的一个值),然后根据你使用的插值器计算插值版本。内插分数是 TypeEvaluator 通过分数参数接收的值,因此计算动画值时不必考虑插值器。

使用 Interpolators

内插器定义如何根据时间函数计算动画中的特定值。例如,您可以指定动画在整个动画中线性发生,这意味着动画在整个时间内均匀移动,或者您可以指定使用非线性时间的动画,例如,在开始或结束时使用加速或减速动画。

动画系统中的 Interpolators 从 Animators 接收代表动画经过时间的一小部分。开发者编辑它们,以符合其旨在提供的动画类型。 Android 系统在 android.view.animation 包中提供了一组常见的 Interpolators。如果这些都不符合您的需求,您可以实现 TimeInterpolator 接口并创建自己的 Interpolators。

例如,下面比较默认 Interpolators AccelerateDecelerateInterpolator 和 LinearInterpolator 如何计算插值分数。LinearInterpolator 对经过的分数没有影响。 AccelerateDecelerateInterpolator 加速进入动画并减速。以下方法定义了这些插值器的逻辑:

AccelerateDecelerateInterpolator

@Override
public float getInterpolation(float input) {
    return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}

LinearInterpolator

@Override
public float getInterpolation(float input) {
    return input;
}

下表表示这些 Interpolators 为持续 1000ms 的动画计算的近似值:

ms elapsedElapsed fraction/Interpolated fraction (Linear)Interpolated fraction (Accelerate/Decelerate)
000
2000.20.1
4000.40.345
6000.60.8
8000.80.9
100011

如表所示,LinearInterpolator 以相同的速度更改值,每经过 200 毫秒增加 0.2。 AccelerateDecelerateInterpolator 在 200ms 和 600ms 之间比 LinearInterpolator 更快地更改值,在 600ms 和 1000ms 之间更慢。

指定关键帧

一个关键帧对线由一个 时间/值 的键值对组成,它允许你在动画的特定时间定义特定的状态。每个关键帧还可以有自己的插值器,以控制前一个关键帧时间与该关键帧时间之间的间隔内的动画行为。

要实例化一个 Keyframe 对象,必须使用工厂方法 ofInt()ofFloat()ofObject() 其中之一来获取适当类型的 Keyframe。然后调用 ofKeyframe() 工厂方法来获取 PropertyValuesHolder 对象。获得对象后,就可以通过传入 PropertyValuesHolder 对象和要设置动画的对象来获取动画效果。以下代码段演示了如何执行此操作:

Keyframe kf0 = Keyframe.ofFloat(0f, 0f);
Keyframe kf1 = Keyframe.ofFloat(.5f, 360f);
Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2);
ObjectAnimator rotationAnim = ObjectAnimator.ofPropertyValuesHolder(target, pvhRotation);
rotationAnim.setDuration(5000);

动画 View

属性动画允许 View 对象简化动画,并且和视图动画相比起来有一些优点。视图动画系统通过改变它们的绘制方式来转换视图对象,这是在每个视图的容器中处理的,因为 View 本身没有要处理的属性。这导致即使它被绘制在屏幕的不同位置,但它的对象还处于原始位置。在 Android 3.0 中,添加了新的属性和相应的 getter 和 setter 方法来消除这个缺点。

属性动画可以通过修改 View 对象中的实际属性来为屏幕上的 View 设置动画。此外,View 还会自动调用 invalidate() 方法,以便使其属性发生变化时刷新屏幕。有助于属性动画的View类中的新属性是:

  • translationXtranslationY:这些属性控制 View 所在的位置,作为由其布局容器设置的左侧和顶部坐标的增量。
  • rotation, rotationX, 和 rotationY:这些属性控制 2D(旋转属性)中的旋转和 3D 的围绕轴点。
  • scaleXscaleY:这些属性控制围绕其轴心点的视图的 2D 缩放。
  • pivotXpivotY:这些属性控制轴点的位置,围绕该枢轴点发生旋转和缩放变换。 默认情况下,轴心点位于对象的中心。
  • xy:用于描述View在其容器中的最终位置,作为左值和顶值以及 translationXtranslateY值的总和。
  • alpha:表示 View 的 Alpha 透明度。 默认情况下,此值为 1(不透明),值为 0 表示完全透明(不可见)。

要为 View 对象的属性设置动画,例如其颜色或者旋转值,你需要做的就是创建一个 property animator 并制定要设置动画的 View 的属性。例如:

ObjectAnimator.ofFloat(myView, "rotation", 0f, 360f);

有关创建 animators 的更多信息,请参阅 :ValueAnimatorObjectAnimator

使用 ViewPropertyAnimator 处理动画

ViewPropertyAnimator 提供了一种简单的方法,可以使用单个 Animator 对象并行地对 View 的几个属性进行动画处理。它的行为与 ObjectAnimator 非常相似,因为它修改了视图属性的实际值,但在同时动画多个属性时效率更高。 此外,使用 ViewPropertyAnimator 的代码更简洁,更易于阅读。 以下代码片段显示了在同时动画视图的 x 和 y 属性时使用多个 ObjectAnimator 对象,单个 ObjectAnimator 和 ViewPropertyAnimator 的差异。

多个 ObjectAnimator 对象

ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f);
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f);
AnimatorSet animSetXY = new AnimatorSet();
animSetXY.playTogether(animX, animY);
animSetXY.start();

一个 ObjectAnimator

PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);
ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvhY).start();

ViewPropertyAnimator

myView.animate().x(50f).y(100f);

有关 ViewPropertyAnimator 的更多详细信息,请参阅 blog post

以 XML 格式声明动画

属性动画系统允许您使用 XML 而不是以代码的方式声明。 通过在 XML 中定义动画,您可以轻松地在多个 Activity 中重复使用动画,并更轻松地编辑动画序列。

要区分使用新属性动画 API 的动画文件和使用旧版视图动画框架的动画文件,从Android 3.1 开始,您应该在 res/animator/ 目录中保存属性动画的 XML 文件。

属性动画类对应的标签:

  • ValueAnimator 对应 <animator>
  • ObjectAnimator 对应 <objectAnimator>
  • AnimatorSet 对应 <set>

要查找您可以在XML声明中使用的属性,请参阅 Animation resources。以下示例顺序播放两组对象动画,第一个嵌套集合将两个对象动画一起播放:

<set android:ordering="sequentially">
    <set>
        <objectAnimator
            android:propertyName="x"
            android:duration="500"
            android:valueTo="400"
            android:valueType="intType"/>
        <objectAnimator
            android:propertyName="y"
            android:duration="500"
            android:valueTo="300"
            android:valueType="intType"/>
    </set>
    <objectAnimator
        android:propertyName="alpha"
        android:duration="500"
        android:valueTo="1f"/>
</set>

要运行次动画,必须将 XML 资源 inflate 为 AnimatorSet 对象,然后在启动动画集之前为动画设置目标对象。为方便起见,调用 setTarget() 为 AnimatorSet 的所有子项设置单个目标对象。以下代码显示了如何执行此操作:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
    R.animator.property_animator);
set.setTarget(myObject);
set.start();

你还可以在 XML 中声明 ValueAnimator,如下所示:

<animator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:valueType="floatType"
    android:valueFrom="0f"
    android:valueTo="-100f" />

要在代码中使用以前的 ValueAnimator,必须 inflate 对象,添加 AnimatorUpdateListener,获取更新的动画值,并在其中一个 View 的属性中使用它,如下所示:

ValueAnimator xmlAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(this,
        R.animator.animator);
xmlAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator updatedAnimation) {
        float animatedValue = (float)updatedAnimation.getAnimatedValue();
        textView.setTranslationX(animatedValue);
    }
});

xmlAnimator.start();

有关定义属性动画的 XML 语法信息,请参阅 Animation resources

对 UI 性能的影响

使用 UI 的 Animator 会为动画运行的每一帧执行额外的渲染工作。因此,使用资源密集型动画会对应用的性能产生负面影响。
动画 UI 所需的工作将添加到渲染管道的阶段。你可以通过启用 Profile GPU 渲染和监控动画执行阶段来了解你的动画是否会影响到应用的性能。更多相关信息,请参阅:Profile GPU rendering walkthrough

绘图动画

在某些情况下,图像需要在屏幕上设置动画。如果需要显示由多个图像组成的自定义加载动画,或者在用户操作后希望一个图标变成另一个图标,则此选项非常有用。Android 提供了几个绘制动画选项。

第一个选项是使用 Drawable 动画,这允许您指定多个静态的 Drawable 文件,这些文件将依次显示以实现动画效果。第二个选项是使用 Vector Drawable 动画,它允许你为矢量 Drawable 的属性设置动画。

使用绘图动画

Drawable 动画是一种逐个加载一系列 Drawable 资源以创建动画效果的方式。这是一个传统的动画,某种意义上来说,它就像是一部电影。AnimationDrawable 类是 Drawable 动画的基础。

肃然你可以使用 AnimationDrawable 类 API 在代码中定义动画的帧,但更简单的方式是使用一个 XML 文件来列出组成动画的帧。这种动画的 XML 帧应该放置于 Android 项目的 res/drawable 目录下。在这种情况下,指令是动画的每个帧的顺序和持续时间。

XML 文件由 <animation-list> 根节点和一系列 <item> 子节点组成,每个子节点定义一个帧:帧的 Drawable 资源和持续时间。以下是 Drawable 动画的 XML 文件示例:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="true">
    <item android:drawable="@drawable/rocket_thrust1" android:duration="200" />
    <item android:drawable="@drawable/rocket_thrust2" android:duration="200" />
    <item android:drawable="@drawable/rocket_thrust3" android:duration="200" />
</animation-list>

这个动画只运行三帧。通过将列表的 android:oneshot 属性设置为 true,设置该动画只循环一次,然后停止并保持在最后一帧。如果设置为 false,则动画将一直循环。在项目的 res/drawable/ 目录中,这个 XML 保存为 rocket_pust.xml,可以将它作为背景图像添加到 View 中,然后调用以进行播放。这是一个示例 Activity,其中动画被添加到 ImageView,然后在触摸屏幕时进行动画处理:

AnimationDrawable rocketAnimation;

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image);
  rocketImage.setBackgroundResource(R.drawable.rocket_thrust);
  rocketAnimation = (AnimationDrawable) rocketImage.getBackground();

  rocketImage.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        rocketAnimation.start();
      }
  });
}

需要注意的是,在 Activity 的 onCreate() 中,无法调用 AnimationDrawable 的 start() 方法,因为 AnimationDrawable 还没有完全附加到 Window。如果你想立即播放动画而不需要交互,那么你应该在 Activity 的 onStart() 方法中启动它。

更多关于 XML 的语法,可用标记和属性的信息,请参阅:Animation Resources

使用 AnimatedVectorDrawable

矢量 Drawable 是一种可缩放的 Drawable,它不会出现像素化或是模糊。AnimatedVectorDrawable 类(以及用于向后兼容的AnimatedVectorDrawableCompat)允许我们为矢量 Drawable 的属性设置动画,例如旋转或者更改路径以将其变形为不同的图像。

通常在三个 XML 文件中定义动画矢量绘图:

  • 使用 res/drawable 中带有 <vector> 元素绘制的矢量。
  • 使用 res/drawable/ 中的 <animated-vector> 元素绘制的动画矢量
  • res/animator 中具有 <objectAnimator> 元素的一个或者多个 Object Animator

动画矢量 Drawable 可以动画化 <group><path> 元素的属性。<group> 元素定义一组路径或者子组,<path> 元素定义要绘制的路径。

定义要制作动画的矢量 Drawable 时,请使用 android:name 属性为组和路径指定唯一的名称,以便可以从动画程序定义中引用。例如:

res/drawable/vectordrawable.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="64dp"
    android:width="64dp"
    android:viewportHeight="600"
    android:viewportWidth="600">
    <group
        android:name="rotationGroup"
        android:pivotX="300.0"
        android:pivotY="300.0"
        android:rotation="45.0" >
        <path
            android:name="v"
            android:fillColor="#000000"
            android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
    </group>
</vector>

动画矢量 Drawable 定义是指矢量 Drawable 的组和路径的名称:

res/drawable/animatorvectordrawable.xml

<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
  android:drawable="@drawable/vectordrawable" >
    <target
        android:name="rotationGroup"
        android:animation="@animator/rotation" />
    <target
        android:name="v"
        android:animation="@animator/path_morph" />
</animated-vector>

动画定义表示 ObjectAnimator 或 AnimatorSet 对象。 此示例中的第一个动画师将目标组旋转 360 度:

res/animator/rotation.xml

<objectAnimator
    android:duration="6000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360" />

该例子中的第二个 Animator 将矢量 Drawable 的路径从一个形状变换为另一个形状。两个路径必须兼容变形:他们必须从具有相同数量的命令以及每个命令的相同数量的参数。

res/animator/path_morph.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="3000"
        android:propertyName="pathData"
        android:valueFrom="M300,70 l 0,-70 70,70 0,0   -70,70z"
        android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
        android:valueType="pathType" />
</set>

这是生成的 AnimatedVectorDrawable:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/images/guide/topics/graphics/multi_file_animated_vector_drawable.mp4" type="video/mp4">

</video>

更多信息,请参阅:AnimatedVectorDrawable

## 使用动画显示/隐藏 View

在使用你的应用时,需要在屏幕上显示新信息同时删除旧信息。立即切换显示的内容可能看起来很刺眼,或者用户很容易错过屏幕上的新内容。利用动画可以减慢变化并引起用户的注意,因此更新效果更加明显。

显示或隐藏 View 时,有三种常用的动画可供选择。你可以使用圆形显示动画、交叉淡入淡出动画或者卡片翻转动画。

创建交叉淡入淡出动画

该动画会初见淡出一个 View 或者 ViewGroup,同时淡入另一个 View 或者 ViewGroup。该动画对于希望在应用程序中切换内容或 View 的情况非常有用。此处显示的 交叉淡入淡出动画使用的是 ViewPropertyAnimator,它适用于 Android 3.1(API 21)或更高版本。

下面是从 Progress 到文本内容的交叉淡入淡出示例。

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_crossfade.mp4" type="video/mp4">

</video>

创建 View

首先,需要创建两个需要交叉淡入淡出的视图。一下示例创建 Progress 和可滚动文本 View

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

设置交叉淡入淡出动画

To set up the crossfade animation:

  1. 为交叉淡入淡出的 View 创建成员变量。稍候在动画期间修改 View 的时候需要这些引用。
  2. 对于正在淡入淡出的 View,将其可见性设置为 GONE。这可以防止 View 占用布局控件,并从布局计算中忽略它,从而加快处理速度。
  3. 在变量成员中缓存 config_shortAnimTime 属性。该属性一定动画的标准“短”持续时间。次持续时间非常适合频繁出现的动画或者微小动画。如果您想使用它们,也可以使用 config_longAnimTimeconfig_mediumAnimTime

以下是使用上一代码片段中的布局作为活动内容视图的示例:

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

交叉淡入淡出 View

现在已经正确设置了 View,通过执行以下操作交叉淡入淡出它们:

  1. 对于淡入的 View,将 alpha 值设置为 0,可见性设置为可见。(请记住,最初设置为“消失”)这使视图可见但完全透明。
  2. 对于淡入的 View,将其 alpha 值从 0 设置为 1。对于淡出的视图,将 alpha 值从 1 设置为 0。
  3. animator.animatorListener 中使用 OnAnimationEnd(),将淡出的 View 的可见性设置为“GONE”。即使 alpha 值为 0,将视图的可见性设置为“GONE”也会阻止视图占用布局空间,并从布局计算中忽略它,从而加快处理速度。

下面的方法显示了如何执行此操作的示例:

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

创建卡片翻转动画

Card Flip 动画效果模拟卡片翻转,在两个视图之间进行切换。此处显示的卡片翻转动画使用 FragmentTransaction,可用于 Android 3.0(API 级别 11)及更高版本。

这是卡片翻转的样子:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_card_flip.mp4" type="video/mp4">

</video>

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

创建 View

“卡片”的每一面都是一个单独的布局,可以包含你想要的任何内容,例如两个文本视图、两个图片或者是任意 View 组合。然后,您将在 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 类:

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 代码中,将内容 View 设置为刚刚创建的布局。在创建 Activity 时显示默认 Fragment 也是个不错的选择,因此一下实例 Activity 将向您显示默认情况下如何显示卡片的正面:

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 栈中,这样当用户按下返回键时,卡片会翻转过来。
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();
    }
}

创建圆形显示动画

当你显示或者隐藏一组 UI 时,显示动画可以为用户提供视觉连续性。ViewAnimationUtils.createCircularReveal() 方法使您可以设置剪贴圆的动画以显示或隐藏 View。该动画在 ViewAnimationUtils 类中提供,该类适用于 Android 5.0(API 级别 21)及更高版本。

这是一个示例,展示如何显示以前不可见的视图:

// 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() 需要五个参数。第一个参数是你需要隐藏或显示的 View。接下来两个参数是剪切圆形的 X 和 Y 坐标。通常这是 View 的中心,但你也可以使用用户触摸的点,一边动画从他们选择的位置开始。第四个参数是剪切圆的起始半径。

在上面的例子中,初始半径为 0,因此需要显示的 View 将被圆圈隐藏。最后一个参数是圆的最终半径。显示 View 时,请确保最终半径大于 View 本身,以便在动画完成之前完全显示 View。

隐藏以前可见的 View:

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

在这种情况下,剪切圆的初始半径设置为 View 一样大,因此在动画开始之前 View 将可见。最终半径设置为 0,因此在动画结束时将隐藏 View。想动画添加监听器非常重要,这样在动画完成时可以将 View 的可见性设置为 INVISIBLE

使用动画移动 View

由于用户的交互或者一些后台动作,屏幕上的 View 通常需要重新定位。这种情况你应该使用动画讲起从起始位置移动到接收位置,而不是立即更新对象位置(这会使其中一个区域闪烁到另一个区域)。

Android 提供了一些方法,允许你在屏幕上重新定位 View,例如 ObjectAnimator。你可以听希望对象定位的结束位置,以及动画的持续时间。你可以将它们和插值器结合使用,以控制动画加速或减速

使用 ObjectAnimator 更改 View 的位置

ObjectAnimator API 提供了可以在指定的持续时间内更改 View 的属性的简单方法。它包含了一些静态方法来创建 ObjectAnimator 实例,这些静态方法的选用取决于设置动画的属性类型。在屏幕上重新定位 View 时,将使用 translationxtranslationy 属性。

这是一个 ObjectAnimator 的例子,它在 2 秒内将 View 从屏幕左侧向右移动 100 像素。

ObjectAnimator animation = ObjectAnimator.ofFloat(view, "translationX", 100f);
animation.setDuration(2000);
animation.start();

上例中使用 objectAnimator.offloat() 方法,因为转换值是 float 类型的。第一个参数是要设置动画的 View。第二个参数是要设置动画的 View 的属性。由于需要水平移动 View,因此需要使用 translationX 属性。最后一个参数是动画的结束值。因为这个值是 100.所以屏幕左侧会空出 100 像素(向右移动)。

下一个方法指定动画的持续时间,该例中,动画将运行 2 秒(2000毫秒)。

最后一个方法使得动画开始执行,这会更新 View 在屏幕上的位置。

有关使用 ObjectAnimator 的更多信息,请参阅:Animating with ObjectAnimator

添加弯曲运动

虽然使用 ObjectAnimator 很方便,但默认情况下它会使用起点和终点之间的直线重新定位 View。Material design 不仅依赖于动画时间的曲线,还依赖于屏幕上对象的空间移动。使用曲线运动可以让你的应用程序有更多的材料感,同时使你的动画更有趣。

使用PathInterpolator

PathInterpolator 类是 Android 5.0(API 21)中引入的一个新的 interpolator。 它基于 Bézier 曲线Path对象。 此插值器指定 1x1 平方的运动曲线,锚点位于 (0, 0)(1, 1),控制点使用构造函数参数指定。 创建 PathInterpolator 的一种方法是创建 Path 对象并将其提供给 PathInterpolator:

// arcTo() and PathInterpolator only available on API 21+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  Path path = new Path();
  path.arcTo(0f, 0f, 1000f, 1000f, 270f, -180f, true);
  PathInterpolator pathInterpolator = new PathInterpolator(path);
}

您还可以将 PathInterpolator 定义为 XML 资源:

<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
    android:controlX1="0.4"
    android:controlY1="0"
    android:controlX2="1"
    android:controlY2="1"/>

创建 PathInterpolator 对象之后,可以将它传递给 Animator.setInterpolator() 方法。然后 Animator 将使用 Interpolator 确定的启动时序或路径曲线。

ObjectAnimator animation = ObjectAnimator.ofFloat(view, "translationX", 100f);
animation.setInterpolator(pathInterpolator);
animation.start();

自定义路径

ObjectAnimator 类具有新的构造函数,使您可以使用两个或多个属或者路径为路径上的坐标设置动画。 例如,以下 Animator 使用 Path 对象为视图的 X 和 Y 属性设置动画:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  Path path = new Path();
  path.arcTo(0f, 0f, 1000f, 1000f, 270f, -180f, true);
  ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.X, View.Y, path);
  animator.setDuration(2000);
  animator.start();
} else {
  // Create animator without using curved path
}

效果是这样的:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/videos/curved_path_animation.mp4" type="video/mp4">

</video>

如果你不想创建自己的路径曲线,系统为 material design 设计规范中的三条基本曲线提供了 XML 资源:

  • @interpolator/fast_out_linear_in.xml
  • @interpolator/fast_out_slow_in.xml
  • @interpolator/linear_out_slow_in.xml

使用 Fling 动画移动 View

基于 Fling 的动画使用与物体速度成比例的摩擦力。使用它来设置对象属性的动画,并希望逐渐结束动画。它从手势速度接受一个最初的动量,然后逐渐减速。当动画的速度降到足够低,在屏幕上看不到任何变化时,动画就结束了。

要了解相关主题,请看:

添加支持库

要使用物理特性,需要添加支持库:

  1. 打开应用模块的 build.gradle 文件。
  2. 将支持库添加到依赖项部分。

    dependencies {
        implementation 'com.android.support:support-dynamic-animation:28.0.0'
    }

创建Fling动画

Flinganimation 类允许您为对象创建一个 Fling 动画。要构建一个 FlingAnimation,请创建一个 FlingAnimation 类的实例,并提供一个对象和要设置动画的对象属性。

FlingAnimation fling = new FlingAnimation(view, DynamicAnimation.SCROLL_X);

设置速度

开始速度定义动画属性在动画开始时更改的速度。默认的开始速度设置为每秒 0 像素。因此,必须定义开始速度以确保动画不会立即结束。

你可以使用一个固定值作为起始速度,也可以根据触摸收拾的速度来确定。如果选择固定值,则应以 Xdp/秒 为单位定义该值,然后将其转换为 X像素/秒。 以 dp/秒 定义值允许速度独立于设备的密度和形状因子。 有关将起始速度转换为 像素数/秒 的更多信息,请参阅 Spring Animation 中的 Converting dp per second to pixels per second 部分。

要设置速度,请调用 setStartVelocity() 方法并以 像素/秒 传递速度。该方法返回了设置速度 fling 对象。

注意:分别使用 GestureDetector.OnGestureListenerVelocityTracker 类来检索和计算触摸手势的速度。

设置动画值范围

当要将属性值限制到某个范围时,可以设置最小动画值和最大动画值。当对具有拱顶范围的属性(如 alpha 为 从 0 到 1)进行动画时,该范围十分尤为有用。

注意:当一个 Fling 动画的值达到最小或最大时,动画结束。

要设置最小值和最大值,请分别调用 setMinValue()setMaxValue() 方法。这两种方法都返回已为其设置值的动画对象。

设置摩擦系数

调用 setFriction()方法更改动画的摩擦力。它定义动画中速度降低的速度。

注意:如果不在动画开始时设置摩擦系数,动画将使用默认的摩擦系数 1。

该方法返回使用您提供的摩擦值的动画对象。

示例

下面的例子展示了水平抛投。从速度跟踪器捕获的速度是速度 x,滚动边界设置为 0 到 maxscroll。摩擦系数设置为1.1。

FlingAnimation fling = new FlingAnimation(view, DynamicAnimation.SCROLL_X);
fling.setStartVelocity(-velocityX)
        .setMinValue(0)
        .setMaxValue(maxScroll)
        .setFriction(1.1f)
        .start();

设置最小可见变化

当给没有定义像素的自定义属性设置动画时,应设置用户可见的动画值的最小更改。它确定结束动画的合理阈值。

在动态显示 dynamicanization.viewProperty 时不需要调用此方法,因为最小可见更改是从该属性派生的。例如:

  • 默认的最小可见更改值为 1 个像素,用于查看属性,如 Translation_xTranslation_yTranslation_zScroll_xScroll_y
  • 对于使用旋转的动画,例如 ROTATIONROTATION_XROTATION_Y,最小可见变化为MIN_VISIBLE_CHANGE_ROTATION_DEGREES1/10像素
  • 对于使用不透明度的动画,最小可见更改为 MIN_VISIBLE_CHANGE_ALPHA1/256

想要设置动画的最小可见更改,请调用 setMinimumVisibleChange() 方法并传递最小可见常量之一或自定义属性需要计算的值。有关计算此值的详细信息,请参阅: Calculating a minimum visible change value

anim.setMinimumVisibleChange(DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE);

注意:仅当动画自定义属性未定义为像素时,才需要传递值。

计算最小可见变化值

要计算自定义属性的最小可见更改值,请使用以下公式:

最小可见变化=自定义属性值的范围/以像素为单位的分布范围

例如,要设置动画的属性从 0 到 100。这相当于 200 像素的变化。根据公式,最小可见变化值为 100/200 = 0.5 像素

使用缩放动画放大 View

本节内容演示如何进行触摸缩放动画,这对于应用程序(如照片库)将视图从缩略图动画到填充屏幕的全尺寸图像非常有用。

以下展示了触摸缩放动画,它可以扩展图像缩略图以填充屏幕:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_zoom.mp4" type="video/mp4">

</video>

如果你想看一个完整的工作示例,请参阅GitHub上 android-WearSpeakerSample 项目中的 UIAnimation 类。

创建 Views

创建一个布局文件,其中包含要缩放的内容的大小版本。下面的示例为可单击的图像缩略图创建一个ImageButton,并创建一个显示图像放大视图的ImageView:

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

    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <ImageButton
            android:id="@+id/thumb_button_1"
            android:layout_width="100dp"
            android:layout_height="75dp"
            android:layout_marginRight="1dp"
            android:src="@drawable/thumb1"
            android:scaleType="centerCrop"
            android:contentDescription="@string/description_image_1" />

    </LinearLayout>

    <!-- This initially-hidden ImageView will hold the expanded/zoomed version of
         the images above. Without transformations applied, it takes up the entire
         screen. To achieve the "zoom" animation, this view's bounds are animated
         from the bounds of the thumbnail button above, to its final laid-out
         bounds.
         -->

    <ImageView
        android:id="@+id/expanded_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible"
        android:contentDescription="@string/description_zoom_touch_close" />

</FrameLayout>

设置缩放动画

应用布局后,请设置触发缩放动画的事件处理程序。以下示例将 view.onclickListener 添加到 ImageButton,以便在用户单击图像按钮时执行缩放动画:

public class ZoomActivity extends FragmentActivity {
    // Hold a reference to the current animator,
    // so that it can be canceled mid-way.
    private Animator currentAnimator;

    // The system "short" animation time duration, in milliseconds. This
    // duration is ideal for subtle animations or animations that occur
    // very frequently.
    private int shortAnimationDuration;

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

        // Hook up clicks on the thumbnail views.

        final View thumb1View = findViewById(R.id.thumb_button_1);
        thumb1View.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                zoomImageFromThumb(thumb1View, R.drawable.image1);
            }
        });

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

缩放 View

现在,您需要在适当的时候从正常大小的 View 动画到缩放的 View。通常,您需要从正常大小 View 的边界到较大大小 View 的边界设置动画。以下方法演示如何通过执行以下操作实现从图像缩略图缩放到放大视图的缩放动画:

  1. 将高分辨率图像指定给隐藏的“放大”图像 View。为了简单起见,下面的示例在 UI 线程上加载一个大的图像资源。您可能希望在单独的线程中进行此加载,以防止在 UI 线程上阻塞,然后在 UI 线程上设置 Bitmap。理想情况下,Bitmap 不应大于屏幕大小。
  2. 计算 ImageView 的开始和结束边界。
  3. 同时为四个定位和大小调整属性 x、y(缩放x和缩放y)中的每一个设置动画,从开始边界到结束边界。这四个动画被添加到动画设置中,以便它们可以同时启动。
  4. 通过运行一个类似的动画来缩小,但当用户在放大图像时触摸屏幕时,则相反。可以通过向 ImageView 添加 View.OnClickListener 来完成此操作。单击时,图像 View 将缩小到图像缩略图的大小,并将其可见性设置为 GONE 以隐藏它。
private void zoomImageFromThumb(final View thumbView, int imageResId) {
    // If there's an animation in progress, cancel it
    // immediately and proceed with this one.
    if (currentAnimator != null) {
        currentAnimator.cancel();
    }

    // Load the high-resolution "zoomed-in" image.
    final ImageView expandedImageView = (ImageView) findViewById(
            R.id.expanded_image);
    expandedImageView.setImageResource(imageResId);

    // Calculate the starting and ending bounds for the zoomed-in image.
    // This step involves lots of math. Yay, math.
    final Rect startBounds = new Rect();
    final Rect finalBounds = new Rect();
    final Point globalOffset = new Point();

    // The start bounds are the global visible rectangle of the thumbnail,
    // and the final bounds are the global visible rectangle of the container
    // view. Also set the container view's offset as the origin for the
    // bounds, since that's the origin for the positioning animation
    // properties (X, Y).
    thumbView.getGlobalVisibleRect(startBounds);
    findViewById(R.id.container)
            .getGlobalVisibleRect(finalBounds, globalOffset);
    startBounds.offset(-globalOffset.x, -globalOffset.y);
    finalBounds.offset(-globalOffset.x, -globalOffset.y);

    // Adjust the start bounds to be the same aspect ratio as the final
    // bounds using the "center crop" technique. This prevents undesirable
    // stretching during the animation. Also calculate the start scaling
    // factor (the end scaling factor is always 1.0).
    float startScale;
    if ((float) finalBounds.width() / finalBounds.height()
            > (float) startBounds.width() / startBounds.height()) {
        // Extend start bounds horizontally
        startScale = (float) startBounds.height() / finalBounds.height();
        float startWidth = startScale * finalBounds.width();
        float deltaWidth = (startWidth - startBounds.width()) / 2;
        startBounds.left -= deltaWidth;
        startBounds.right += deltaWidth;
    } else {
        // Extend start bounds vertically
        startScale = (float) startBounds.width() / finalBounds.width();
        float startHeight = startScale * finalBounds.height();
        float deltaHeight = (startHeight - startBounds.height()) / 2;
        startBounds.top -= deltaHeight;
        startBounds.bottom += deltaHeight;
    }

    // Hide the thumbnail and show the zoomed-in view. When the animation
    // begins, it will position the zoomed-in view in the place of the
    // thumbnail.
    thumbView.setAlpha(0f);
    expandedImageView.setVisibility(View.VISIBLE);

    // Set the pivot point for SCALE_X and SCALE_Y transformations
    // to the top-left corner of the zoomed-in view (the default
    // is the center of the view).
    expandedImageView.setPivotX(0f);
    expandedImageView.setPivotY(0f);

    // Construct and run the parallel animation of the four translation and
    // scale properties (X, Y, SCALE_X, and SCALE_Y).
    AnimatorSet set = new AnimatorSet();
    set
            .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
                    startBounds.left, finalBounds.left))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
                    startBounds.top, finalBounds.top))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
                    startScale, 1f))
            .with(ObjectAnimator.ofFloat(expandedImageView,
                    View.SCALE_Y, startScale, 1f));
    set.setDuration(shortAnimationDuration);
    set.setInterpolator(new DecelerateInterpolator());
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            currentAnimator = null;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            currentAnimator = null;
        }
    });
    set.start();
    currentAnimator = set;

    // Upon clicking the zoomed-in image, it should zoom back down
    // to the original bounds and show the thumbnail instead of
    // the expanded image.
    final float startScaleFinal = startScale;
    expandedImageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (currentAnimator != null) {
                currentAnimator.cancel();
            }

            // Animate the four positioning/sizing properties in parallel,
            // back to their original values.
            AnimatorSet set = new AnimatorSet();
            set.play(ObjectAnimator
                        .ofFloat(expandedImageView, View.X, startBounds.left))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.Y,startBounds.top))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.SCALE_X, startScaleFinal))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView,
                                        View.SCALE_Y, startScaleFinal));
            set.setDuration(shortAnimationDuration);
            set.setInterpolator(new DecelerateInterpolator());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    currentAnimator = null;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    currentAnimator = null;
                }
            });
            set.start();
            currentAnimator = set;
        }
    });
}

让动画有弹簧一样的物理性质

基于物理的运动是由力驱动的。弹力是一种引导互动和运动的力。弹簧力具有以下特性:阻尼和刚度。在基于弹簧的动画中,值和速度是根据应用于每个帧的弹力计算的。

如果你想让你的应用程序动画只在一个方向上减速,可以考虑使用基于摩擦的动画(fling animation)来代替。

弹簧动画的生命周期

在基于弹簧的动画中,Springforce 类允许您自定义弹簧的刚度、阻尼比和最终位置。动画一开始,弹力就更新动画值和每帧上的速度。动画将继续,直到弹力达到平衡。

例如,如果你在屏幕上拖动一个应用图标,然后用手指从图标上抬起释放它,这个图标会被一种不可见但熟悉的力量拉回到原来的位置。

下图显示了类似的弹簧效应。圆圈中间的加号(+)表示通过触摸手势施加的力。

创建弹簧动画

为应用程序构建弹簧动画的一般步骤如下:

  • 添加支持库,必须将支持库添加到项目中才能使用Spring动画类。
  • 创建弹簧动画,主要步骤是创建 SpringAnimation 类的实例并设置运动行为参数。
  • (可选)注册监听器,注册监听器以监视动画生命周期更改和动画值更新。

注意:只有在动画值更改需要每帧更新时,才应注册更新侦听器。更新侦听器防止动画可能在单独的线程上运行。

以下各节将详细讨论构建弹簧动画的一般步骤。

添加支持库

要使用基于物理的支持库,必须将支持库添加到项目中,如下所示:

  1. 打开应用模块的 build.gradle 文件。
  2. 将支持库添加到依赖项部分。

    dependencies {
     def dynamicanimation_version = "1.0.0"
     implementation 'androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version'
    }

若要查看此库的当前版本,请参阅 versions页上的有关信息。

创建弹簧动画

SpringAnimation 类用于为对象创建弹簧动画。要构建弹簧动画,需要创建一个 SpringAnimation 类的实例并提供一个对象、一个要设置动画的对象的属性以及一个您希望动画停留的可选最终弹簧位置。

注意:在创建弹簧动画时,弹簧的最终位置是可选的。但是,必须在启动动画之前定义它。

final View img = findViewById(R.id.imageView);
// Setting up a spring animation to animate the view’s translationY property with the final
// spring position at 0.
final SpringAnimation springAnim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0);

基于弹簧的动画可以通过更改 View 对象中的实际属性来为屏幕上的视图设置动画。系统中提供以下 View 的属性:

  • alpha:表示 View 上的透明度。默认情况下,值为 1(不透明),0 表示完全透明(不可见)。
  • TRANSLATION_XTRANSLATION_YTRANSLATION_Z:这些属性控制视图所在的位置,作为其左坐标,顶部坐标和高度的增量,由其布局容器设置。

    • TRANSLATION_X 描述了左坐标。
    • TRANSLATION_Y 描述了顶部坐标。
    • TRANSLATION_Z 描述 View 的高度。
  • ROTATIONROTATION_XROTATION_Y:这些属性控制 2D(旋转属性)中的旋转和围绕轴点的 3D 旋转。
  • SCROLL_XSCROLL_Y:这些属性指示源左侧和上边缘的滚动偏移量(以像素为单位)。 它还表示页面滚动的位置。
  • SCALE_XSCALE_Y:这些属性控制围绕 View 轴心点的的 2D 缩放。
  • XYZ:这些是描述视图在其容器中的最终位置的基本实用程序属性。

    • X 是左值和 TRANSLATION_X 的总和。
    • Y 是 top 值和 TRANSLATION_Y 的总和。
    • Z 是高度值和 TRANSLATION_Z 的总和。

注册监听器

DynamicAnimation 类提供两个监听器:OnAnimationUpdateListenerOnAnimationEndListener。 这些监听器会监听动画中的更新,例如动画值发生变化以及动画结束时。

OnAnimationUpdateListener

如果要为多个 View 设置动画以创建链式动画,可以设置 OnAnimationUpdateListener 以在每次当前 View 的属性发生更改时接收回调。 回调通知另一个 View 根据当前 View 属性中发生的更改来更新其弹簧位置。 要注册监听器,请执行以下步骤:

  1. 调用 addUpdateListener() 方法并将监听器附加到动画。

注意:您需要在动画开始之前注册更新监听器。 但是,只有在动画值更改需要每帧更新时才应注册更新监听器。 更新监听器可防止动画可能在单独的线程上运行。

  1. 重写 onAnimationUpdate() 方法以通知调用者当前对象的更改。 以下示例代码说明了OnAnimationUpdateListener 的总体用法。

    // Creating two views to demonstrate the registration of the update listener.
    final View view1 = findViewById(R.id.view1);
    final View view2 = findViewById(R.id.view2);
    
    // Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
    final SpringAnimation anim1X = new SpringAnimation(view1,
            DynamicAnimation.TRANSLATION_X);
    final SpringAnimation anim1Y = new SpringAnimation(view1,
        DynamicAnimation.TRANSLATION_Y);
    final SpringAnimation anim2X = new SpringAnimation(view2,
            DynamicAnimation.TRANSLATION_X);
    final SpringAnimation anim2Y = new SpringAnimation(view2,
            DynamicAnimation.TRANSLATION_Y);
    
    // Registering the update listener
    anim1X.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
    
    // Overriding the method to notify view2 about the change in the view1’s property.
        @Override
        public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                      float velocity) {
            anim2X.animateToFinalPosition(value);
        }
    });
    
    anim1Y.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
    
      @Override
        public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                      float velocity) {
            anim2Y.animateToFinalPosition(value);
        }
    });

OnAnimationEndListener

OnAnimationEndListener 通知动画结束。 您可以设置监听器,以便在动画达到平衡或取消时接收回调。 要注册监听器,请执行以下步骤:

  1. 调用 addEndListener() 方法并将监听器附加到动画。
  2. 覆盖 onAnimationEnd() 方法,以便在动画达到平衡或取消时接收通知。

移除监听器

要停止接收动画更新回调和动画结束回调,请分别调用 removeUpdateListener()removeEndListener() 方法。

设置动画取值范围

如果要将属性值限制在特定范围内,可以设置最小和最大动画值。 如果为具有固有范围的属性它还有助于控制范围(例如 alpha 是从 0 到 1)。

  • 要设置最小值,请调用 setMinValue() 方法并传递属性的最小值。
  • 要设置最大值,请调用 setMaxValue() 方法并传递属性的最大值。

两种方法都返回要为其设置值的动画。

注意:如果已设置起始值并已定义动画值范围,请确保起始值在最小值和最大值范围内。

设置开始速度

“开始速度”定义动画属性在动画开始时更改的速度。 默认开始速度设置为每秒 0 像素。 您可以使用触摸手势的速度或使用固定值作为起始速度来设置速度。 如果您选择提供固定值,我们建议您以 dp/秒 为单位定义值,然后将其转换为像素数/秒。 以 dp/秒 定义值允许速度与密度和形状因子无关。

要设置速度,请调用 setStartVelocity() 方法并以 像素数/秒 传递速度。 该方法返回设置了速度的弹力对象。

注意:使用 GestureDetector.OnGestureListenerVelocityTracker 类方法检索和计算触摸手势的速度。

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Compute velocity in the unit pixel/second
vt.computeCurrentVelocity(1000);
float velocity = vt.getYVelocity();
anim.setStartVelocity(velocity);

dp/秒 转换为 像素值/秒

弹簧的速度必须以 像素值/秒 为单位。 如果选择提供固定值作为速度的开始,请以 dp/秒 为单位提供值,然后将其转换为 像素值/秒 。 要进行转换,请使用 TypedValue 类中的 applyDimension() 方法。 请参阅以下示例代码:

float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics());

设置弹簧属性

SpringForce 类为每个弹簧属性定义 gettersetter 方法,例如阻尼比和刚度。 要设置弹簧属性,重要的是检索弹力对象或创建可在其上设置属性的自定义弹力。 有关创建自定义弹力的更多信息,请参阅创建自定义弹簧力部分。

阻尼比

阻尼比描述了弹簧振荡的逐渐减小。 通过使用阻尼比,您可以定义振荡从一次反弹到下一次反弹的衰减速度。 有四种不同的方法可以阻尼弹簧:

  • 当阻尼比大于1时发生过阻尼。 它让对象快速返回到静止位置。
  • 当阻尼比等于1时发生临界阻尼。 它允许对象在最短的时间内返回到静止位置。
  • 当阻尼比小于1时发生欠阻尼。 它通过传递静止位置让对象超调多次,然后逐渐到达静止位置。
  • 当阻尼比等于零时发生无阻尼。 它让物体永远振荡。

要将阻尼比添加到弹簧,请执行以下步骤:

  • 调用 getspring() 方法来检索弹簧以添加阻尼比。
  • 调用 setDampingratio() 方法并传递要添加到弹簧中的阻尼比。该方法返回设置阻尼比的弹力对象。

注意:阻尼比必须是非负数。如果将阻尼比设置为零,弹簧将永远不会到达静止位置。换句话说,它永远振荡。

系统中提供以下阻尼比常数:

  • DAMPING_RATIO_HIGH_BOUNCY

  • DAMPING_RATIO_MEDIUM_BOUNCY

  • DAMPING_RATIO_LOW_BOUNCY

  • DAMPING_RATIO_NO_BOUNCY

默认的阻尼比为 DAMPING_RATIO_MEDIUM_BOUNCY

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
//Setting the damping ratio to create a low bouncing effect.
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
…

刚度

刚度度定义了弹簧常数,它测量弹簧的刚度。当弹簧不在静止位置时,刚性弹簧会对附着的物体施加更大的力。要向弹簧添加刚度,请执行以下步骤:

  1. 调用 getSpring() 方法来检索弹簧以添加刚度。
  2. 调用 setstifness() 方法并传递要添加到弹簧中的刚度值。该方法返回设置刚度的弹簧力对象。

注意:刚度必须为正数。

系统提供以下刚度常数:

  • STIFFNESS_HIGH

  • STIFFNESS_MEDIUM

  • STIFFNESS_LOW

  • STIFFNESS_VERY_LOW

默认刚度设置默认为 STIFFNESS_MEDIUM

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
//Setting the spring with a low stiffness.
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW);
…

创建自定义弹力

可以创建自定义弹力作为使用默认弹力的替代方法。自定义弹力允许您在多个弹簧动画中共享同一个弹力实例。创建弹力后,可以设置阻尼比和刚度等特性。

  1. 创建一个 Springforce 对象。

    SpringForce force = new SpringForce();
  2. 通过调用各自的方法来分配属性。还可以创建方法链。

    force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);
  3. 调用 setSpring() 方法将弹簧设置为动画。

    setSpring(force);

开始idonghua

有两种方法可以启动弹簧动画:调用 start() 或调用 animateToFinalPosition() 方法。这两个方法都需要在主线程上调用。

animateToFinalPosition() 方法执行两个任务:

  • 设置弹簧的最终位置。
  • 启动动画(如果尚未启动)。

由于该方法会更新弹簧的最终位置并在需要时启动动画,因此可以随时调用该方法来更改动画的进程。例如,在链接的弹簧动画中,一个 View 的动画依赖于另一个 View 。对于这样的动画,使用 animateToFinalPosition() 方法更方便。通过在链接的弹簧动画中使用此方法,您不必担心要更新的下一个动画当前是否正在运行。

下图说明了一个链接的弹簧动画,其中一个视图的动画依赖于另一个视图。

要使用 animateToFinalPosition() 方法,请调用 animateToFinalPosition() 方法并传递弹簧的其余位置。还可以通过调用 setFinalPosition() 方法来设置弹簧的其余位置。

start() 方法不会立即将属性值设置为 start 值。属性值在每个动画脉冲处发生更改,这发生在绘制过程之前。因此,这些更改会反映在下一帧中,就好像这些值是立即设置的一样。

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
//Starting the animation
anim.start();
…

取消动画

可以取消或跳到动画的结尾。一个理想的情况是,当用户交互要求动画立即终止时,您需要取消或跳到终止。这主要发生在当用户突然退出应用程序或视图变为不可见时。

有两种方法可以用来终止动画。cancel() 方法在动画所在的值处终止动画。方法将动画跳过到最终值,然后终止动画。

在终止动画之前,首先检查弹簧的状态很重要。如果状态为无阻尼,动画将永远无法到达静止位置。要检查弹簧的状态,请调用 canSkipToEnd() 方法。如果弹簧受到阻尼,该方法将返回 true,否则返回 false。

知道弹簧的状态后,可以使用 skipToEnd() 方法或 cancel() 方法终止动画。只能在主线程上调用 cancel() 方法。

注意:通常,skipToEnd() 方法会导致视觉跳转。

设置布局自动更新动画

Android 提供了预先加载的动画,每次您更改布局时系统都会运行该动画。您所需要做的就是在布局中设置一个属性,告诉 Android 系统动画这些布局更改,系统为您执行默认动画。

提示:如果要提供自定义布局动画,请创建一个 layoutTransition 对象,并使用 setlayoutTransition() 方法将其提供给布局。

以下是将 item 添加到 List 时默认的布局动画:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_layout_changes.mp4" type="video/mp4">

</video>

创建布局

在 Activity 的布局 XML 文件中,将要为其启用动画的布局的 android:animatelayoutchanges 属性设置为 true。例如:

<LinearLayout android:id="@+id/container"
    android:animateLayoutChanges="true"
    ...
/>

添加、更新或删除布局中的项目

您所需要做的就是添加、删除或更新布局中的项目,这些项目将自动设置动画:

private ViewGroup containerView;
...
private void addItem() {
    View newView;
    ...
    containerView.addView(newView, 0);
}

使用过渡动画(补间动画)更改布局

Android 的过渡框架允许您通过简单地提供起始布局和结束布局来在用户界面中动画化各种运动。您可以选择所需的动画类型(例如淡入/淡出视图或更改视图大小),过渡框架将计算出如何从开始布局到结束布局的动画。

过渡框架包括以下功能:

  • 动画分组:对 View 层次中的所有视图应用一个或多个动画效果。
  • 内置动画:为常见效果(如淡出或移动)使用预定义的动画。
  • 资源文件支持:从布局资源文件加载 View 层次结构和内置动画。
  • 生命周期回调:接收对动画和层次结构更改过程提供控制的回调。

注意:本页介绍如何在同一 Activity 中构建布局之间的转换。如果用户在 Activity 之间移动,那么您应该阅读 Start an activity using an animation

有关在布局更改之间设置动画的示例代码,请参见 BasicTransition

两种布局之间动画的基本过程如下:

  1. 为开始布局和结束布局创建场景对象。但是,起始布局的场景通常是根据当前布局自动确定的。
  2. 创建一个过渡对象来定义所需的动画类型。
  3. 调用 transitionmanager.go(),系统运行动画以交换布局。

下图说明了布局、场景、过渡和最终动画之间的关系。

创建一个场景

场景存储 View 层次结构的状态,包括其所有 View 及其属性值。过渡框架可以在开始和结束场景之间运行动画。

可以从布局资源文件或代码中的一组 View 创建场景。但是,转换的起始场景通常是从当前用户界面自动确定的。

场景还可以定义自己的操作,这些操作在更改场景时运行。例如,此功能对于在过渡到场景后清理 View 设置很有用。

注意:框架可以在不使用场景的情况下为单个 View 层次结构中的更改设置动画,如应用无场景转换中所述。 但是,了解场景对于使用过渡非常重要。

从布局资源创建场景

您可以直接从布局资源文件创建 Scene 实例。 当文件中的 View 层次结构主要是静态时,请使用此技术。 生成的场景表示创建 Scene 实例时 View 层次结构的状态。 如果更改 View 层次结构,则必须重新创建场景。 框架从文件中的整个 View 层次结构创建场景; 您无法从布局文件的一部分创建场景。

要从布局资源文件创建场景实例,请将布局中的场景根作为视图组实例检索,然后使用场景根和包含场景视图层次结构的布局文件的资源 ID 调用 scene.getsceneforlayout() 函数。

定义场景布局

本节其余部分的代码片段将向您展示如何使用相同的场景根元素创建两个不同的场景。这些片段还演示了可以加载多个不相关的场景对象,而不暗示它们彼此相关。

该示例包含以下布局定义:

  • 具有文本标签和子布局的 Activity 的主要布局。
  • 具有两个文本字段的第一个场景的相对布局。
  • 第二个场景的相对布局,具有相同的两个文本字段,顺序不同。

设计该示例是为了使所有动画都出现在活动主布局的子布局中。 主布局中的文本标签保持静态。

活动的主要布局定义如下:

res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

此布局定义包含场景根的文本字段和子布局。 第一个场景的布局包含在主布局文件中。 这允许应用程序将其显示为初始用户界面的一部分,并将其加载到场景中,因为框架只能将整个布局文件加载到场景中。

第一个场景的布局定义如下:

res/layout/a_scene.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view1
        android:text="Text Line 1" />
    <TextView
        android:id="@+id/text_view2
        android:text="Text Line 2" />
</RelativeLayout>

第二个场景的布局包含以不同顺序放置的相同的两个文本字段(具有相同的 ID),定义如下:

res/layout/another_scene.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view2
        android:text="Text Line 2" />
    <TextView
        android:id="@+id/text_view1
        android:text="Text Line 1" />
</RelativeLayout>

从布局生成场景

为两个相对布局创建定义后,可以为每个布局获取一个场景。 这使您可以稍后在两个 UI 配置之间进行转换。 要获取场景,需要引用场景根和布局资源 ID。

以下代码段显示如何获取对场景根的引用并从布局文件创建两个 Scene 对象:

Scene aScene;
Scene anotherScene;

// Create the scene root for the scenes in this app
sceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes
aScene = Scene.getSceneForLayout(sceneRoot, R.layout.a_scene, this);
anotherScene =
    Scene.getSceneForLayout(sceneRoot, R.layout.another_scene, this);

在应用中,现在有两个基于 View 层次结构的 Scene 对象。 两个场景都使用 res/layout/activity_main.xml中 FrameLayout 元素定义的场景根。

在代码中创建一个场景

您还可以在 ViewGroup 对象的代码中创建 Scene 实例。 在代码中直接修改 View 层次结构或动态生成 View 层次结构时,请使用这种方法。

要从代码中的 View 层次结构创建场景,请使用 Scene(sceneRoot,viewHierarchy) 构造函数。 调用此构造函数等效于在已经 inflate 布局文件时调用 Scene.getSceneForLayout() 函数。

以下代码片段演示了如何从场景根元素和代码中场景的 View 层次结构创建 Scene 实例:

Scene mScene;

// Obtain the scene root element
sceneRoot = (ViewGroup) someLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered
viewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene
mScene = new Scene(sceneRoot, mViewHierarchy);

创建场景动作

框架允许您定义系统在进入或退出场景时运行的自定义场景操作。在许多情况下,不需要定义自定义场景操作,因为框架会自动为场景之间的更改设置动画。

场景操作对于处理这些情况很有用:

  • 动画显示不在同一层次中的 View。可以使用退出和进入场景操作为开始和结束场景设置视图动画。
  • 动画转换框架无法自动动画的视图,如 ListView 对象。有关更多信息,请参阅 Limitations

要提供自定义场景操作,请将操作定义为可运行的对象,并将其传递给 scene.setexitation()scene.setEnterAction() 函数。框架在运行转换动画之前在开始场景调用 setexaction() 函数,在运行转换动画之后在结束场景调用 setentertaction() 函数。

注意:不要使用场景操作在开始和结束场景中的视图之间传递数据。有关详细信息,请参见 定义转换生命周期回调

应用转换

转换框架表示具有转换对象的场景之间的动画样式。您可以使用几个内置子类(如 autotransitionfade)来实例化转换,或者定义自己的转换。然后,通过传递结束场景和过渡到 TransitionManager.go() ,可以在场景之间运行动画。

转换生命周期类似于活动生命周期,它表示框架在动画开始和完成之间监视的转换状态。在重要的生命周期状态下,框架调用回调函数,您可以实现这些函数,以便在转换的不同阶段对用户界面进行调整。

创建过渡

在上一节中,您学习了如何创建表示不同视图层次结构状态的场景。定义了开始场景和结束场景之后,需要创建一个定义动画的过渡对象。框架允许您在资源文件中指定内置转换,并在代码中对其进行扩展,或者直接在代码中创建内置转换的实例。

ClassTagAttributesEffect
AutoTransition<autoTransition/>-默认转换。淡出、移动和调整大小以及淡入视图的顺序。
Fade<fade/>`android:fadingMode="[fade_infade_outfade_in_out]"`fade_in在视图中淡入<br/>fade_out在视图中淡出<br/>fade_in_out(默认)执行fade_out后跟fade_in。
ChangeBounds<changeBounds/>-移动视图并调整其大小。

从资源文件创建转换实例

此技术使您可以修改转换定义,而无需更改 Activity 代码。 此技术对于将复杂的转换定义与应用程序代码分开也很有用,如 指定多个转换 中所示。

要在资源文件中指定内置转换,请按照下列步骤操作:

  1. res/transition/ 目录添加到项目中。
  2. 在此目录中创建新的 XML 资源文件。
  3. 为其中一个内置转换添加 XML 节点。

例如,以下资源文件指定淡入淡出过渡:

res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

以下代码段显示了如何从资源文件中扩展 Activity 内的 Transition 实例:

Transition fadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

在代码中创建转换实例

如果修改代码中的用户界面,并且创建具有很少参数或没有参数的简单内置转换实例,则此技术对于动态创建转换对象非常有用。

要创建内置转换的实例,请调用 Transition 类的子类中的一个公共构造函数。 例如,以下代码段创建了 Fade 转换的实例:

Transition fadeTransition = new Fade();

应用转换

您通常应用转换以在不同视图层次结构之间进行更改以响应事件,例如用户操作。 例如,考虑一个搜索应用:当用户输入搜索词并单击搜索按钮时,应用会更改为表示结果布局的场景,同时应用淡出搜索按钮并淡入搜索结果的转换。

要在应用转换以响应活动中的某个事件时进行场景更改,请使用结束场景和用于动画的转场实例调用TransitionManager.go() 函数,如以下代码段所示:

TransitionManager.go(endingScene, fadeTransition);

框架在运行转换实例指定的动画时,使用结束场景中的视图层次结构更改场景根目录内的视图层次结构。 起始场景是上次过渡的结束场景。 如果没有先前的转换,则从用户界面的当前状态自动确定起始场景。

如果您未指定转换实例,则转换管理器可以应用自动转换,该转换在大多数情况下执行合理的操作。 有关更多信息,请参阅 TransitionManager 类的 API 参考。

选择特定目标视图

默认情况下,框架将过渡应用于开始和结束场景中的所有视图。 在某些情况下,您可能只想将动画应用于场景中的视图子集。 例如,框架不支持动画更改 ListView 对象,因此您不应尝试在转换期间为它们设置动画。 该框架使您可以选择要设置动画的特定视图。

转换动画的每个视图称为目标。 您只能选择属于与场景关联的视图层次结构的目标。

要从目标列表中删除一个或多个视图,请在开始转换之前调用 removeTarget() 方法。 要仅将您指定的视图添加到目标列表,请调用 addTarget() 函数。 有关更多信息,请参阅 Transition 类的 API 参考。

指定多个过渡

要从动画中获得最大的影响,您应该将其与场景之间发生的更改类型相匹配。 例如,如果要删除某些视图并在场景之间添加其他视图,则淡出/淡入动画会提示某些视图不再可用。 如果要将视图移动到屏幕上的不同点,则更好的选择是为移动设置动画,以便用户注意视图的新位置。

您不必只选择一个动画,因为过渡框架使您能够在包含一组单独的内置或自定义过渡的过渡集中组合动画效果。

要从XML中的转换集合定义转换集,请在 res/transitions/ 目录中创建资源文件,并列出 transitionSet 元素下的转换。 例如,以下代码段显示了如何指定与 AutoTransition 类具有相同行为的转换集:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

要将转换集扩展为代码中的 TransitionSet 对象,请调用活动中的 TransitionInflater.from() 函数。 TransitionSet 类从 Transition 类扩展,因此您可以将其与转换管理器一起使用,就像任何其他 Transition 实例一样。

应用没有场景的过渡

更改视图层次结构不是修改用户界面的唯一方法。您还可以通过在当前层次结构中添加,修改和删除子视图来进行更改。例如,您可以使用单个布局实现搜索交互。从显示搜索条目字段和搜索图标的布局开始。要更改用户界面以显示结果,请在用户通过调用 ViewGroup.removeView() 函数单击它时删除搜索按钮,并通过调用ViewGroup.addView() 函数添加搜索结果。

如果备选方案是具有几乎相同的两个层次结构,则可能需要使用此方法。您可以拥有一个包含您在代码中修改的视图层次结构的布局文件,而不必为用户界面中的细微差别创建和维护两个单独的布局文件。

如果以这种方式在当前视图层次结构中进行更改,则无需创建场景。相反,您可以使用延迟转换在视图层次结构的两个状态之间创建和应用转换。转换框架的此功能以当前视图层次结构状态开始,记录您对其视图所做的更改,并应用在系统重绘用户界面时动画更改的转换。

要在单个视图层次结构中创建延迟转换,请按照下列步骤操作:

  • 当发生触发转换的事件时,调用 TransitionManager.beginDelayedTransition() 函数,提供要更改的所有视图的父视图以及要使用的转换。 框架存储子视图的当前状态及其属性值。
  • 根据用例的要求更改子视图。 框架记录您对子视图及其属性所做的更改。
  • 当系统根据您的更改重绘用户界面时,框架会动态显示原始状态和新状态之间的更改。

以下示例说明如何使用延迟转换为文本视图添加动画。 第一个代码段显示了布局定义文件:

res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    ...
</RelativeLayout>

下一个代码段显示了动画添加文本视图:

private TextView labelText;
private Fade mFade;
private ViewGroup rootView;
...

// Load the layout
setContentView(R.layout.activity_main);
...

// Create a new TextView and set some View properties
labelText = new TextView(this);
labelText.setText("Label");
labelText.setId(R.id.text);

// Get the root view and create a transition
rootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(Fade.IN);

// Start recording changes to the view hierarchy
TransitionManager.beginDelayedTransition(rootView, mFade);

// Add the new TextView to the view hierarchy
rootView.addView(labelText);

// When the system redraws the screen to show this update,
// the framework will animate the addition as a fade in

定义转换生命周期回调

转换生命周期与活动生命周期类似。它表示框架在调用 TransitionManager.go() 函数和完成动画之间监视的过渡状态。在重要的生命周期状态下,框架调用 TransitionListener 接口定义的回调。

转换生命周期回调非常有用,例如,在场景更改期间将视图属性值从起始视图层次结构复制到结束视图层次结构。您不能简单地将值从其起始视图复制到结束视图层次结构中的视图,因为在转换完成之前,结束视图层次结构不会 infalte。相反,您需要将值存储在变量中,然后在框架完成转换时将其复制到结束视图层次结构中。要在转换完成时收到通知,您可以在活动中实现 TransitionListener.onTransitionEnd() 函数。

有关更多信息,请参阅 TransitionListener 类的 API 参考。

限制

本节列出了转换框架的一些已知限制:

  • 应用于 SurfaceView 的动画可能无法正确显示。 SurfaceView 实例从非 UI 线程更新,因此更新可能与其他视图的动画不同步。
  • 应用于 TextureView 时,某些特定的过渡类型可能无法产生所需的动画效果。
  • 扩展 AdapterView 的类(如 ListView)以与转换框架不兼容的方式管理其子视图。 如果您尝试基于AdapterView 为视图设置动画,则设备显示可能会挂起。
  • 如果尝试使用动画调整 TextView 的大小,文本将在对象完全调整大小之前弹出到新位置。 要避免此问题,请不要为包含文本的视图的大小调整设置动画。

创建自定义过渡动画

自定义转换允许您创建任何内置转换类都不可用的动画。例如,可以定义一个自定义转换,将文本和输入字段的前景色转换为灰色,以指示在新屏幕中禁用这些字段。这种类型的更改可以帮助用户查看您禁用的字段。

自定义转换与内置转换类型之一类似,将动画应用于开始和结束场景的子视图。但是,与内置转换类型不同,您必须提供捕获属性值并生成动画的代码。也可以为动画定义目标视图的子集。

接下来教您捕获属性值并生成动画以创建自定义转换。

集成 Transition 类

要创建自定义转换,请向项目中添加一个扩展转换类的类,并重写以下代码段中显示的函数:

public class CustomTransition extends Transition {

    @Override
    public void captureStartValues(TransitionValues values) {}

    @Override
    public void captureEndValues(TransitionValues values) {}

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues,
                                   TransitionValues endValues) {}
}

以下部分将解释如何覆盖这些函数。

捕获视图属性值

过渡动画使用属性动画中描述的属性动画系统。属性动画在指定的时间段内在起始值和结束值之间更改视图属性,因此框架需要同时具有属性的起始值和结束值才能构造动画。

但是,属性动画通常只需要视图所有属性值的一小部分。例如,颜色动画需要颜色属性值,而移动动画需要位置属性值。由于动画所需的属性值特定于转换,因此转换框架不向转换提供每个属性值。相反,框架调用回调函数,允许转换只捕获它需要的属性值,并将它们存储在框架中。

捕获开始值

要将开始视图值传递给框架,请实现 CaptureStartValues(TransitionValues) 函数。框架为开始场景中的每个视图调用此函数。函数参数是一个 TransitionValues 对象,它包含对视图的引用和一个映射实例,您可以在其中存储所需的视图值。在您的实现中,检索这些属性值,并通过将它们存储在映射中将其传递回框架。

要确保属性值的键与其他 TransitionValues 键不冲突,请使用以下命名方案:

package_name:transition_name:property_name

以下代码段显示 CaptureStartValues() 函数的实现:

public class CustomTransition extends Transition {

    // Define a key for storing a property value in
    // TransitionValues.values with the syntax
    // package_name:transition_class:property_name to avoid collisions
    private static final String PROPNAME_BACKGROUND =
            "com.example.android.customtransition:CustomTransition:background";

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        // Call the convenience method captureValues
        captureValues(transitionValues);
    }


    // For the view in transitionValues.view, get the values you
    // want and put them in transitionValues.values
    private void captureValues(TransitionValues transitionValues) {
        // Get a reference to the view
        View view = transitionValues.view;
        // Store its background property in the values map
        transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground());
    }
    ...
}

捕获结束值

框架为结束场景中的每个目标视图调用一次 CaptureEndValues(TransitionValues) 函数。在所有其他方面, captureEndValues() 的工作方式与 captureStartValues() 相同。

以下代码段显示 CaptureEndValues() 函数的实现:

@Override
public void captureEndValues(TransitionValues transitionValues) {
    captureValues(transitionValues);
}

在本例中,captureStartValues()captureEndValues() 函数都调用 captureValues() 来检索和存储值。captureValues() 检索的视图属性是相同的,但在开始和结束场景中它有不同的值。框架为视图的开始和结束状态维护单独的映射。

创建自定义 Animator

要使视图在开始场景中的状态和结束场景中的状态之间的更改具有动画效果,可以通过覆盖 createAnimator() 函数来提供动画程序。当框架调用此函数时,它将在场景根视图和包含捕获的起始值和结束值的 TransitionValues对象中传递。

框架调用 CreateAnimator() 函数的次数取决于开始和结束场景之间发生的更改。例如,考虑将淡出/淡入动画实现为自定义转换。如果开始场景有五个目标,其中两个目标从结束场景中移除,而结束场景有三个目标从开始场景加上一个新目标,则框架将调用 CreateAnimator() 六次:其中三个调用对目标的淡出和淡入进行动画处理。在停留在两个场景对象中时;另外两个调用将淡出从结束场景中移除的目标设置动画;另一个调用将淡入结束场景中的新目标设置动画。

对于同时存在于开始和结束场景中的目标视图,框架为 StartValuesEndValues 参数提供 TransitionValues 对象。对于只存在于开始或结束场景中的目标视图,框架为相应的参数提供 TransitionValues 对象,为另一个提供空值。

要在创建自定义转换时实现 CreateAnimator(ViewGroup, TransitionValues, TransitionValues)函数,请使用捕获的视图属性值创建 Animator 对象并将其返回框架。有关示例实现,请参见 CustomTransition 示例中的 ChangeColor 类。有关属性动画制作程序的详细信息,请参见 属性动画

应用自定义转换

自定义转换与内置转换的工作原理相同。可以使用转换管理器应用自定义转换,如 应用转换 中所述。

使用动画启动 Activity

material design 应用中的 Activity 转换通过运动和公共元素之间的转换提供不同状态之间的可视连接。可以为输入和输出转换以及活动之间共享元素的转换指定自定义动画。

  • 输入转换确定 Activity 中的视图如何进入场景。例如,在“分解输入”转换中,视图从外部进入场景,然后飞向屏幕中心。
  • 退出转换确定 Activity 中的视图如何退出场景。例如,在“分解出口”转换中,视图从中心退出场景。
  • 共享元素转换决定了两个 Activity 之间共享的视图如何在这些活动之间转换。例如,如果两个活动在不同的位置和大小上具有相同的图像,那么 changeImageTransform 共享元素转换将在这些活动之间平滑地转换和缩放图像。

Android 支持这些进入和退出转换:

  • explode - 将视图移入或移出场景中心。
  • slide - 将视图移入或移出场景的其中一个边缘。
  • fade - 通过更改其不透明度从场景中添加或删除视图。

任何扩展 Visibility 类的转换都支持作为进入或退出转换。 有关更多信息,请参阅 Transition 类的 API 参考。

Android 还支持这些共享元素转换:

  • changeBounds - 为目标视图布局边界中的更改设置动画。
  • changeClipBounds - 为目标视图剪辑边界中的更改设置动画。
  • changeTransform - 设置目标视图的比例和旋转变化的动画。
  • changeImageTransform - 设置目标图像大小和比例的更改动画。

当您在应用程序中启用 Activity 转换时,默认的交叉淡入淡出转换将在进入和退出活动之间激活。

有关使用共享元素在活动之间动态显示的示例代码,请参阅 ActivityScenerTransactionBasic

检查系统版本

活动转换 API 可在 Android 5.0(API 21)及更高版本上使用。要保持与早期版本的 Android 的兼容性,请在运行时检查系统版本,然后调用 API 以获取以下任何功能:

// Check if we're running on Android 5.0 or higher
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    // Apply activity transition
} else {
    // Swap without transition
}

指定自定义转换

首先,在定义从 material 主题继承的样式时,使用 android:windowActivityTransitions 属性启用窗口内容转换。您还可以在样式定义中指定输入、退出和共享元素转换:

<style name="BaseAppTheme" parent="android:Theme.Material">
  <!-- enable window content transitions -->
  <item name="android:windowActivityTransitions">true</item>

  <!-- specify enter and exit transitions -->
  <item name="android:windowEnterTransition">@transition/explode</item>
  <item name="android:windowExitTransition">@transition/explode</item>

  <!-- specify shared element transitions -->
  <item name="android:windowSharedElementEnterTransition">
    @transition/change_image_transform</item>
  <item name="android:windowSharedElementExitTransition">
    @transition/change_image_transform</item>
</style>

此示例中的 change_image_transform 转换定义如下:

<!-- res/transition/change_image_transform.xml -->
<!-- (see also Shared Transitions below) -->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
  <changeImageTransform/>
</transitionSet>

changeImageTransform 元素对应于 ChangeImageTransform 类。 有关更多信息,请参阅 Transition的 API 。

要在代码中启用窗口内容转换,请调用 window.requestFeature() 函数:

// inside your activity (if you did not enable transitions in your theme)
getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);

// set an exit transition
getWindow().setExitTransition(new Explode());

要在代码中指定转换,请使用转换对象调用这些函数:

setexitTransition()setsharedelementexitTransition() 函数定义调用 Activity 的退出转换。setEnterTransition()setSharedElementEnterTransition() 函数定义被调用 Activity 的 Enter 转换。

要获得转换的全部效果,必须在调用和被调用 Activity 上启用窗口内容转换。否则,调用 Activity 将启动退出转换,但随后将看到窗口转换(如缩放或淡入)。

要尽快启动 Enter 转换,请对调用的活动使用 window.setallowentTransitionOverlap() 函数。这可以让你有更戏剧性的进入过渡。

使用转换启动 Activity

如果启用转换并为 Activity 设置退出转换,则当启动另一个 Activity 时,转换将激活,如下所示:

startActivity(intent,
              ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

如果为第二个 Activity 设置了 Enter 转换,则在 Activity 开始时也会激活转换。要在启动另一个 Activity 时禁用转换,请提供一个空选项包。

使用共享元素启动 Activity

要在具有共享元素的两个活动之间制作屏幕转换动画,请执行以下操作:

  1. 在主题中启用窗口内容转换。
  2. 在样式中指定共享元素转换。
  3. 将转换定义为 XML 资源。
  4. 使用 android:transitionname 属性为两个布局中的共享元素分配一个公用名。
  5. 使用 activityoptions.makescenetransitionanimation() 函数。
// get the element that receives the click event
final View imgContainerView = findViewById(R.id.img_container);

// get the common element for the transition in this activity
final View androidRobotView = findViewById(R.id.image_small);

// define a click listener
imgContainerView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent(this, Activity2.class);
        // create the transition animation - the images in the layouts
        // of both activities are defined with android:transitionName="robot"
        ActivityOptions options = ActivityOptions
            .makeSceneTransitionAnimation(this, androidRobotView, "robot");
        // start the new activity
        startActivity(intent, options.toBundle());
    }
});

对于在代码中生成的共享动态视图,请使用 view.setTransitionName() 函数在两个 Activity 中指定一个公共元素名称。

要在完成第二个 Activity 时反转场景转换动画,请调用 activity.finishAfterTransition() 函数而不是activity.finish()

使用多个共享元素启动 Activity

要在具有多个共享元素的两个 Activity 之间制作场景转换动画,请使用 android:transitionname 属性(或在两个活动中使用 view.setTransitionName() 函数)在两个布局中定义共享元素,并按如下所示创建 ActivityOptions 对象:

ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this,
        Pair.create(view1, "agreedName1"),
        Pair.create(view2, "agreedName2"));

使用 viewpager 在 Fragment 之间滑动

屏幕幻灯片是一个屏幕到另一个屏幕之间的过渡,在诸如安装向导或幻灯片等 UI 中很常见。本课向您展示如何使用支持库提供的 viewpager 来制作屏幕幻灯片。viewpager 对象可以自动设置屏幕幻灯片的动画。以下是屏幕幻灯片从一个内容屏幕切换到下一个内容屏幕的效果:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_screenslide.mp4" type="video/mp4">

</video>

如果你想跳到前面看到一个完整的工作示例,可以在 GitHub 上查看 这个示例应用程序 。viewpager 是 androidx 的一部分。有关更多信息,请参阅使用 Androidx

注意:您也可以使用当前处于 alpha 阶段的新 viewpager2 库来尝试屏幕幻灯片。有关信息,请参见 使用 viewpager2 在片段之间滑动

创建视图

创建一个布局文件,稍后将用于 Fragment 的内容。您还需要为 Fragment 的内容定义一个字符串。以下示例包含用于显示某些文本的文本视图:

<!-- fragment_screen_slide_page.xml -->
<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:padding="16dp"
        android:lineSpacingMultiplier="1.2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/lorem_ipsum" />
</ScrollView>

创建 Fragment

创建一个 Fragment 类,该类返回刚刚在 onCreateView() 方法中创建的布局。然后,只要需要向用户显示一个新页面,就可以在宿主 Activity 中创建此 Fragment 的实例:

import android.support.v4.app.Fragment;
...
public class ScreenSlidePageFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.fragment_screen_slide_page, container, false);

        return rootView;
    }
}

添加 ViewPager

viewpager 对象有内置的滑动手势来转换页面,默认情况下,它们显示屏幕幻灯片动画,因此您不需要创建自己的动画。viewpager 使用 pageradapter 对象作为新页面的显示源,因此 pageradapter将使用您先前创建的 fragment 类。

首先,创建包含 viewpager 的布局:

<!-- activity_screen_slide.xml -->
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

创建执行以下操作的 Activity:

  • 使用 viewpager 将内容视图设置为布局。
  • 创建一个扩展 FragmentStatePageRadapter 抽象类并实现 getItem() 方法的类,以将 ScreenSidepageFragment 的实例作为新页提供。pager adapter 还要求实现getcount() 方法,该方法返回适配器将创建的页数(示例中为5页)。
  • 将 PagerAdapter 连接到 ViewPager。
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
...
public class ScreenSlidePagerActivity extends FragmentActivity {
    /**
     * The number of pages (wizard steps) to show in this demo.
     */
    private static final int NUM_PAGES = 5;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next wizard steps.
     */
    private ViewPager mPager;

    /**
     * The pager adapter, which provides the pages to the view pager widget.
     */
    private PagerAdapter pagerAdapter;

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

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = (ViewPager) findViewById(R.id.pager);
        pagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
        mPager.setAdapter(pagerAdapter);
    }

    @Override
    public void onBackPressed() {
        if (mPager.getCurrentItem() == 0) {
            // If the user is currently looking at the first step, allow the system to handle the
            // Back button. This calls finish() on this activity and pops the back stack.
            super.onBackPressed();
        } else {
            // Otherwise, select the previous step.
            mPager.setCurrentItem(mPager.getCurrentItem() - 1);
        }
    }

    /**
     * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
     * sequence.
     */
    private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
        public ScreenSlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return new ScreenSlidePageFragment();
        }

        @Override
        public int getCount() {
            return NUM_PAGES;
        }
    }
}

使用 PageTransformer 自定义动画

要从默认屏幕幻灯片动画显示不同的动画,请实现 ViewPager.PageTransformer 接口并将其提供给 view pager。 该接口公开了一个方法 transformPage()。 在屏幕转换的每个点上,对于每个可见页面(通常只有一个可见页面)调用此方法一次,对于屏幕外的相邻页面调用此方法。 例如,如果第 3 页可见并且用户拖向第 4 页,则在手势的每个步骤调用 transformPage() 以获取第 2,3 和 4 页。

transformPage() 的实现中,您可以通过根据屏幕上页面的位置确定需要转换哪些页面来创建自定义幻灯片动画,该位置是从 transformPage() 方法的 position 参数获得的。

position 参数指示给定页面相对于屏幕中心的位置。 它是一个动态属性,随着用户滚动页面而变化。 当页面填满屏幕时,其位置值为 0,当页面刚刚从屏幕右侧绘制时,其位置值为 1。如果用户在第 1 页和第 2 页之间滚动,则第 1 页的位置为 -0.5 和第二页的位置为 0.5。 根据屏幕上页面的位置,您可以通过使用 setAlpha()setTranslationX()setScaleY() 等方法设置页面属性来创建自定义幻灯片动画。

当您拥有 PageTransformer 的实现时,请使用您的实现调用 setPageTransformer() 来应用您的自定义动画。 例如,如果您有一个名为 ZoomOutPageTransformerPageTransformer,则可以设置自定义动画,如下所示:

ViewPager mPager = (ViewPager) findViewById(R.id.pager);
...
mPager.setPageTransformer(true, new ZoomOutPageTransformer());

有关 PageTransformer 的示例和视频,请参阅 缩小页面变换器深度页面变换器 部分。

缩小页面转换器

当在相邻页面之间滚动时,此页面转换器将缩小和淡出页面。随着页面越来越靠近中心,它会变回正常大小并淡入。

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_page_transformer_zoomout.mp4" type="video/mp4">

</video>

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 1) { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA +
                    (scaleFactor - MIN_SCALE) /
                    (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}

深度页面转换器

此页面转换器使用默认的幻灯片动画向左滑动页面,而使用“深度”动画向右滑动页面。这个深度动画会淡出页面,并线性缩放。

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_page_transformer_depth.mp4" type="video/mp4">

</video>

在深度动画期间,默认动画(屏幕幻灯片)仍会发生,因此必须用负 x 平移来抵消屏幕幻灯片。例如:

view.setTranslationX(-1 * view.getWidth() * position);

下面的示例演示如何取消工作页转换器中默认的屏幕幻灯片动画:

public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0f);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1f);
            view.setTranslationX(0f);
            view.setScaleX(1f);
            view.setScaleY(1f);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0f);
        }
    }
}

使用 viewpager2 在 Fragment 之间滑动

屏幕幻灯片是一个屏幕到另一个屏幕之间的过渡,在诸如安装向导或幻灯片等 UI 中很常见。本主题向您展示如何使用 ViewPager2 对象进行屏幕幻灯片。ViewPager2 对象可以自动设置屏幕幻灯片的动画。以下是从一个内容屏幕切换到下一个内容屏幕的屏幕幻灯片示例:

<video id="video" controls="" preload="none">

<source id="mp4" src="https://developer.android.google.cn/training/animation/anim_screenslide.mp4" type="video/mp4">

</video>

如果你想看完整的工作示例,可以在 GitHub 上查看这个示例应用程序。要使用 viewpager2,需要向项目中添加一些 androidx 依赖项

注意:viewpager2 目前处于测试阶段。有关为屏幕幻灯片使用稳定的 viewpager 库的信息,请参阅使用viewpager 在片段之间滑动。

创建视图

创建一个布局文件,稍后将用于 Fragment 的内容。您还需要为 Fragment 的内容定义一个字符串。以下示例包含显示某些文本的文本视图:

<!-- fragment_screen_slide_page.xml -->
<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:padding="16dp"
        android:lineSpacingMultiplier="1.2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/lorem_ipsum" />
</ScrollView>

创建 Fragment

创建一个 Fragment 类,该类返回刚刚在 onCreateView() 方法中创建的布局。然后,只要需要向用户显示一个新页面,就可以在宿主 Activity 中创建此 Fragment 的实例:

import androidx.fragment.app.Fragment;
...
public class ScreenSlidePageFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.fragment_screen_slide_page, container, false);

        return rootView;
    }
}

添加 ViewPager2

viewpager2 对象有内置的滑动手势来转换页面,默认情况下,它们显示屏幕幻灯片动画,因此您不需要创建自己的动画。ViewPager2 使用 FragmentStateAdapter 对象作为新页面的显示源,因此 FragmentStateAdapter 将使用先前创建的 Fragment 类。

开始之前,请创建包含 ViewPager2 对象的布局:

<!-- activity_screen_slide.xml -->
<androidx.viewpager2.widget.ViewPager2
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

创建执行以下操作的 Activity:

  • 将内容视图设置为带有ViewPager2的布局。
  • 创建一个扩展 FragmentStateAdapter 抽象类并实现 getItem() 方法的类,以将 ScreenSidepageFragment 的实例作为新页提供。pager adapter 还要求实现 getItemCount() 方法,该方法返回适配器将创建的页数(示例中为5页)。
  • 将 FragmentStateAdapter 连接到 ViewPager2 对象。
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
...
public class ScreenSlidePagerActivity extends FragmentActivity {
    /**
     * The number of pages (wizard steps) to show in this demo.
     */
    private static final int NUM_PAGES = 5;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next wizard steps.
     */
    private ViewPager2 mPager;

    /**
     * The pager adapter, which provides the pages to the view pager widget.
     */
    private FragmentStateAdapter pagerAdapter;

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

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = findViewById(R.id.pager);
        pagerAdapter = new ScreenSlidePagerAdapter(this);
        mPager.setAdapter(pagerAdapter);
    }

    @Override
    public void onBackPressed() {
        if (mPager.getCurrentItem() == 0) {
            // If the user is currently looking at the first step, allow the system to handle the
            // Back button. This calls finish() on this activity and pops the back stack.
            super.onBackPressed();
        } else {
            // Otherwise, select the previous step.
            mPager.setCurrentItem(mPager.getCurrentItem() - 1);
        }
    }

    /**
     * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
     * sequence.
     */
    private class ScreenSlidePagerAdapter extends FragmentStateAdapter {
        public ScreenSlidePagerAdapter(FragmentActivity fa) {
            super(fa);
        }

        @Override
        public Fragment getItem(int position) {
            return new ScreenSlidePageFragment();
        }

        @Override
        public int getItemCount() {
            return NUM_PAGES;
        }
    }
}

剩下 缩小页面变换器 和 深度页面变换器 跟之前大差不差,就不写了。