一、什么是Android动画
在Android中我们一般将动画分为两类,一类是View Animation(视图动画),一类是Property Animation,当然也有说分为三种,Frame Animation,Tween Animation,和Property Animation,由于前两种一般都是作用于整个View的,所以就统称为View Animation。
二、视图动画
1. 帧动画(Frame Animation)
- 什么是帧动画
是把动画过程中的每张静态图片收集起来,通过android控制,依次显示这些静态图片。
knight_attack.xml - 如何使用
// item = 动画图片资源;duration = 设置一帧持续时间(ms)
public class FrameActivity extends AppCompatActivity {
private Button btn_startFrame,btn_stopFrame;
private ImageView iv;
private AnimationDrawable animationDrawable;
iv = (ImageView) findViewById(R.id.iv);
btn_startFrame = (Button) findViewById(R.id.btn_startFrame);
btn_stopFrame = (Button) findViewById(R.id.btn_stopFrame);
<-- 开始动画 -->
btn_startFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
iv.setImageResource(R.drawable.knight_attack);
// 1. 设置动画
animationDrawable = (AnimationDrawable) iv.getDrawable();
// 2. 获取动画对象
animationDrawable.start();
// 3. 启动动画
}
});
//停止动画
btn_stopFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
iv.setImageResource(R.drawable.knight_attack);
// 1. 设置动画
animationDrawable = (AnimationDrawable) iv.getDrawable();
// 2. 获取动画对象
animationDrawable.stop();
// 3. 暂停动画
}
});
}
}
- 原理&缺陷&应用场景
原理:将动画拆分为 帧 的形式,且定义每一帧 = 每一张图片,逐帧动画的本质:按序播放一组预先定义好的图片
优缺点:简单易用,容易引起OOM,因为使用了大量的图片资源
应用场景:较为复杂的个性化动画效果;使用时一定要避免使用尺寸较大的图片,否则会引起OOM。
2.2 补间动画(Tween Animation)
- 什么是补间动画
补间动画就是开发者只需要制定动画的开始、结束动画的关键帧,中间的帧有系统计算并补齐。 - 平移动画(Transtate):TranstateAnimation类
位移变化的动画。创建该动画师只要制定Deon规划开始的位置(XY表示)结束(XY表示)并制定动画的持续时间。
// 采用 标签表示平移动画
Button mButton = (Button) findViewById(R.id.Button);
// 步骤1:创建 需要设置动画的 视图View
Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
// 步骤2:创建 动画对象 并传入设置的动画效果xml文件
mButton.startAnimation(translateAnimation);
// 步骤3:播放动画
- 缩放动画(Scale):ScaleAnimation类
大小缩放动画:
创建该动画需要制定动画开始的缩放比例(以XY轴的缩放参数表示)、结束时的缩放比(以XY轴的缩放参数表示)并制定动画的持续时间。由于缩放时以不同的点为中心,缩放效果并不相同,通过pointX,pointY设置
// 采用 标签表示是缩放动画
- 旋转动画(Rotate):RotateAnimation类
旋转动画:设置时需要设定开始的旋转角度和结束时的旋转角度,并指定动画的持续时间即可,由于 旋转时以不同的点为中心, 旋转效果并不相同,通过pointX,pointY设置。
// 采用 标签表示是旋转动画
- 透明度动画(Alpha):AlphaAnimation类
指定透明度和持续时间即可
// 采用 标签表示是透明度动画
- 组合动画
// 采用< Set/>标签
// 组合动画同样具备公共属性
android:duration="3000" // 动画持续时间(ms),必须设置,动画才有效果
android:startOffset ="1000" // 动画延迟开始时间(ms)
android:fillBefore = “true” // 动画播放完后,视图是否会停留在动画开始的状态,默认为true
android:fillAfter = “false” // 动画播放完后,视图是否会停留在动画结束的状态,优先于fillBefore值,默认为false
android:fillEnabled= “true” // 是否应用fillBefore值,对fillAfter值无影响,默认为true
android:repeatMode= “restart” // 选择重复播放动画模式,restart代表正序重放,reverse代表倒序回放,默认为restart|
android:repeatCount = “0” // 重放次数(所以动画的播放次数=重放次数+1),为infinite时无限重复
android:interpolator = @[package:]anim/interpolator_resource // 插值器,即影响动画的播放速度,下面会详细讲
// 组合动画独特的属性
android:shareinterpolator = “true”
// 表示组合动画中的动画是否和集合共享同一个差值器
// 如果集合不指定插值器,那么子动画需要单独设置
// 组合动画播放时是全部动画同时开始
// 如果想不同动画不同时间开始就要使用android:startOffset属性来延迟单个动画播放时间
// 设置旋转动画,语法同单个动画
// 设置平移动画,语法同单个动画
// 设置透明度动画,语法同单个动画
// 设置缩放动画,语法同单个动画
// 特别注意:
// 1. 在组合动画里scale缩放动画设置的repeatCount(重复播放)和fillBefore(播放完后,视图是否会停留在动画开始的状态)是无效的。
// 2. 所以如果需要重复播放或者回到原位的话需要在set标签里设置
// 3. 但是由于此处rotate旋转动画里已设置repeatCount为infinite,所以动画不会结束,也就看不到重播和回复原位
Button mButton = (Button) findViewById(R.id.Button);
// 创建 需要设置动画的 视图View
// 组合动画设置
AnimationSet setAnimation = new AnimationSet(true);
// 步骤1:创建组合动画对象(设置为true)
// 步骤2:设置组合动画的属性
// 特别说明以下情况
// 因为在下面的旋转动画设置了无限循环(RepeatCount = INFINITE)
// 所以动画不会结束,而是无限循环
// 所以组合动画的下面两行设置是无效的
setAnimation.setRepeatMode(Animation.RESTART);
setAnimation.setRepeatCount(1);// 设置了循环一次,但无效
// 步骤3:逐个创建子动画(方式同单个动画创建方式,此处不作过多描述)
// 子动画1:旋转动画
Animation rotate = new RotateAnimation(0,360,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotate.setDuration(1000);
rotate.setRepeatMode(Animation.RESTART);
rotate.setRepeatCount(Animation.INFINITE);
// 子动画2:平移动画
Animation translate = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,-0.5f,
TranslateAnimation.RELATIVE_TO_PARENT,0.5f,
TranslateAnimation.RELATIVE_TO_SELF,0
,TranslateAnimation.RELATIVE_TO_SELF,0);
translate.setDuration(10000);
// 子动画3:透明度动画
Animation alpha = new AlphaAnimation(1,0);
alpha.setDuration(3000);
alpha.setStartOffset(7000);
// 子动画4:缩放动画
Animation scale1 = new ScaleAnimation(1,0.5f,1,0.5f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
scale1.setDuration(1000);
scale1.setStartOffset(4000);
// 步骤4:将创建的子动画添加到组合动画里
setAnimation.addAnimation(alpha);
setAnimation.addAnimation(rotate);
setAnimation.addAnimation(translate);
setAnimation.addAnimation(scale1);
mButton.startAnimation(setAnimation);
// 步骤5:播放动画
三、属性动画(任意对象)
1. 什么是属性动画:
属性动画是增强版补间动画
2. 为什么引入属性动画
补间动画作用对象局限:
- a.只能够作用在视图View上,即只可以对一个Button、TextView、甚至是LinearLayout、或者其它继承自View的组件进行动画操作,但无法对非View的对象进行动画操作
- b.没有改变View的属性,只是改变视觉效果
- c. 动画效果单一
3.ValueAnimator 类:
通过不断控制 值 的变化,再不断 手动 赋给对象的属性,从而实现动画效果,属性动画主要的时间引擎,他负责计算各个帧的属性。它定义了属性动画的绝大部分核心功能,包括计算各个帧的相关属性,负责处理更新事件,按属性值的类型控制计算。
a.计算各帧的相关属性;
b.为指定对象设置这些就算后的值。
- 从上面原理可以看出:ValueAnimator类中有3个重要方法:
a. ValueAnimator.ofInt(int values)
b. ValueAnimator.ofFloat(float values)
c. ValueAnimator.ofObject(int values)
3.1 ValueAnimator.ofInt(int values) (将初始值 以整形的形式 过渡到结束值)
// 步骤1:设置动画属性的初始值 & 结束值
ValueAnimator anim = ValueAnimator.ofInt(0, 3);
// ofInt()作用有两个
// 1. 创建动画实例
// 2. 将传入的多个Int参数进行平滑过渡:此处传入0和1,表示将值从0平滑过渡到1
// 如果传入了3个Int参数 a,b,c ,则是先从a平滑过渡到b,再从b平滑过渡到C,以此类推
// ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置,即默认设置了如何从初始值 过渡到 结束值
// 关于自定义插值器我将在下节进行讲解
// 下面看看ofInt()的源码分析 ->>关注1
// 步骤2:设置动画的播放各种属性
anim.setDuration(500);
// 设置动画运行的时长
anim.setStartDelay(500);
// 设置动画延迟播放时间
anim.setRepeatCount(0);
// 设置动画重复播放次数 = 重放次数+1
// 动画播放次数 = infinite时,动画无限重复
anim.setRepeatMode(ValueAnimator.RESTART);
// 设置重复播放动画模式
// ValueAnimator.RESTART(默认):正序重放
// ValueAnimator.REVERSE:倒序回放
// 步骤3:将改变的值手动赋值给对象的属性值:通过动画的更新监听器
// 设置 值的更新监听器
// 即:值每次改变、变化一次,该方法就会被调用一次
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int currentValue = (Integer) animation.getAnimatedValue();
// 获得改变后的值
System.out.println(currentValue);
// 输出改变后的值
// 步骤4:将改变后的值赋给对象的属性值,下面会详细说明
View.setproperty(currentValue);
// 步骤5:刷新视图,即重新绘制,从而实现动画效果
View.requestLayout();
}
});
anim.start();
// 启动动画
}
// 关注1:ofInt()源码分析
public static ValueAnimator ofInt(int... values) {
// 允许传入一个或多个Int参数
// 1. 输入一个的情况(如a):从0过渡到a;
// 2. 输入多个的情况(如a,b,c):先从a平滑过渡到b,再从b平滑过渡到C
ValueAnimator anim = new ValueAnimator();
// 创建动画对象
anim.setIntValues(values);
// 将传入的值赋值给动画对象
return anim;
}
效果:
具备重用性,即将通用的动画写到XML里,可在各个界面中去重用它
- 步骤1:在路径 res/animator的文件夹里创建相应的动画 .xml文件,并设置参数
// ValueAnimator采用 标签
- 步骤2:加载动画
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);
// 载入XML动画
animator.setTarget(view);
// 设置动画对象
animator.start();
// 启动动画
3.2 ValueAnimator.oFloat(float values)(将初始值 以浮点的形式 过渡到结束值)
- 在Java中:
ValueAnimator anim = ValueAnimator.ofFloat(0, 3);
// 其他使用类似ValueAnimator.ofInt(int values),此处不作过多描述
- 在xml中:
// ValueAnimator采用 标签
- 加载代码:
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);
// 载入XML动画
animator.setTarget(view);
// 设置动画对象
animator.start();
- 对比ValueAnimator.ofInt()与ValueAnimator.oFloat()仅仅只是在估值器上的区别:(即如何从初始值 过渡 到结束值)
ValueAnimator.oFloat()采用默认的浮点型估值器 (FloatEvaluator)
ValueAnimator.ofInt()采用默认的整型估值器(IntEvaluator)
3.3 ValueAnimator.ofObject()(将初始值 以对象的形式 过渡到结束值)
- 抽象方法逻辑:
// 创建初始动画时的对象 & 结束动画时的对象
myObject object1 = new myObject();
myObject object2 = new myObject();
ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);
// 创建动画对象 & 设置参数
// 参数说明
// 参数1:自定义的估值器对象(TypeEvaluator 类型参数) - 下面会详细介绍
// 参数2:初始动画的对象
// 参数3:结束动画的对象
anim.setDuration(5000);
anim.start();
- 两种数值器的认识
插值器(Interpolator)决定 值 的变化模式/变化的快慢(匀速、加速.......)
估值器(TypeEvaluator)决定 值 的具体变化数值 - ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具备系统内置的估值器,即FloatEvaluator & IntEvaluator
即系统已经默认实现了 如何从初始值 过渡到 结束值 的逻辑
源码解析:
public class FloatEvaluator implements TypeEvaluator {
// FloatEvaluator实现了TypeEvaluator接口
// 重写evaluate()
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
float startFloat = ((Number) startValue).floatValue();
return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
// 初始值 过渡 到结束值 的算法是:
// 1. 用结束值减去初始值,算出它们之间的差值
// 2. 用上述差值乘以fraction系数
// 3. 再加上初始值,就得到当前动画的值
}
}
- 步骤1:定义对象类
package nfc.gz.com.myanimator;
/**
* @author Wudi
* @date 2019/1/7
*/
public class Point {
private float x;
private float y;
private int color;
public Point(float x,float y) {
this.x=x;
this.y=y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
}
- 步骤2:根据需求实现TypeEvaluator接口
// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
// 根据fraction来计算当前动画的x和y的值
float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
// 将计算后的坐标封装到一个新的Point对象中并返回
Point point = new Point(x, y);
return point;
}
}
- 步骤3:将属性动画作用到自定义View当中
package nfc.gz.com.myanimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
/**
* @author Wudi
* @date 2019/1/7
*/
public class PointView extends View {
public static final float RADIUS = 70f;
private Point currentPoint;
private Paint mPaint;
public PointView(Context context) {
super(context);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.YELLOW);
}
public PointView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.YELLOW);
}
public PointView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.YELLOW);
}
@Override
protected void onDraw(Canvas canvas) {
// 如果当前点坐标为空(即第一次)
if (currentPoint == null) {
currentPoint = new Point(RADIUS, RADIUS);
// 创建一个点对象(坐标是(70,70))
// 在该点画一个圆:圆心 = (70,70),半径 = 70
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, RADIUS, mPaint);
// (重点关注)将属性动画作用到View中
// 步骤1:创建初始动画时的对象点 & 结束动画时的对象点
// 初始点为圆心(70,70)
Point startPoint = new Point(RADIUS, RADIUS);
// 结束点为(700,1000)
Point endPoint = new Point(700, 1000);
// 步骤2:创建动画对象 & 设置初始值 和 结束值
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
// 参数说明
// 参数1:TypeEvaluator 类型参数 - 使用自定义的PointEvaluator(实现了TypeEvaluator接口)
// 参数2:初始动画的对象点
// 参数3:结束动画的对象点
// 步骤3:设置动画参数
anim.setDuration(5000);
// 设置动画时长
// 步骤3:通过 值 的更新监听器,将改变的对象手动赋值给当前对象
// 此处是将 改变后的坐标值对象 赋给 当前的坐标值对象
// 设置 值的更新监听器
// 即每当坐标值(Point对象)更新一次,该方法就会被调用一次
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
// 将每次变化后的坐标值(估值器PointEvaluator中evaluate()返回的Piont对象值)到当前坐标值对象(currentPoint)
// 从而更新当前坐标值(currentPoint)
// 步骤4:每次赋值后就重新绘制,从而实现动画效果
invalidate();
// 调用invalidate()后,就会刷新View,即才能看到重新绘制的界面,即onDraw()会被重新调用一次
// 所以坐标值每改变一次,就会调用onDraw()一次
}
});
anim.start();
// 启动动画
} else {
// 如果坐标值不为0,则画圆
// 所以坐标值每改变一次,就会调用onDraw()一次,就会画一次圆,从而实现动画效果
// 在该点画一个圆:圆心 = (30,30),半径 = 30
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x, y, RADIUS, mPaint);
}
}
}
- 从上面可以看出,其实ValueAnimator.ofObject()的本质还是操作 * 值 ,只是是采用将 多个值 封装到一个对象里的方式 同时对多个值一起操作而已
就像上面的例子,本质还是操作坐标中的x,y两个值,只是将其封装到Point对象里,方便同时操作x,y两个值而已
4. ObjectAnimator类
4.1基本使用方法和原理
如直接改变 View的 alpha 属性 从而实现透明度的动画效果
继承自ValueAnimator类,即底层的动画实现机制是基于ValueAnimator类
本质原理: 通过不断控制 值 的变化,再不断 自动 赋给对象的属性,从而实现动画效果。
- ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;
- ObjectAnimator 类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作
在java中:
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);
// ofFloat()作用有两个
// 1. 创建动画实例
// 2. 参数设置:参数说明如下
// Object object:需要操作的对象
// String property:需要操作的对象的属性
// float ....values:动画初始值 & 结束值(不固定长度)
// 若是两个参数a,b,则动画效果则是从属性的a值到b值
// 若是三个参数a,b,c,则则动画效果则是从属性的a值到b值再到c值
// 以此类推
// 至于如何从初始值 过渡到 结束值,同样是由估值器决定,此处ObjectAnimator.ofFloat()是有系统内置的浮点型估值器FloatEvaluator,同ValueAnimator讲解
anim.setDuration(500);
// 设置动画运行的时长
anim.setStartDelay(500);
// 设置动画延迟播放时间
anim.setRepeatCount(0);
// 设置动画重复播放次数 = 重放次数+1
// 动画播放次数 = infinite时,动画无限重复
anim.setRepeatMode(ValueAnimator.RESTART);
// 设置重复播放动画模式
// ValueAnimator.RESTART(默认):正序重放
// ValueAnimator.REVERSE:倒序回放
animator.start();
// 启动动画
在xml中:
// ObjectAnimator 采用 标签
Animator animator = AnimatorInflater.loadAnimator(context, R.animator.view_animation);
// 载入XML动画
animator.setTarget(view);
// 设置动画对象
animator.start();
// 启动动画
即在ObjectAnimator.ofFloat()的第二个参数String property传入alpha、rotation、translationX 和 scaleY 等blabla
- 而自动赋给对象的属性的本质是调用该对象属性的set() & get()方法进行赋值
- 所以ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值,如上面的例子:
自动赋值源码:
/ 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);
anim.setDuration(500);
animator.start();
// 启动动画,源码分析就直接从start()开始
<-- start() -->
@Override
public void start() {
AnimationHandler handler = sAnimationHandler.get();
if (handler != null) {
// 判断等待动画(Pending)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉
numAnims = handler.mPendingAnimations.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
// 判断延迟动画(Delay)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉
numAnims = handler.mDelayedAnims.size();
for (int i = numAnims - 1; i >= 0; i--) {
if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
}
super.start();
// 调用父类的start()
// 因为ObjectAnimator类继承ValueAnimator类,所以调用的是ValueAnimator的star()
// 经过层层调用,最终会调用到 自动赋值给对象属性值的方法
// 下面就直接看该部分的方法
}
<-- 自动赋值给对象属性值的逻辑方法 ->>
// 步骤1:初始化动画值
private void setupValue(Object target, Keyframe kf) {
if (mProperty != null) {
kf.setValue(mProperty.get(target));
// 初始化时,如果属性的初始值没有提供,则调用属性的get()进行取值
}
kf.setValue(mGetter.invoke(target));
}
}
// 步骤2:更新动画值
// 当动画下一帧来时(即动画更新的时候),setAnimatedValue()都会被调用
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
// 内部调用对象该属性的set()方法,从而从而将新的属性值设置给对象属性
}
}
自动赋值的逻辑:
- 初始化时,如果属性的初始值没有提供,则调用属性的 get()进行取值;
- 当 值 变化时,用对象该属性的 set()方法,从而从而将新的属性值设置给对象属性。
所以:
- ObjectAnimator 类针对的是任意对象 & 任意属性值,并不是单单针对于View对象
如果需要采用ObjectAnimator 类实现动画效果,那么需要操作的对象就必须有该属性的set() & get() - 同理,针对上述另外的三种基本动画效果,View 也存在着setRotation()、getRotation()、setTranslationX()、getTranslationX()、setScaleY()、getScaleY()等set() & get() 。
4.2通过自定义对象属性实现动画效果
- 步骤1:设置对象类属性的set() & get()方法
a.通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set()
b. 通过包装原始动画对象,间接给对象加上该属性的 get()&
set()。即 用一个类来包装原始对象
public class MyView2 extends View {
// 设置需要用到的变量
public static final float RADIUS = 100f;// 圆的半径 = 100
private Paint mPaint;// 绘图画笔
private String color;
// 设置背景颜色属性
// 设置背景颜色的get() & set()方法
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
// 将画笔的颜色设置成方法参数传入的颜色
invalidate();
// 调用了invalidate()方法,即画笔颜色每次改变都会刷新视图,然后调用onDraw()方法重新绘制圆
// 而因为每次调用onDraw()方法时画笔的颜色都会改变,所以圆的颜色也会改变
}
// 构造方法(初始化画笔)
public MyView2(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
// 复写onDraw()从而实现绘制逻辑
// 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
@Override
protected void onDraw(Canvas canvas) {
canvas.drawCircle(500, 500, RADIUS, mPaint);
}
}
- 步骤2:在布局文件加入自定义View控件
- 步骤3:根据需求实现TypeEvaluator接口
public class ColorEvaluator implements TypeEvaluator {
// 实现TypeEvaluator接口
private int mCurrentRed;
private int mCurrentGreen ;
private int mCurrentBlue ;
// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑:此处是写颜色过渡的逻辑
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
// 获取到颜色的初始值和结束值
String startColor = (String) startValue;
String endColor = (String) endValue;
// 通过字符串截取的方式将初始化颜色分为RGB三个部分,并将RGB的值转换成十进制数字
// 那么每个颜色的取值范围就是0-255
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);
// 将初始化颜色的值定义为当前需要操作的颜色值
mCurrentRed = startRed;
mCurrentGreen = startGreen;
mCurrentBlue = startBlue;
// 计算初始颜色和结束颜色之间的差值
// 该差值决定着颜色变化的快慢:初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢;否则,变化则很快
// 具体如何根据差值来决定颜色变化快慢的逻辑写在getCurrentColor()里.
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);
// getCurrentColor()决定如何根据差值来决定颜色变化的快慢 ->>关注1
} 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);
// 由于我们计算出的颜色是十进制数字,所以需要转换成十六进制字符串:调用getHexString()->>关注2
// 最终将RGB颜色拼装起来,并作为最终的结果返回
return currentColor;
}
// 关注1:getCurrentColor()
// 具体是根据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;
}
// 关注2:将10进制颜色值转换成16进制。
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}
}
- 步骤4:调用ObjectAnimator.ofObject()方法
public class MainActivity extends AppCompatActivity {
MyView2 myView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myView2 = (MyView2) findViewById(R.id.MyView2);
ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "color", new ColorEvaluator(),
"#0000FF", "#FF0000");
// 设置自定义View对象、背景颜色属性值 & 颜色估值器
// 本质逻辑:
// 步骤1:根据颜色估值器不断 改变 值
// 步骤2:调用set()设置背景颜色的属性值(实际上是通过画笔进行颜色设置)
// 步骤3:调用invalidate()刷新视图,即调用onDraw()重新绘制,从而实现动画效果
anim.setDuration(8000);
anim.start();
}
}
- 特别注意:如何手动设置对象类属性的 set() & get()
- 对象必须要提供属性a的set()方法
a. 如果没传递初始值,那么需要提供get()方法,因为系统要去拿属性a的初始值
b. 若该条件不满足,程序直接Crash
2.对象提供的 属性a的set()方法 对 属性a的改变 必须通过某种方法反映出来
a. 如带来ui上的变化
b. 若这条不满足,动画无效,但不会Crash)
eg:由于View的setWidth()并不是设置View的宽度,而是设置View的最大宽度和最小宽度的;所以通过setWidth()无法改变控件的宽度;所以对View视图的width做属性动画没有效果
Button mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
// 此Button的宽高设置具体为具体宽度200px
ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
// 设置动画的对象
- 解决方案
手动设置对象类属性的set() & get()。共有两种方法:
1.通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set()
2.通过包装原始动画对象,间接给对象加上该属性的 get()& set()。即用一个类来包装原始对象
public class MainActivity extends AppCompatActivity {
Button mButton;
ViewWrapper wrapper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.Button);
// 创建动画作用对象:此处以Button为例
wrapper = new ViewWrapper(mButton);
// 创建包装类,并传入动画作用的对象
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
// 设置动画的对象是包装类的对象
}
});
}
// 提供ViewWrapper类,用于包装View对象
// 本例:包装Button对象
private static class ViewWrapper {
private View mTarget;
// 构造方法:传入需要包装的对象
public ViewWrapper(View target) {
mTarget = target;
}
// 为宽度设置get() & set()
public int getWidth() {
return mTarget.getLayoutParams().width;
}
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
}