Android动画精炼详解第(二)节:属性动画讲解及实现
一、前期基础知识储备
上节讲解的《Android动画精炼详解第(一)课:帧动画、补间动画讲解和示例》讲解了最常见最常用的三种动画的分类:帧动画、补间动画、属性动画,并带领大家简单实现了帧动画和补间动画,接下来的本节内容将为大家继续讲解第三种动画——属性动画。
属性动画(Property Animation)是在Android3.0中引入的,为什么要引入属性动画呢?
(1)补间动画的缺陷:在补间动画中,我们只能改变View的绘制效果,View的真实属性没有变化,而属性动画则是直接改变View对象的属性值。什么意思?假如你把一个Button从屏幕的左上角移到右下角,你在补间动画中点击右下角的Button按钮是没有任何作用的,因为Button的真实属性位置还在左上角。这是补间动画的致命缺陷。其次补间动画另一一个缺陷,就是它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,补间动画机制就是使用硬编码的方式来完成的,功能限定死就是这些,基本上没有任何扩展性可言。
(2)属性动画的优势:相较于补间动画,属性动画真实改变了View的属性,而之前的那两种动画则只是视图上的改变,其触发位置仍旧没有变化,因此属性动画更适合做一些需要交互较强的动画。补间动画是只能够作用在View上,只要是任意继承View的控件都可以使用,但是对于非View控件,补间动画就爱莫能助了。
二、上代码,具体实现属性动画
在属性动画中,有2个类需要我们重点关注:ValueAnimator类及其子类ObjectAnimator类.
①ValueAnimator类,上官方文档:
This class provides a simple timing engine for running animations which calculate animated values and set them on target objects.There is a single timing pulse that all animations use. It runs in a custom handler to ensure that property changes happen on the UI thread.
By default, ValueAnimator uses non-linear time interpolation, via the AccelerateDecelerateInterpolator class, which accelerates into and decelerates out of an animation. This behavior can be changed by calling setInterpolator(TimeInterpolator).
由官方文档,我们知道,ValueAnimator是整个属性动画机制当中最核心的一个类,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,是一个非常重要的类。
举一个最简单的例子来讲解ValueAnimator类的使用:
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(300);
anim.start();
在这里调用ValueAnimator的ofFloat()方法可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和1就表示将值从0平滑过渡到1,然后调用ValueAnimator的setDuration()方法来设置动画运行的时长,最后调用start()方法启动动画。
————————————————————我是分隔线—————————————————————
②ObjectAnimator类,上官方文档:
This subclass of ValueAnimator provides support for animating properties on target objects. The constructors of this class take parameters to define the target object that will be animated as well as the name of the property that will be animated. Appropriate set/get functions are then determined internally and the animation will call these functions as necessary to animate the property.
由官方文档,我们可以知道属性动画在实际开发中打交道最多的一个类就是ObjectAnimator类,继承自ValueAnimator类,它是属性动画中最重要的一个执行类。ObjectAnimator使用起来也相对较方便,举一个常见的例子:
public class MainActivity extends AppCompatActivity {
private ObjectAnimator anim;
private TextView textview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textview = (TextView) findViewById(R.id.text_view);
ObjectAnimator anim = ObjectAnimator.ofFloat(textview,"alpha", 1f, 0f, 1f);
anim.setDuration(6000);
anim.start();
}
}
这里我们实现的是将一个TextView在6秒中内从常规变换成全透明,再从全透明变换成常规。第一个参数是要操作的View,第二个参数是要操作的属性,第三个参数是对应的属性变化,其是一个可变数组,即后面可继续添加参数,如这个效果就是将对textView控件加上了一个淡入淡出的动画。第三个参数及其之后的参数为关键转折点。重点说下第二个参数,我们对View的动画操作常用的有(即第二个参数的值):
pivotX和pivotY:设置View的支点位置,默认为View的中心点
translationX和translationY:View的X轴和Y轴的偏移量,或者说位移距离。
rotation、rotationX和rotationY:View围绕支点,分别以Z轴、X轴和Y轴旋转,其中Z轴垂直屏幕。
scaleX和scaleY:View以支点做缩放。
x和y:直接设置View的位置,相当于原位置移动x和y距离。
alpha:设置View的透明度,1不透明,0全透明。
运行效果图:
PS:ObjectAnimator类中获取ObjectAnimator实例的ofFloat()方法官方文档描述参见:ofFloat()
③实现组合动画—AnimatorSet类
独立的动画能够实现的视觉效果毕竟是相当有限的,因此将多个动画组合到一起播放就显得尤为重要。Android团队在设计属性动画的时候也充分考虑到了组合动画的功能,因此提供了一套非常丰富的API来让我们将多个动画组合到一起。
实现组合动画功能主要需要借助AnimatorSet这个类,这个类提供了一个play()方法,如果我们向这个方法中传入一个Animator对象(ValueAnimator或ObjectAnimator)将会返回一个AnimatorSet.Builder的实例,AnimatorSet.Builder中包括以下四个方法:
after(Animator anim) 将现有动画插入到传入的动画之后执行
after(long delay) 将现有动画延迟指定毫秒后执行
before(Animator anim) 将现有动画插入到传入的动画之前执行
with(Animator anim) 将现有动画和传入的动画同时执行
举一个简单的动画逻辑:让TextView先从屏幕外移动进屏幕,然后开始旋转360度,旋转的同时进行淡入淡出操作,代码如下:
ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, "translationX", -500f, 0f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).after(moveIn);
animSet.setDuration(5000);
animSet.start();
运行结果如图:
④动画事件的监听
在属性动画执行的过程中,如果需要在某个执行过程做其他操作,则需添加监听事件,Animator类当中提供了一个addListener()方法,这个方法接收一个AnimatorListener,我们只需要去实现这个AnimatorListener就可以监听动画的各种事件了,代码如下:
objectAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 动画开始
}
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束
}
@Override
public void onAnimationCancel(Animator animation) {
// 取消动画
}
@Override
public void onAnimationRepeat(Animator animation) {
// 动画重复时
}
});
但是也许很多时候我们并不想要监听那么多个事件,可能我只想要监听动画结束这一个事件,那么每次都要将四个接口全部实现一遍就显得非常繁琐。没关系,为此Android提供了一个适配器类,叫作AnimatorListenerAdapter,使用这个类就可以解决掉实现接口繁琐的问题了,代码如下:
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
}
});
最后,再次看看ValueAnimator的官方文档描述:
Animators can be created from either code or resource files.
由官方文档,我们知道,属性动画和补间动画一样,有两种实现方式,第一种是在Activity代码中设置,第二种是在XML布局中写好代码,然后再在Activity中引入这个布局。
通过XML来编写动画可能会比通过代码来编写动画要慢一些,但是在重用方面将会变得非常轻松,比如某个将通用的动画编写到XML里面,我们就可以在各个界面当中轻松去重用它。如果想要使用XML来编写动画,首先要在res目录下面新建一个anim文件夹,所有属性动画的XML文件都应该存放在这个文件夹当中。在XML文件中我们一共可以使用如下三种标签:
animator> 对应代码中的ValueAnimator
objectAnimator> 对应代码中的ObjectAnimator
set> 对应代码中的AnimatorSet
看看官方文档中的XML代码的例子:
<-- animator节点 - 对应ValueAnimator -->
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType"
android:repeatCount="1"
android:repeatMode="reverse"/>
<-- set节点 - 对应AnimatorSet -->
<-- 缩放动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000">
<scale android:fromXScale="0.2" android:toXScale="1"
android:pivotX="50%"
android:pivotY="50%"
android:fromYScale="0.2" android:toYScale="1"/>
</set>
<-- 淡入淡出动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:repeatMode="reverse">
<alpha android:fromAlpha="0.2" android:toAlpha="1"
android:repeatCount="infinite"/>
</set>
<-- 平移动画 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000"
android:fillAfter="true"
android:interpolator="@android:anim/overshoot_interpolator">
<!--android:fillAfter="true" 设置是否保留最终状态 -->
<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="400"
android:toYDelta="0">
<!-- android:repeatCount="infinite" 无限重复 只能在子节点中单独设置-->
</translate>
</set>
animator节点动画加载到代码里
ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(MyActivity.this,R.animator.animator);
valueAnimator.start();
objectAnimator节点动画加载到代码里
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.object_animator);
animator.setTarget(mTv1);
animator.start();
set节点动画加载到代码里
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.set_animator);
set.setTarget(mTv1);
set.start();
其他内容,感兴趣的读者可参考下《安卓动画学习(六)--xml实现属性动画》
三、一个使用属性动画的综合实例
要求:在一个属性动画执行完之后,执行另一个属性动画
public class PaintPosition extends View {
private Paint paint;
private String str = "Hello World";
private float mRadius;
public static final float INT = 60f;
private Point currentPoint;
public PaintPosition(Context context) {
super(context);
init();
}
public PaintPosition(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PaintPosition(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextSize(50);
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
}
protected void onDraw(Canvas canvas) {
canvas.drawText(str, getWidth() / 2 - paint.measureText(str) / 2,
getHeight() / 2, paint);
//当一个值为空时没有处理,就一点效果都没有,只有当动画运行之后,才会有值,所以需要判断初始值为0的时候
canvas.save();
if (mRadius == 0) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, paint);
// startCircleAnimation();
}else {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mRadius, paint);
}
//当一个值为空时没有判断为空的情况就会报空指针错误
canvas.save();
if (currentPoint == null) {
currentPoint = new Point(INT, INT);
drawCircle(canvas);
startTestAnimation();
} else {
drawCircle(canvas);
}
}
private void drawCircle(Canvas canvas) {
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, INT, paint);
}
public class Point {
private float x;
private float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
}
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;
}
}
private void startTestAnimation() {
Point startPoint = new Point(INT, INT);
Point endPoint = new Point(getWidth() - INT, getHeight() - INT);
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(3000);
anim.start();
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startCircleAnimation();
}
});
}
private void startCircleAnimation() {
ValueAnimator anim = ValueAnimator.ofFloat(100f, 200f);
anim.setDuration(3000);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mRadius = (float) animation.getAnimatedValue();
invalidate();
}
});
anim.start();
}
}
运行效果如图:
属性动画有两个动画监听器:
①anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() 用于监听动画值的改变;
在该例中,该监听器用于监听动画发生时小圆的位置坐标和大圆的半径坐标。
②anim.addListener(new AnimatorListenerAdapter() 用于监听动画发生过程中的事件;
在该例中,该监听器用于监听小圆动画结束时大圆动画的开始。