换个思路来看Android属性动画
一句话总结属性动画:
根据属性值变化区间和持续时间,在每个时间点确定一个值,调用动画作用对象的某条属性的set方法,修改该属性的值,具体想要这个这个属性值改变之后,动画效果怎么表现,需要在作用对象的作用属性的set方法或者动画的UpdateListener数据更新监听中自定义。
也就是说一个属性动画的并不能确定具体的动画效果是什么样子的,还需要配合动画作用的对象才能确定最终的动画表现
属性动画所定义的只是 作用对象
的作用属性
在作用持续时间
和变化范围
内的变化
以下会有两个例子帮助理解这句话
一.从简单的例子开始
这里直接给出使用属性动画实现的简单例子,可以帮助建立起属性动画的使用方法主线,后续在这条主线的基础上补充分支
例子一:抖动的Hello World
首先在Activity的布局文件中添加一个TextView
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="Hello World"
android:textSize="25sp"
/>
然后在Activity中设置属性动画:
public void initTextAnimator(){
textView=findViewById(R.id.tv_text);
//ofFloat(Object target, String propertyName, float... values)
//第一个参数为object类型的target,这里是textview
//第二个参数是要做动画的属性值
//最后一个可以传入任意个数的参数,动画在他们数值之间过渡
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView, "rotation", 0f, 20f, -20f, 20f, -20f, 0f);
//动画时长
objectAnimator.setDuration(3000);
//动画重复次数
objectAnimator.setRepeatCount(2);
//动画重复模式
objectAnimator.setRepeatMode(ValueAnimator.RESTART);
//设置动画插值器
objectAnimator.setInterpolator(new AccelerateInterpolator());
//启动动画
objectAnimator.start();
}
onCreate中调用initTextAnimator()就可以实现以上动画效果
现在简单分析:
这个属性动画的作用对象:TextView
作用属性:rotation
作用变化范围:0 → 20 → -20 → 20 → -20 → 0
作用时间:3000毫秒
重复次数:2
重复模式:从头开始
插值器:先慢后快
也就是说在3秒的时间内,rotation
旋转属性要从0到20再到-20...最后到0,所以我们就可以看到Hello World开始摇摆
但其实rotation
属性并不在TextView中,而是在它的父类View中,我们可以在View中找到rotation
的set方法
public void setRotation(float rotation) {
if (rotation != getRotation()) {
// Double-invalidation is necessary to capture view's old and new areas
invalidateViewProperty(true, false);
mRenderNode.setRotationZ(rotation);
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
notifySubtreeAccessibilityStateChangedIfNeeded();
}
}
例子二:画个圈(需要一点点自定义View的前置知识,花20分钟即可了解,推荐文章https://www.jianshu.com/p/705a6cb6bfee)
这是我们想要的效果:
怎么通过属性动画来实现这个画个圈的效果呢?
这里我自定义了一个
MyCircleView
首先我们分析以下画这个圈是通过画弧来实现的,每一帧这个弧度都会增长一点,从0到360,最终完成一个完整的圈
所以在
MyCircleView
中增加一个属性sweepAngle
并为它添加get和set方法
public class MyCircleView extends View {
//画笔
private Paint paint;
//绘制弧度
int sweepAngle;
public int getSweepAngle() {
return sweepAngle;
}
public void setSweepAngle(int sweepAngle) {
this.sweepAngle = sweepAngle;
}
...其他代码省略...
}
这里的弧度sweepAngle
就是我们接下来定义的属性动画要作用的属性,当然MyCircleView
就是属性动画要作用的对象了,作用的属性一定要有set方法,否则会报错。
接下来我们在onDraw(Canvas canvas)
根据弧度画出圆弧:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取绘制的View的宽度
int width = getWidth();
//获取绘制的View的高度
int height = getHeight();
paint.setAntiAlias(true);//设置抗锯齿
paint.setStyle(Paint.Style.STROKE); // 画弧线,画笔样式设置为空心
paint.setStrokeWidth(5);//设置线宽
paint.setColor(Color.BLUE);//画笔颜色为blue
// 设置矩形区域 drawArc会画出于次矩形区域内切的弧(正方形是正圆弧,长方形是椭圆弧)
RectF rectF = new RectF(0, 0, width, width);
//画弧(矩形区域,起始角,弧度,是否经过圆心,画笔)
canvas.drawArc(rectF, 150, sweepAngle, false, paint); // 第四个参数 userCenter为true,表示轨迹经过圆心
}
以上代码最主要的其实只是最后一句:
canvas.drawArc(rectF, 150, sweepAngle, false, paint);
根据sweepAngle初始值画出弧度(目前为0)
接下来才开始用到属性动画,定义一个播放动画的方法showAnim
//传入参数 想要变化到的弧度toSweepAngle
public void showAnim(int toSweepAngle){
//定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
//设置动画时间为5000ms 也就是5秒
mSweepAnimator.setDuration(3000);
//开始播放
mSweepAnimator.start();
}
然后我们知道,属性动画就是调用的set方法改变某属性的值,所以我们需要在sweepAngle的set方法中,重新绘制View,使这个值的变化体现出来
public void setSweepAngle(int sweepAngle) {
this.sweepAngle = sweepAngle;
//重绘View
postInvalidate();
}
现在简单分析:
这个属性动画的作用对象:MyCircleView
作用属性:sweepAngle
作用变化范围:0 → 360
作用时间:3000毫秒
也就是说sweepAngle在3秒内从0连续变化到了360,并且每变化一次,MyCircleView都会进行重绘,体现出变化的效果
接下来我们在MainActivity中调用showAnim方法就可以看到动画了
public void initProgressBar(){
myCircleView =findViewById(R.id.my_circle_view);
//设置点击监听 点击时播放动画
myCircleView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myCircleView.showAnim(360);
}
});
}
以下是完整的MyCircleView代码,可以直接使用
public class MyCircleView extends View {
//画笔
private Paint paint;
//绘制弧度
int sweepAngle;
public MyCircleView(Context context) {
super(context);
initView();
}
public MyCircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public MyCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initView();
}
private void initView() {
paint = new Paint();
sweepAngle=0;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//这里只处理了wrap_content 时 默认为大小300
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 300);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取各个编剧的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
float strokeWidth=5;//圆圈线宽
//获取绘制的View的宽度
int width = getWidth();
//获取绘制的View的高度
int height = getHeight();
paint.setAntiAlias(true);//设置抗锯齿
paint.setStyle(Paint.Style.STROKE); // 画弧线,画笔样式设置为空心
paint.setStrokeWidth(5);//设置线宽
paint.setColor(Color.BLUE);//画笔颜色为blue
// 设置矩形区域 drawArc会画出于次矩形区域内切的弧(正方形是正圆弧,长方形是椭圆弧)
RectF rectF = new RectF(paddingLeft+strokeWidth/2, paddingTop+strokeWidth/2, width-paddingRight-strokeWidth/2, width-paddingBottom-strokeWidth/2);
//画弧(矩形区域,起始角,弧度,是否经过圆心,画笔)
canvas.drawArc(rectF, 150, sweepAngle, false, paint); // 第四个参数 userCenter为true,表示轨迹经过圆心
}
//展示进度条动画
public void showAnim(int toSweepAngle){
//重置弧度为0
sweepAngle=0;
//定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
//设置动画时间为5000ms 也就是5秒
mSweepAnimator.setDuration(3000);
//开始播放
mSweepAnimator.start();
}
public int getSweepAngle() {
return sweepAngle;
}
public void setSweepAngle(int sweepAngle) {
this.sweepAngle = sweepAngle;
postInvalidate();
}
}
二.主线上的一些扩展分支
通过以上两个例子,我们可以掌握属性动画的主线工作原理 即:
根据某条属性值变化区间和持续时间,该属性在持续时间内连续变化,调用动画作用对象的该属性的set方法,修改该属性的值
但是目前我们只是掌握了主干上的实现原理,就像一颗光秃秃的树干,接下来要学习更多的分支内容,使这个树干长出树枝,树叶,才能把属性动画做的漂亮
2.1 ValueAnimator和ObjectAnimator
Animator类提供了创建动画的基本组成,通常不直接使用这个类而是用ValueAnimator和ObjectAnimator来创建属性动画。
这二位有什么区别?
ValueAnimator
是整个属性动画机制当中最核心的一个类。它使用一种时间循环的机制来计算值与值之间的动画过渡,负责管理动画的播放次数、播放模式、设置动画设置监听器、设置自定义类型等。有两块动画属性:计算动画值和设置这些对象或属性的动画。ValueAnimator不执行第二个,所以你必须设置ValueAnimator更新值和修改对象的监听。ValueAnimator不会自动调用set方法,甚至在创建动画实例的时候可以不定义ValueAnimator的作用属性,但是必须在UpdateListener
动画状态的监听中调用作用对象的作用属性的set方法
因为ValueAnimator并不直接作用于对象或属性,通常会通过这些计算的值来不断改变动画的对象,可以通过设置监听器并调用getAnimatedValue()来不断获得帧刷新的计算值
animator.setDuration(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
Log.e("TTT", "value is " + value);
}
});
animator.start();
ObjectAnimator
是ValueAnimator的一个子类,ObjectAnimator可以对任意对象及对象属性设置动画。通常情况下,使用ObjectAnimator更多一些因为它使目标对象动画过程更简单,然而因为ObjectAnimator有更多的限制,有时候使用ValueAnimator更合理一些,比如需要特定的acessor方法出现在目标对象。
2.2 ValueAnimator/ObjectAnimator常用方法
/**
* 设置动画时长,单位是毫秒
*/
setDuration(long duration)
/**
* 获取ValueAnimator在运动时,当前运动点的值
*/
Object getAnimatedValue();
/**
* 开始动画
*/
void start()
/**
* 设置循环次数,设置为ValueAnimator.INFINITE表示无限循环
*/
void setRepeatCount(int value)
/**
* 设置循环模式
* value取值有RESTART,REVERSE,
*/
void setRepeatMode(int value)
/**
* 取消动画
*/
void cancel()
2.3 插值器Interpolator
什么是插值器?
Interpolator是时间插值器,用来修饰动画效果,它可以指定属性值如何随时间变化的,反应了动画的运动速率,运动速率可以是线性变化的(如匀速)也可以是非线性变化的(如加速、减速)
比如例子二中,弧度在3秒内从0到360是一个匀速的线性变化,那我现在想要画圆先快后慢,需要怎么做?这个时候就需要用到插值器了
使用public void setInterpolator(TimeInterpolator value)
方法,为动画添加一个插值器DecelerateInterpolator
即可
//展示进度条动画
public void showAnim(int toSweepAngle){
//重置弧度为0
sweepAngle=0;
//定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
//设置动画时间为5000ms 也就是5秒
mSweepAnimator.setDuration(3000);
//设置插值器
mSweepAnimator.setInterpolator(new DecelerateInterpolator());
//开始播放
mSweepAnimator.start();
}
当然,DecelerateInterpolator
只是先快后慢的插值器,系统还提供了其他各种各样的插值器:
插值器(接口/类) | 对应效果 |
---|---|
AccelerateDecelerateInterpolator(默认) | 插入器的变化速度在开始和结束的地方慢,在中间的时候加速 |
AccelerateInterpolator | 变化速度开始缓慢,然后加速 |
AnticipateInterpolator | 开始后退,然后前进 |
AnticipateOvershootInterpolator | 开始后退,然后前进超过终点,最后返回终点 |
BounceInterpolator | 动画结束的时候弹跳至终点 |
CycleInterpolator | 动画循环播放指定的次数 |
DecelerateInterpolator | 变化速度加速开始,然后减慢 |
LinearInterpolator | 变化速度是固定的线性变化 |
OvershootInterpolator | 前进超过终点,最后返回终点 |
TimeInterpolator(接口) | TimeInterpolator是一个接口,如果以上插值器都不符合你的需求,可以实现TimeInterpolator接口来自定义插值器 |
2.4 动画的监听
有时候我们需要对动画进行监听,根据动画播放的状态进行某些动作
1.监听动画播放的四个状态
objectAnimator.addListeoner(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) {
//动画重播
}
});
//如果只想用其中的一个,只需改成适配器类AnimatorListenerAdapter即可:
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
}
});
2.监听动画播放过程中的值变化 这里也很关键 通常我们会在这调用作用对象的相关方法来表现出动画效果(不是所有对象的set方法都可以编辑)
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
/**
* 获取ValueAnimator在运动时,当前运动点的值
*/
animation.getAnimatedValue();
//float value = (float) animation.getAnimatedValue();
}
});
2.5 估值器Evaluators
Evaluators来告诉系统对于一个给定的属性如何来计算它的值,它们获得Animator提供的数据(动画起始值和结束值),并根据这些数据来计算动画值。
我们,知道创建动画实例的常用方法
ofFloat
:创建属性为浮点型的动画 默认估值器FloatEvaluator
ofInt
:创建属性为整型的动画 默认估值器IntEvaluator
ofArgb
:创建属性为颜色值的动画 默认估值器ArgbEvaluator
ofObject
:创建自定义属性的动画 没有估值器 需要自定义
ofPropertyValuesHolder
:包含PropertyValuesHolder的animator实例
其中ofPropertyValuesHolder的用法会补充在后边 这里暂时跳过:
我们重点看ofObject
,我们知道系统为ofFloat
,ofInt
,ofArgb
都提供了估值器
但是对ofObject 系统没办法提供默认的估值器,因为不知道该怎么计算,这个时候就需要我们针对自定义属性来实现TypeEvaluator
接口完成自定义估值器
public class MyEvalutor implements TypeEvaluator {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
return null;
}
}
2.5.1 估值器使用例子
原链接:https://www.jianshu.com/p/0389e2c8e6a8
先在activity中加入一个Textview
然后创建一个自定义object的ObjectAnimator例子,先定义一个ObInfo类:
public class ObInfo implements Serializable {
public int color;//用来定义颜色
public float x; //用来定义X轴
public float y; //用来定义Y轴
public ObInfo(int color, float x, float y) {
this.color = color;
this.x = x;
this.y = y;
}
}
接着自定义TypeEvaluator :
public class MyEvalutor implements TypeEvaluator<ObInfo> {
@Override
public ObInfo evaluate(float fraction, ObInfo startValue, ObInfo endValue) {
float x = startValue.x + fraction * (endValue.x - startValue.x);
float y = startValue.y + fraction * (endValue.y - startValue.y);
int color = (int) (startValue.color + fraction * (endValue.color - startValue.color));
return new ObInfo(color, x, y);
}
}
最后实现动画:
ObInfo info1 = new ObInfo(0xffffff00, 500, 200);
ObInfo info2 = new ObInfo(0xff0000ff, 500, 1000);
ValueAnimator animator = ValueAnimator.ofObject(new MyEvalutor(), info1, info2, info1);
animator.setDuration(4000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
ObInfo info = (ObInfo) animation.getAnimatedValue();
tv_text.layout(tv_text.getLeft(), (int) info.y, tv_text.getRight(), (int) (info.y + tv_text.getHeight()));
tv_text.setTextColor(info.color);
}
});
animator.start();
效果
2.6 PropertyValuesHolder和Keyframe
https://blog.csdn.net/harvic880925/article/details/50752838
原文链接https://www.jianshu.com/p/f32ef6b6e3a5
PropertyValuesHolder:顾名思义,就是属性值持有者,它保存了动画过程中所需要操作的属性和对应的值,我们通过ofFloat(Object target, String propertyName, float… values)构造的动画,ofFloat()的内部实现其实就是将传进来的参数封装成PropertyValuesHolder实例来保存动画状态。在封装成PropertyValuesHolder实例以后,后面的操作也是以PropertyValuesHolder为主的。
Keyframe:意为关键帧,设置了关键帧后,动画就可以在各个关键帧之间平滑过渡的,一个关键帧必须包含两个原素,第一时间点,第二位置,即这个关键帧是表示的是某个物体在哪个时间点应该在哪个位置上。fraction表示当前进度,value表示当前位置。
使用方法:先创建关键帧Keyframe
,然后通过多个关键帧创建一个PropertyValuesHolder
,最后通过ObjectAnimator.ofPropertyValuesHolder
实例化
//创建关键帧
//这里总共三个关键帧
//Keyframe.ofFloat(0.5f, 5f); 表示在动画进度50%的时候 属性要到5
Keyframe keyframe1 = Keyframe.ofFloat(0f, 0f);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 5f);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 0f);
//创建PropertyValuesHolder
PropertyValuesHolder propertyValuesHolder = PropertyValuesHolder.ofKeyframe(propertyName, keyframe1, keyframe2, keyframe3);
//创建ObjectAnimator
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(target, propertyValuesHolder);
//设置动画时长
objectAnimator.setDuration(1000);
//设置动画重复次数
objectAnimator.setRepeatCount(1);
//设置动画重复模式
objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
//启动动画
objectAnimator.start();
2.7 AnimatorSet
如果想使用组合动画,可以使用AnimatorSet将多个动画组合到一起:
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();
执行顺序:
1.执行bounceAnim动画
2.同时执行squashAnim1, squashAnim2, stretchAnim1, stretchAnim2动画
3.执行bounceBackAnim动画
4.最后fadeAnim
2.5 xml中定义动画
ValueAnimator 还可以用XML文件来写,这样写的好处是更容易被复用,为了和API 11之前的动画做区分,请将属性动画的XML文件放在res/animator/目录下,如新建一个value_animator.xml文件,示例:
<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse"
android:valueFrom="0.0"
android:valueTo="1.0"
android:valueType="floatType" />
在代码中加载XML文件:
//加载XML文件
ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(this, R.animator.value_animator);
//设置要执行动画的目标
animator.setTarget(myObject);
//动画执行
animator.start();
还可以使用 PropertyValuesHolder和Keyframe标签创建一个多步的动画,如:
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse">
<propertyValuesHolder>
<keyframe android:fraction="0f" android:value="0f"/>
<keyframe android:fraction="0.5f" android:value="5f"/>
<keyframe android:fraction="1f" android:value="0f"/>
</propertyValuesHolder>
</animator>
三.优秀的动画例子学习
3.1 一个精致的打勾小动画View
https://github.com/ChengangFeng/TickView