“喜欢那一瞬的心动,如蓝天下的一朵白云,清潭里的一抹游鱼,她发梢的晚风徐徐。”
前言
其实动画相关的东西在之前的 Fragment 的内容里也有涉及到
之前对动画一知半解的,处于只是听过,偶尔用过的状态
于是找了个时间把动画的东西总结了一下发出来
好家伙,写之前我是真没想到有这么多。。。
主要是代码很多,搞得篇幅很长。
如果你真的看完,那你是真的看得起我…
安卓动画
安卓动画可以分为逐帧动画、补间动画和后来推出的属性动画
不过动画实现的实质都是逐帧的,就像 flash 一样
接下来就一个个来看看各个动画的实现方式和用法
前期准备
在实现动画之前我们做一下准备工作。
搞一个新的页面,然后一个图片一个按钮,点击按钮后启动图片的动画效果。
新建一个类作为我们展示动画的页面,我这里新建了 AnimActivity,其对应的布局为 activity_anim.xml,布局放了个 ImageView 作为展示动画的载体,以及一个 Button 用来启动动画。布局代码如下,注意ImageView
还没有设置 src 图片
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AnimActivity">
<ImageView
android:id="@+id/anim_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="播放动画"
android:layout_alignParentBottom="true"
android:onClick="btnClick"/>
</RelativeLayout>
然后是 AnimActivity 中的代码,绑定好 ImageView 后实现按钮的点击事件
public class AnimActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anim);
imageView = findViewById(R.id.anim_image_view);
}
public void btnClick(View view) {
}
}
逐帧动画(FrameAnimation)
逐帧动画可以说是最原始的动画效果,我们提供每一帧的画面,最后让所有帧都连起来,就形成了一个动画
像我们游戏中的 FPS 指的就是每秒的帧数(Frame Per Second);注意和 FPS 游戏区分开,它指的是 First-Person Shooting…
扯远了,逐帧动画需要我们准备一组图像,然后按顺序播放,就像快速翻动本子形成的动画。虽然有些繁琐,但是中间步骤没有涉及计算算法,在某种意义上来说也很简单。
可以用 XML 文件或直接用代码实现,对于逐帧动画来说,代码实现比较多。XML 文件的实现步骤如下:
- 导入每一帧的图片
- 在 drawable 目录下建立 animation.xml 动画资源文件
- 在 animation.xml 描述每一帧
- 代码中用``加载动画文件
- 代码中用``启动动画
XML 文件实现
导入的图片大家随便找吧,导入完后我们需要一个资源文件来说明每一帧对应的是哪个图片
新建drawable -> frame_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/img_1" android:duration="80"/>
<item android:drawable="@drawable/img_2" android:duration="80"/>
<item android:drawable="@drawable/img_3" android:duration="80"/>
<item android:drawable="@drawable/img_4" android:duration="80"/>
<item android:drawable="@drawable/img_5" android:duration="80"/>
</animation-list>
这里用到animation-list
,其中每个item
都代表一帧。android:oneshot
表示图片是否只播放一次,我们选择false
表示播放完一次后重新播放,即循环播放。最后接个图片的持续时间duration
(ms)
然后来到 Activity 里,修改点击事件,载入动画并启动。
public void btnClick(View view) {
imageView.setBackgroundResource(R.drawable.frame_anim);
AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getBackground();
animationDrawable.start();
}
我们用 setBackgroundResource
载入动画资源,因为逐帧动画靠的是改变 ImageView 的背景,所以这里的 ImageView 不能设置 src,否则 src 的图片会覆盖背景,导致资源文件里的图片无法显示,会一直傻呆呆的显示 src 的图片。(除非你简单粗暴地在 XML 里设置了 ImageView 的 background 属性)
然后用 AnimationDrawable
对象获取到背景,他是Drawable
的后代类,表示这类可以逐帧播放的图片。因为背景载入的是 frame_anim.xml
,里面有很多图片,所以相当于拿到每一帧的图片,最后用 start()
方法开始将每一帧的图片载入到背景,即实现播放效果。
这里注意一点,讲道理 setBackgroundResource
载入动画资源的步骤应该放到初始化里,比如在findViewById
后面,否则 ImageView 会一直是空白的(因为没有 src 和背景)。像我这样得点击按钮他才会载入图片并开始播放逐帧动画,在我点按钮前都是空白。
主要是我想偷懒少贴点代码。放在初始化里的话,载入的是第一张图片,即@drawable/img_1
,然后等点击按钮才会开始动。
代码实现
代码实现就不需要我们建 XML 文件了,但是相对的,需要更多的代码量来描述 XML 文件的内容
直接修改按钮的点击事件
public void btnClick(View view) {
AnimationDrawable animationDrawable = new AnimationDrawable();
imageView.setBackground(animationDrawable);
//利用反射获得帧图片的文件
String packageName = this.getApplicationContext().getPackageName();
for (int i = 1; i <= 5; i++) {
int imgId = this.getResources().getIdentifier("img_" + i, "drawable", packageName); //构建Id
Drawable frame = this.getResources().getDrawable(imgId); //根据Id找到Drawable图片文件
animationDrawable.addFrame(frame, 80);
}
animationDrawable.setOneShot(false); //循环播放
animationDrawable.start(); //播放动画
}
有许多新东西我们一一来看看
首先,同样我们需要一个 AnimationDrawable
对象来存储所有的帧图片,然后用 setBackground
来设置 ImageView 的背景(这个方法的前身是setBackgroundDrawable
,不过他被弃用了)
然后利用反射,把所有的帧图片加到 AnimationDrawable
里,不然的话 5 张图片要 5 行,10 张图片就要写 10 行了,还不利于封装=。=我们这里先拿到了包名,然后用getIdentifier()
构建每个图片 Id,参数分别是图片文件名
、所在资源文件夹
、包名
,最后得到的东西就和 R.drawable.img_1
一样,是图片的 Id,毕竟 getResources()
方法就是获取 Id 的。
接着根据 Id 找到 Drawable 图片文件,把图片一个个加入AnimationDrawable
,addFrame()
的参数分别是Drawable图片
和图片持续时间(ms)
最后设置是否循环播放,再用start()
启动动画即可。
代码实现很突出的一个优点就是可以对其进行封装
我们可以提出上下文(context),前缀(prefix),起始/结束后缀(start/end),持续时间(duration),是否循环播放(isOneShot)等变量,将上述播放动画的代码封装成一个方法。
public void startFrameAnim(Context context, String prefix, int start, int end, int duration, boolean isOneShot) {
AnimationDrawable animationDrawable = new AnimationDrawable();
imageView.setBackground(animationDrawable);
//利用反射获得帧图片的文件
String packageName = context.getApplicationContext().getPackageName();
for (int i = start; i <= end; i++) {
int imgId = context.getResources().getIdentifier(prefix + i, "drawable", packageName); //构建Id
Drawable frame = context.getResources().getDrawable(imgId); //根据Id找到Drawable图片文件
animationDrawable.addFrame(frame, duration);
}
animationDrawable.setOneShot(isOneShot); //循环播放
animationDrawable.start(); //播放动画
}
这样,在点击按钮中,我们只用调用这个方法,传入对应的参数就行了
public void btnClick(View view) {
startFrameAnim(this, "img_", 1, 5, 80, false);
}
也就是说,只要我规定了帧图片的命名规则,那么我就可以在其他地方(其他 Activity)调用这个动画的加载方法。甚至还可以专门写一个进行动画播放的工具类供 Activity 使用,实现降低耦合,优化代码框架。
补间动画(TweenAnimation)
认识 Flash 的宝们可能会比较熟徐补间动画,补间动画就是由我们提供关键帧(Key Frame),然后电脑根据两个关键帧的图像位置,进行差值运算,自动生成关键帧之间的帧。所以叫补间动画,自动补充中间的部分嘛。像我以前玩 MMD,用的都是补间动画来实现骨骼的移动。
补间动画的本质也是逐帧动画,区别在于其中间的帧是自动生成的,而非我们提供的。
实现比较简单,因为我们只用规定其初始和结束两个状态就行了,用 XML 文件实现动画的大致步骤如下。
注意这里要给 ImageView 设置src 属性了,不然都没个图片,动画效果都看不到=。=
- 资源目录下建立文件夹,通常为 res -> anim
- 建立动画资源文件,如 animation.xml
- 在文件中描述动画属性
- 调用
AnimationUtils.loadAnimation
载入动画文件 - 使用 View 的
startAnimation
启动动画
当然还可以用代码实现,优缺点也很明显,XML 文件可以复用很方便,而直接用代码实现易读性比较高,方便维护。
渐变动画(AlphaAnimation)
渐变动画改变的是控件的透明度(alpha),偶尔我们会使用 ARGB 的颜色系统,第一个 A 就代表 alpha 透明度。比如 #00000000
,前两个 0 就表示透明度为 100%,这是个透明的颜色。
XML 文件实现
先说使用 XML 文件来进行动画的实现,新建动画资源文件 res -> anim -> alpha_anim.xml,代码如下
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="0.1"
android:toAlpha="1"
android:duration="2000"
android:repeatCount="2"/>
</set>
其中,fromAlpha
和toalpha
分别表示起始透明度和结束透明度,duration
表示持续时间(ms),repeatCount
表示重复次数(第一次不算,所以一共会进行 3 次动画)
之所以是补间动画,因为我们只规定了起始和结束透明度,而在这 2 秒的时间内,系统会自动帮我们补充动画的过渡效果。
最后修改按钮的点击事件
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.alpha_anim);
imageView.startAnimation(animation);
}
一开始先用clearAnimation()
清除一下 ImageView 正在进行的动画。如果用户点击太快,我们的动画还没执行完,会导致动画的一个叠加,往往效果会在我们的意料之外,所以每次点击时最好都清除一下动画效果。
我们利用 AnimationUtils.loadAnimation
载入我们的动画,参数分别是上下文 Context和我们的动画资源文件,然后产生一个 Animation
对象
最后我们可以直接让 ImageView 启用这个Animation
对象来实现动画。
代码实现
代码实现其实也很简单,就是不需要创建 XML 动画资源文件了,我们可以直接修改按钮点击事件
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = new AlphaAnimation(0.1f, 1.0f);
animation.setDuration(2000);
animation.setRepeatCount(2);
imageView.startAnimation(animation);
}
同样是启用 Animation
对象,不过这次我们不再调用工具类载入动画资源,而是实例化一个新的 AlphaAnimation
对象,参数分别是起始透明度和结束透明度,注意要是 float 类型
然后再设置一下动画的持续时间和重复次数,效果和 XML 里的属性一样一样的。
这里用到的 AlphaAnimation
是 Animation
的一个子类,包括之后用到的ScaleAnimation
等也都是,分别对应不同的动画类型。
缩放动画(ScaleAnimation)
缩放动画针对的是控件的宽高和原来的一个倍数变化,比如变为原来的 2 倍或 0.5 倍之类的
XML 文件实现
新建 res -> anim -> scale_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:fromXScale="0"
android:fromYScale="0"
android:toXScale="2.0"
android:toYScale="2.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="2000"/>
</set>
fromX/YScale
是初始状态的宽高大小,这里设置为 0 表示一开始的宽高是 0,就是从一个点开始放大。
toX/YScale
是结束状态的宽高大小,这里 2.0 表示结束的时候宽高为原图片的两倍。
pivotX/Y
表示伸缩的参考点,这里是 50%,表示宽和高分别以中间位置为参考点,向两边/上下放大,即初始状态、原图片、最终状态的宽高中间位置是对齐的。如果pivotX
设置为 0,则以最左边为参考点,向右边拉伸放大,即初始状态、原图片、最终状态的宽最左位置是对其的。
这里设置了持续时间是 2 秒,没有设置重复次数所以只会执行一次动画。
代码实现
同样,不需要 XML,我们只用修改点击事件里的代码
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = new ScaleAnimation(0.0f, 2.0f, 0.0f, 2.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(2000);
animation.setRepeatCount(2);
imageView.startAnimation(animation);
}
这里用的子类是 ScaleAnimation
,他的前四个参数分别表示 fromXScale
,toXScale
,fromYScale
,toYScale
,都是 float 类型
之后的两个参数表示android:pivotX="50%"
,其中 Animation.RELATIVE_TO_SELF
表示以自身为参照,后面的 0.5f
就表示 50%,合起来就是以自身宽度的 50%为放缩参照点。这里稍微麻烦了点,因为前面这个参数还可以以 Parent 为参照…
同理,最后的两个参数就表示android:pivotY="50%"
平移动画(TranslateAnimation)
平移动画实际上就是设定 X 轴/Y 轴方向上的移动情况
XML 文件实现
新建 res -> anim -> trans_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="0"
android:duration="2000"/>
</set>
平移动画里,X/Y 的起始位置和结束位置的参数比较讲究,对于一个控件来说,X/Y 坐标原点就是其左下角。
上面 X 轴起始位置使用的是 100%
,这个百分比是相对于控件本身来说的,实际上转化为数值就是这个控件自身的宽度。也就是动画开始后,图片的起始位置实际上是原本图片的最右侧位置,即动画开始后,图片从右侧开始往左移动,这个距离为自身宽度(100%
)
同理,如果是50%
或者200%
,就表示右侧距离一半或者两倍的自身宽度。
这里还可以使用具体的像素数值,比如1080
,表示从右侧 1080 像素位置开始移动,可以实现从屏幕外进入屏幕的效果。
由于原点位于原本图片的左下角,所以正数的参数代表右侧,我们也可以输入负数参数表示左侧,比如 -1080
就表示从左侧距离1080
像素的位置开始移动
然后在点击事件中修改传入的资源文件
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.trans_anim);
imageView.startAnimation(animation);
}
代码实现
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = new TranslateAnimation(1080, 0,0,0);
animation.setDuration(2000);
animation.setRepeatCount(2);
imageView.startAnimation(animation);
}
子类TranslateAnimation
的构造方法中,四个参数分别代表 fromXDelta
,toXDelta
,fromYDelta
,toYDelta
旋转动画(RotateAnimation)
旋转动画就是旋转,我们可以给定初始和结束位置的角度、旋转的定点,就可以实现旋转效果
像某些音乐 app 里,播放歌曲的页面往往会有唱片旋转的效果,就是这样实现的
XML 文件实现
新建 res -> anim -> rotate_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<rotate
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="2000"/>
</set>
这几个属性都比较好理解,fromDegrees
和toDegrees
分别代表起始和结束位置的角度,一般是 0~360,表示转一圈。
pivotX
和 pivotY
就是旋转的定点,都是 50%,就是绕着中心点转。
修改按钮点击事件,换个资源文件
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.rotate_anim);
imageView.startAnimation(animation);
}
代码实现
public void btnClick(View view) {
imageView.clearAnimation(); //清除动画
Animation animation = new RotateAnimation(0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(2000);
animation.setRepeatCount(2);
imageView.startAnimation(animation);
}
同理,用的是RotateAnimation
,参数分别表示fromDegrees
,toDegrees
,pivotX
,pivotY
设置 Intent 切换 Activity 的动画
接下来我们来实际替换一下 Activity 切换的时候的动画。这里补充一句,我用虚拟机的时候会出问题,动画效果没有显示出来,用真机调试就没有问题,所以如果动画没有显示的话可以换真机试试。
XML 动画资源文件
对于动画资源文件,我们需要定义两个,分别代表新 Activity 进入的动画和旧 Activity 退出的动画
这里就简单做一个移动过渡的动画
先设置进入动画效果,新建 res -> anim -> trans_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator">
<translate
android:fromXDelta="100%"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="0"
android:duration="2000"/>
</set>
因为这里动画的作用对象是 Activity,所以用100%
正好是整个屏幕的宽度
效果就是新 Activity 从右往左进入。为了更明显体现动画效果这里时间设置长一点,为 2 秒。
接下来是退出动画,新建 res -> anim -> trans_out.xml
注意退出动画的对象是旧的 Activity,我们要让他往左移动直到整体移出屏幕,所以终点 X 坐标设置为-100%
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/accelerate_interpolator">
<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="-100%"
android:toYDelta="0"
android:duration="2000"/>
</set>
上面都用到了 android:interpolator
这个属性,即插值器,用来控制动画的执行速率。买一送一,在下面会介绍一下这玩意。
调用动画
说是调用,实际上是一个覆盖,毕竟系统本来就有默认的动画效果。要使用我们自己的动画效果也十分简单,在startActivity
用一句overridePendingTransition
就可以实现了。
我们还是修改按钮的点击事件,反正用Intent
搞个跳转 Activity,能看到动画就行了
public void btnClick(View view) {
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.trans_in, R.anim.trans_out);
}
注意overridePendingTransition
一定要在startActivity
之后调用,不然是没有效果的。
插值器 Interpolator
插值器本质上是一个数学函数,用于控制动画的执行速率。比如控制动画匀速播放、加速/减速播放、先加速后减速播放等,都是由插值器实现的。
在上面的动画资源文件中,我们用到了android:interpolator
属性,它就是对插值器的设置。
实际上每个系统提供的插值器都有一个类来表示,所以像之前我们不写 XML 文件而纯用代码实现的话,设置插值器就可以用如下方式:(以越来越快为例)
animation.setInterpolator(new AccelerateInterpolator());
系统提供的插值器有以下几种,都可以分别用 XML 属性或用代码设置。如果不设置,默认使用AccelerateDecelerateInterpolator
(先快后慢)的插值器
效果 | 代码 | XML 属性 |
---|---|---|
先快后慢(默认) | AccelerateDecelerateInterpolator() | @android:anim/accelerate_decelerate_interpolator |
越来越快 | AccelerateInterpolator() | @android:anim/decelerate_interpolator |
越来越慢 | DecelerateInterpolator() | @android:anim/accelerate_decelerate_interpolator |
匀速 | LinearInterpolator() | @android:anim/linear_interpolator |
先后退再向前加速 | AnticipateInterpolator() | @android:anim/anticipate_interpolator |
快速超出终点一段再回到终点 | OvershootInterpolator() | @android:anim/overshoot_interpolator |
超出终点一段再回到终点 | AnticipateOvershootInterpolator() | @android:anim/anticipate_overshoot_interpolator |
弹几下回到终点 | BounceInterpolator() | @android:anim/bounce_interpolator |
基本上系统提供的插值器就能满足平时需求,当然我们也可以自定义插值器,这里就不深入探讨了,具体可以看看参考的博客。
属性动画(Property Animation)
属性动画是 Android 3.0 提供的动画模式,是补间动画的扩展,甚至可以代替补间动画。
补间动画存在许多缺陷,比如“只能对 View 进行操作”,“只有渐变、缩放、平移、旋转四个动画”,“只改变 View 的显示缺不改变属性”等。比如移动一个按钮,虽然看起来按钮移动了,但是还是要点击按钮的原本位置才能触发点击事件。即看起来位置变了,实际上没有。
属性动画之所以叫属性动画,就是他实现了属性的改变。我们设定动画时长、类型、初始值、结束值即可。属性动画根据内容不断改变值,并将值赋给属性,从而实现有动画效果的同时还修改了属性。
基础使用:ObjectAnimator
好的,因为属性动画的实现依靠的是全新的类,所以我们得把之前几个动画的代码推翻重做…
属性动画的核心类是 ValueAnimator
,比如下面的代码实现了将一个值从 0 到 3 到 1 的平滑改变:
ValueAnimator animator = ValueAnimator.ofFloat(0f, 3f, 1f);
animator.start();
实际上ofFloat
后面可以传入任意多个参数,这个值会按顺序进行平滑变化,比如ofFloat(0f, 3f, 1f, 2f, 5f...)
但是呢,显然我们很少用到一个值的改变,说好的动画呢?更多时候我们需要修改一个对象的某个属性值,所以用到最多的还是 ObjectAnimator
类
不过由于ObjectAnimator
继承了ValueAnimator
,所以我们还是认为ValueAnimator
是属性动画的核心类。
那么如何用ObjectAnimator
来修改对象的属性值呢?还记得上面我们有个 ImageView 吧,我们来修改他的透明度吧
public void btnClick(View view) {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "alpha", 1f, 0f, 1f);
objectAnimator.setDuration(2000);
objectAnimator.start();
}
虽然是ObjectAnimator
,不过还是ofFloat
方法,传入的参数分别对象、属性、变化值(任意多个)。然后我们设置了动画的持续时间(ValueAnimator
也可以设置事件),并启动动画。这里就体现了属性动画和之前的动画不同的地方:我们在补间动画中实现淡入淡出效果的时候是修改其颜色(RGBA),在这里则是直接修改透明度(alpha)
所以只要我们修改属性内容,就可以实现许多动画效果
比如旋转:ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "rotation", 0f, 360f);
比如纵向放大:ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "scaleY", 1f, 3f, 1f);
看起来很神奇,仔细一想,ObjectAnimator
是怎么根据传入的属性值来确定要修改哪个属性呢?又有哪些属性值可以用来传入呢?
实际上,我们用 alpha
调整透明度,但是ImageView
本身是没有这个属性的,不过作为他的父类,View
,有这个属性(这一点和郭霖的博客有所出入,查了下应该是 Android 3.0 新加入的)。此外,View
还有该属性的 getter 和 setter 方法,即 public float getAlpha()
和 public void setAlpha(float value)
,而这则是属性动画找到对应属性的依据
同理,也是因为View
有getRotation()
和 setRotation()
方法,我们才能实现旋转效果;有getScaleX()
、setScaleX
和 getScaleY()
、setScaleY()
才能……等等
组合使用:AnimatorSet
上面展现了属性动画的简单使用,不过仅实现了单个动画效果,如果要两个动画效果一起实现要怎么做呢?
这里就需要用到AnimatorSet
这个类,他的play()
方法返回一个AnimatorSet.Builder
实例,他有四个方法来帮助我们决定两个动画要如何实现
方法 | 作用 |
---|---|
after(Animator anim) | 将现有动画插入到传入的动画之后执行 |
after(long delay) | 将现有动画延迟指定毫秒后执行 |
before(Animator anim) | 将现有动画插入到传入的动画之前执行 |
with(Animator anim) | 将现有动画和传入的动画同时执行 |
比如我们让我们的ImageView
边淡入淡出边旋转
public void btnClick(View view) {
ObjectAnimator fadeInOutAnim = ObjectAnimator.ofFloat(imageView, "alpha", 1f, 0f, 1f);
ObjectAnimator rotateAnim = ObjectAnimator.ofFloat(imageView, "rotation", 0f, 360f);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(rotateAnim).with(fadeInOutAnim);
animatorSet.setDuration(2000);
animatorSet.start();
}
XML 文件事件使用
和补间动画类似,属性动画也可以通过 XML 文件实现。虽然代码量变多了,但是更易于重用。我们在 res 目录下面新建 animator 文件夹用于存放属性动画的资源。
我们尝试用 XML 文件实现上面组合实现的动画效果:边淡入淡出边旋转
新建res -> animator -> anim_property.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<objectAnimator
android:duration="2000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType"/>
<set android:ordering="sequentially">
<objectAnimator
android:duration="1000"
android:propertyName="alpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType"/>
<objectAnimator
android:duration="1000"
android:propertyName="alpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"/>
</set>
</set>
可以看到,XML 中的标签实际上就对应了各个类,<objectAnimator>
对应了ObjectAnimator
,<set>
对应了AnimatorSet
(还有<animator>
对应了ValueAnimator
,不过用的比较少)
不过要注意我们要把透明度从 0 -> 1 和从 1 -> 0 的过程分开来写成两个<objectAnimator>
最后在代码中载入这个 XML 文件,修改点击事件:
public void btnClick(View view) {
Animator animator = AnimatorInflater.loadAnimator(this, R.animator.anim_property);
animator.setTarget(imageView);
animator.start();
}
这里就比较简单了,用AnimatorInflater
传入上下文 Context 和 XML 文件,设置目标对象,最后启用动画。
应用于非控件对象
属性动画的一大特点就是可以对非控件对象(非 View 对象)使用。我们仿照郭霖的博客里,对一个 Point 类进行属性的变换。
首先我们写一个 Point 类,有 x 和 y 两个变量用于记录坐标的位置
public class Point {
private float x;
private float y;
//constructor
public Point(float x, float y) {
this.x = x;
this.y = y;
}
//getter
public float getX() {
return x;
}
public float getY() {
return y;
}
}
动画过程:TypeEvaluator
在此之前,我们先看看属性动画是如何控制动画的执行过程的。在之前的动画中我们知道,属性动画默认是一个平滑过度,即匀速执行动画。那他是怎么实现的呢?这就要用到TypeEvaluator
之前,我们使用的是ofFloat()
方法,他会自动调用系统的FloatEvaluator
,我们来看看源码
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
实际上FloatEvaluator
实现了TypeEvaluator
接口,重写evaluate()
方法,传入三个参数,分别是表示动画的完成度的fraction
,初始值,结束值。
FloatEvaluator
用结束值减初始值得到差值,然后乘以fraction
这个系数,再加上初始值,那么就得到当前动画的值了。
ValueAnimator
的ofFloat()
和ofInt()
方法分别用于浮点型和整型的数据进行动画操作,此外他还有一个ofObject()
方法,用于对任意对象进行动画操作的。但如果调用这方法,系统会不知道如何计算动画的过度过程,因此就要我们自己实现TypeEvaluator
。
于是乎我们定义PointEvaluator
来对 Point 类进行动画过程的计算。
public class PointEvaluator implements TypeEvaluator {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
Point point = new Point(x, y); return point;
}
}
逻辑大同小异,我们用更改后新的 Point 替换旧的 Point,实现这个 Point 对象 x,y 坐标的修改。
在代码中新建两个 Point 作为起始值和结束值,调用ofObject()
方法即可实现 Point 对象的属性动画效果。不过这个方法要把我们之前定义的PointEvaluator
对象作为参数传入。
public void btnClick(View view) {
Point startPoint = new Point(0,0);
Point endPoint = new Point(300, 300);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint,endPoint);
anim.setDuration(2000);
anim.start();
}
将 Point 应用于自定义 View:ValueAnimator 的高级用法
此处内容来源于郭霖的博客,实现一个平移的动画效果。
我们新建一个自定义 ViewMyAnimView
,其中根据一个 Point 对象,来画出一个圆,并使这个圆从左上角移动到右下角。
(在布局中应用这个自定义 View 即可,这里就不放代码了,具体可见原博客)
public class MyAnimView extends View {
public static final float RADIUS = 50f;
private Point currentPoint;
private Paint mPaint;
public MyAnimView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
if (currentPoint == null) {
currentPoint = new Point(RADIUS, RADIUS);
drawCircle(canvas);
startAnimation();
} else {
drawCircle(canvas);
}
}
public void drawCircle(Canvas canvas) {
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y ,RADIUS, mPaint);
}
public void startAnimation() {
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint,endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(2000);
anim.start();
}
}
我们用一个 Point 作为圆心(currentPoint),用一个 Paint 画笔来画圆。
一开始载入 View,我们的currentPoint
为空,我们在初始位置画出一个圆,进入startAnimation()
方法,其中采用属性动画方式,修改 Point 的坐标值。同时这里用addUpdateListener()
方法设置了一个监听器,每次修改currentPoint
的属性值(x 和 y)后调用invalidate()
方法重新用onDraw()
绘制 View。
重新绘制 View 时,currentPoint
不为空,因此直接根据当前currentPoint
的 x,y 来绘制新的圆,重复过程知道动画结束。
这样就实现了这个圆移动的动画效果。区别于其他动画,我们仅修改了圆心的坐标,而非对整个圆添加动画效果。同时这也是ValueAnimator 的高级用法。
动态改变颜色:ObjectAnimator 的高级用法
我们继续实现一些补间动画无法实现的功能,比如上面这个移动的圆,我们能不能在他运动的过程中改变他的颜色呢?
之前我们用过ObjectAnimator
,他可以根据传参来达到修改属性的目的,前提是这个属性有 getter 和 setter。
于是我们在MyAnimView
中定义一个 Color 属性,这里用字符串形式来表达 RGB 颜色。注意 setter 里,改变颜色后用invalidate()
重新绘制。
public class MyAnimView extends View {
······
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
invalidate();
}
······
}
然后我们想用ObjectAnimator
来修改 color 这个属性,但是别忘了,我们需要编写一个TypeEvaluator
来说明两种颜色是如何变化的。
新建ColorEvaluator
,他的代码虽然很多,但是还是比较好理解的,别怕别怕,不想看可以不看的,不影响咱理解属性动画。
public class ColorEvaluator implements TypeEvaluator {
private int mCurrentRed = -1;
private int mCurrentGreen = -1;
private int mCurrentBlue = -1;
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
//颜色初值和终值
String startColor = (String) startValue;
String endColor = (String) endValue;
int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
// 初始化颜色的值
if (mCurrentRed == -1) {
mCurrentRed = startRed;
}
if (mCurrentGreen == -1) {
mCurrentGreen = startGreen;
}
if (mCurrentBlue == -1) {
mCurrentBlue = startBlue;
}
// 计算初始颜色和结束颜色之间的差值
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff + greenDiff + blueDiff;
if (mCurrentRed != endRed) {
mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0, fraction);
} else if (mCurrentGreen != endGreen) {
mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction);
} else if (mCurrentBlue != endBlue) {
mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff, redDiff + greenDiff, fraction);
}
// 将计算出的当前颜色的值组装返回
String currentColor = "#" + getHexString(mCurrentRed)
+ getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
return currentColor;
}
/**
* 根据fraction值来计算当前的颜色。
*/
private int getCurrentColor(int startColor, int endColor, int colorDiff, int offset, float fraction) {
int currentColor;
if (startColor > endColor) {
currentColor = (int) (startColor - (fraction * colorDiff - offset));
if (currentColor < endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (fraction * colorDiff - offset));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}
/**
* 将10进制颜色值转换成16进制。
*/
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}
}
首先在我们得到颜色的初始值和结束值,对其进行字符串截取将颜色分为 RGB 三个部分,并转换成十进制,即每个颜色的取值范围是 0-255。
然后计算颜色初始值和结束值之间的差值(colorDiff),他决定颜色变化的快慢,如果差值小,说明颜色接近,颜色变化就会比较缓慢,反之则变化快。 这具体由getCurrentColor()
实现,他根据fraction
计算目前应过度到什么颜色,并根据差值来控制变化速度。
最后,用getHexString()
方法把我们的十进制颜色变为十六进制,再将 RGB 三种颜色拼装,作为最终的结果返回。
最后调用就用之前ObjectAnimator
的ofObject()
方法,记得根据 Id 绑定控件myAnimView
,传入属性参数“color”,传入ColorEvaluator
对象,起始颜色(蓝色#0000FF)和结束颜色(红色#FF0000)
ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, "color", new ColorEvaluator(), "#0000FF", "#FF0000");
anim.setDuration(2000);
anim.start();
好像有哪里不对······
咦,这样我的颜色虽然会变,但是之前的平移动画怎么办?
因为他们是两个动画,所以需要进行一个动画的组合,有请我们的好伙伴AnimatorSet
来到MyAnimView
的startAnimation()
方法
public void startAnimation() {
//改变Point坐标动画(ValueAnimator)
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint,endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
//改变颜色动画(ObjectAnimator)
ObjectAnimator colorAnim = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(), "#0000FF", "#FF0000");
//组合动画(AnimatorSet)
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(anim).with(colorAnim);
animatorSet.setDuration(2000);
animatorSet.start();
}
现在我们有两个动画,分别是改变 Point 坐标的anim
和改变颜色的colorAnim
,然后用AnimatorSet
让两个动画一起进行
Interpolator 差值器
这东西听起来是不是很眼熟,他本质上和补间动画的差值器是一样的,控制动画的变化速率,这里就来看看在属性动画中如何设置这玩意。
因为属性动画是 3.0 新增的,在这之前就有了Interpolator
接口,为了兼容,3.0 同时新增了TimeInterpolator
接口,他有很多实现类我们可以直接拿来使用。比如AccelerateInterpolator
就是加速,DecelerateInterpolator
就是减速,同样默认是先加速后减速的AccelerateDecelerateInterpolator
,这也和补间动画相同。
我们修改之前的代码,换一个Interpolator
,模拟小球竖直落下然后弹起的效果。
public void startAnimation() {
//改变Point坐标动画(ValueAnimator)
Point startPoint = new Point(getWidth() / 2, RADIUS);
Point endPoint = new Point(getWidth() / 2, getHeight() - RADIUS);
······
//两个动画设置不变
//在start()方法前,修改Interpolator
anim.setInterpolator(new BounceInterpolator());
······
}
如果想继续学习Interpolator
,然后自定义Interpolator
,可以看郭霖的博客,这里就不深入了。
ViewPropertyAnimator
这个东西是 Android 3.1 新增的一个小玩意,主要是用于简化属性动画的代码使用。
3.0 推出属性动画后,属性动画越来越得到大家的青睐,但是好像又觉得, ObjectAnimator animator = ObjectAnimator.ofFloat(image, "alpha", 1f, 0f...);
这样的代码用起来挺麻烦的,要将对象、属性、变化值,都传入方法当中,似乎有悖于面向对象的思维。
于是推出了ViewPropertyAnimator
,在官方文档中,这样说道:
“
ViewPropertyAnimator
有助于使用单个底层Animator
对象轻松为View
的多个属性并行添加动画效果。它的行为方式与ObjectAnimator
非常相似,因为它会修改视图属性的实际值,但在同时为多个属性添加动画效果时,它更为高效。此外,使用ViewPropertyAnimator
的代码更加简洁,也更易读。”
怎么轻松高效,简洁易读呢?我们来试试,回到之前的ImageView
页面,我想让他透明度变为 0,可以使用如下的一句代码:
public void btnClick(View view) {
imageView.animate().alpha(0f).setDuration(2000).setInterpolator(new AccelerateInterpolator()).start();
}
很显然,ViewPropertyAnimator
使用了连缀语法来进行代码上的简化,每个方法的返回值都是它自身的实例,也是典型的建造者模式。
animate()
方法会创建并返回一个ViewPropertyAnimator
的实例,之后进行方法调用,属性设置都是通过这个实例完成。
事实上,使用ViewPropertyAnimator
的时候,就算最后没有显示调用start()
,动画也会自动启动。不过如果我们不断地连缀新的方法,那么动画就不会立刻执行,而是等到所有在ViewPropertyAnimator
上设置的方法都执行完毕后,再启动动画。
后话
好家伙,写属性动画的时间比逐帧动画和补间动画加起来都多
还好有郭霖的博客,不然就跟无头苍蝇一样乱
不过官方文档里也有关于属性动画的介绍,还有专门一篇博客介绍,有兴趣可以去看看
能看到这里,你也不容易,感谢。