@TryLoveCatch
2022-05-17T10:35:39.000000Z
字数 11962
阅读 1373
Android知识体系
属性动画,默认持续时间为300ms
,默认帧率为10ms/帧
HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)
- ObjectAnimator的translationX和translationY动画,传入的值,都是基于View本身的位置来算的
- 在xml里面,android:translationX和android:translationY来设置view的位置
我们可以将TimeInterpolator和TypeEvaluator看作是工厂流水线上的两个小员工,那么ValueAnimator就是车间主管啦。TimeInterpolator这个小员工面对的是产品的半成品,他负责控制半成品输出到下一个生产线的速度。而下一个生产线上的小员工TypeEvaluator的任务就是打磨半成品得到成品,最后将成品输出。
插值器:
TimeInterpolator
,根据时间流逝的百分比,来计算当前属性值需要改变的百分比。
public interface TimeInterpolator {
float getInterpolation(float input);
}
估值器:
TypeEvaluator
,根据当前属性需要改变的百分比,来计算改变后的属性值。
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
它们两个决定了动画的变化过程,举个例子说明:
如上图所示,它表示一个匀速动画,采用LinearInterpolator
和IntEvaluator
,在40ms
内,View
的x
属性从0
到40
的变化。
我们来分析一下,由于默认帧率为10ms/帧
,所以这个动画,应该有5帧
,我们来看第三帧
是如何计算出x=20
的。当t=20ms
时,时间流逝的百分比就是0.5(20/40=0.5)
,然后我们根据插值器
来计算属性x
需要改变的百分比,由于我们用的是LinearInterpolator
,源码如下:
public class LinearInterpolator implements Interpolator {
public LinearInterpolator() {
}
public float getInterpolation(float input) {
return input;
}
}
所以属性x
需要改变的百分比也是0.5
,这样插值器
的作用就结束了,接下来就是估值器
,来得到属性x
具体的值,我们使用的是IntEvaluator
,源码如下:
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
具体来说下evaluate()
的三个参数:
fraction
:估值小数,就是我们在插值器里面计算的结果,这里就是0.5
startValue
:开始值,这里就是0
endValue
:结束值,这里就是40
所以,根据这个方法的算法,0+0.5*(40-0)=20
,得到x=20
。
当数学遇上动画:讲述 ValueAnimator、TypeEvaluator 和 TimeInterpolator 之间的恩恩怨怨 (1)
TimeInterpolator,时间插值器,根据时间流逝的百分比计算出当前属性值改变的百分比。
TimeInterpolator
public interface TimeInterpolator {
float getInterpolation(float input);
}
这里面还有一个接口Interpolator
public interface Interpolator extends TimeInterpolator {
}
Interpolator竟然是继承TimeInterpolator,是不是有点奇怪?
其实,是这样的:
从Android 1.0版本开始就一直存在Interpolator接口了,而之前的补间动画当然也是支持这个功能的。只不过在属性动画中新增了一个TimeInterpolator接口,这个接口是用于兼容之前的Interpolator的,这使得所有过去的Interpolator实现类都可以直接拿过来放到属性动画当中使用
我们来看回TimeInterpolator,里面只有一个方法
float getInterpolation(float input);
input参数是由系统计算传入的,变化很有规律,就是根据动画的时长从0到1匀速增加。
input指的是真实的时间,如果动画的总时长是10s,已经过了4s了,那么input=0.4,所以返回值按理说,取值范围是[0,1],0表示动画开始,1表示动画结束,但是也可以小于0(”下冲”)或者大于1(”上冲”)。
我们都知道时间是一秒一秒过去的,也就是线性的,匀速前进的。如果属性值从起始值到结束值是匀速变化的话,那么整个动画看起来就是慢慢地均匀地变化着。但是,如果我们想让动画变得很快或者变得很慢怎么办?答案是我们可以通过“篡改时间”来完成这个任务!这正是TimeInterpolator类的工作,它实际上就是一条函数曲线。
举个栗子!如下图所示,x轴表示时间的比率,y轴表示属性值。在不考虑TypeEvaluator的计算的情况下,假设属性值是从0变化到1,默认情况下线性插值器就和曲线y=x一样,在时间t的位置上的值为f(t)=t,当t=0.5的时刻传给TypeEvaluator的是t=0.5的时刻的值0.5。但是,当我们将TimeInterpolator设置为函数y=x^2或者y=x^(0.5)时,动画的效果就截然不同啦。在t=0.5的时刻,y=x^2=0.25 < 0.5,表示它将时间推迟了,传给TypeEvaluator的是0.25时刻的值0.25;而y=x^(0.5)=0.71 > 0.5,表示它将时间提前了,传给TypeEvaluator的是0.71时刻的值0.71。
仔细观察曲线的斜率不难发现,曲线y=x^2的斜率在不断增加,说明变化越来越快,作用在View组件上就是刚开始挺慢的,然后不断加速的效果;而曲线y=x^(0.5)的斜率在不断减小,说明变化越来越慢,作用在View组件上就是刚开始挺快的,然后不断减速的效果。
TypeEvaluator,类型估值算法,根据当前属性改变的百分比来计算改变后的属性值。
TypeEvaluator
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
TypeEvaluator实际上也是一条函数曲线,它的输入是TimeInterpolator传进来的被“篡改”了的时间比率,还有动画的起始值和结束值信息,输出就是动画当前应该更新的属性值。假设TimeInterpolator是LinearInterpolator(f(t)=t),也就是说时间比率不被“篡改”的话,那么ValueAnimator对应的函数其实就简化成了TypeEvaluator函数(F=g(x,a,b)=g(f(t),a,b)=g(t,a,b)),即动画实际上只由TypeEvaluator来控制。
假设TimeInterpolator是x=f(t),而TypeEvaluator的函数是y=g(x,a,b),那么,结合而成的复合函数 F=g(f(t),a,b),就是ValueAnimator
假设TimeInterpolator是LinearInterpolator(线性插值器,f(t)=t),也就是说时间比率不被“篡改”的话,那么ValueAnimator对应的函数其实就简化成了TypeEvaluator函数(F=g(x,a,b)=g(f(t),a,b)=g(t,a,b)),即动画实际上只由TypeEvaluator来控制。
假设TypeEvaluator是“LinearTypeEvaluator”(线性估值器,并没有这个说法,所以加上引号,计算方式就是g(x,a,b)=a+x*(b-a))的话,那么ValueAnimator对应的函数也可以简化,F=g(x,a,b)=g(f(t),a,b)=a+f(t)*(b-a),即动画实际上只由TimeInterpolator来控制。
TimeInterpolator是用来控制动画速度的,而TypeEvaluator是用来控制动画中值的变化曲线的。
虽然它们本质的作用是不同的,但是在某些定制的情况下,上面说的两种特殊情况下的ValueAnimator所产生的动画效果是一样的!
也就是说,为了实现某一个动画,我们可以单独使用TimeInterpolator,或者单独使用TypeEvaluator,都可以成功实现。
实现在1s中内将float类型的数值从0变化到1。
ValueAnimator animator1 = new ValueAnimator();
animator1.setFloatValues(0.0f, 1.0f);
animator1.setDuration(1000);
animator1.setInterpolator(new LinearInterpolator());//传入null也是LinearInterpolator
animator1.setEvaluator(new TypeEvaluator() {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
return startValue + fraction * (endValue - startValue);
}
});
animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.e("demo 1", "" + animation.getAnimatedValue());
}
});
// =================================================================
ValueAnimator animator2 = new ValueAnimator();
animator2.setFloatValues(0.0f, 1.0f);
animator2.setDuration(1000);
animator2.setInterpolator(new Interpolator() {
@Override
public float getInterpolation(float input) {
return input;
}
});
animator2.setEvaluator(new TypeEvaluator() {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
return fraction;
}
});
animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Log.e("demo 2", "" + animation.getAnimatedValue());
}
});
animator1.start();
animator2.start();
两个ValueAnimator的在真实时间序列中的输出结果是一样的,所以说,在特殊的单独控制动画的情况下,TimeInterpolator和TypeEvaluator对于制作动画有着殊途同归的作用。
当然,你可以同时使用自定义的TimeInterpolator和自定义的TypeEvaluator结合来控制动画,但是很显然,这种情况下的动画不容易控制。
从数学中函数的角度上来说那就是复合函数肯定比简单函数复杂,我们解决问题的时候要可能化繁为简,所以自然会考虑将ValueAnimator这个复杂函数简化成特殊情况下的简单函数TimeInterpolator或者TypeEvaluator来处理。
上面说了,TimeInterpolator和TypeEvaluator对于制作动画有着殊途同归的作用。而且上面的例子,我们也看出来了,自定义imeInterpolator和自定义TypeEvaluator的代码基本上是一样的,那么我们是不是可以把这两个进行统一处理呢?
yava,这个项目就实现了这个需求,我们来看一下它里面3个重要的类。
IFunction接口
/**
* 函数接口:给定输入,得到输出
*/
public interface IFunction {
float getValue(float input);
}
AbstractFunction抽象类
/**
* 抽象函数实现
*/
public abstract class AbstractFunction implements IFunction, Interpolator, TypeEvaluator<Float> {
@Override
public float getInterpolation(float input) {
return getValue(input);
}
@Override
public Float evaluate(float fraction, Float startValue, Float endValue) {
return startValue + getValue(fraction) * (endValue - startValue);
}
}
这个类,既可以当做简单函数使用,也可以当做Interpolator或者TypeEvaluator去用于制作动画。
EasingFunction枚举:包含了30个常见的缓动函数
public enum EasingFunction implements IFunction, Interpolator, TypeEvaluator<Float> {
/* ------------------------------------------------------------------------------------------- */
/* BACK
/* ------------------------------------------------------------------------------------------- */
BACK_IN {
@Override
public float getValue(float input) {
return input * input * ((1.70158f + 1) * input - 1.70158f);
}
},
BACK_OUT {
@Override
public float getValue(float input) {
return ((input = input - 1) * input * ((1.70158f + 1) * input + 1.70158f) + 1);
}
},
BACK_INOUT {
@Override
public float getValue(float input) {
float s = 1.70158f;
if ((input *= 2) < 1) {
return 0.5f * (input * input * (((s *= (1.525f)) + 1) * input - s));
}
return 0.5f * ((input -= 2) * input * (((s *= (1.525f)) + 1) * input + s) + 2);
}
},
//other easing functions ......
//如果这个function在求值的时候需要duration作为参数的话,那么可以通过setDuration来设置,否则使用默认值
private float duration = 1000f;//目前只有ELASTIC***这三个是需要duration的,其他的都不需要
public float getDuration() {
return duration;
}
public EasingFunction setDuration(float duration) {
this.duration = duration;
return this;
}
//将Function当做Interpolator使用,默认的实现,不需要枚举元素去重新实现
@Override
public float getInterpolation(float input) {
return getValue(input);
}
//将Function当做TypeEvaluator使用,默认的实现,不需要枚举元素去重新实现
@Override
public Float evaluate(float fraction, Float startValue, Float endValue) {
return startValue + getValue(fraction) * (endValue - startValue);
}
//几个数学常量
public static final float PI = (float) Math.PI;
public static float TWO_PI = PI * 2.0f;
public static float HALF_PI = PI * 0.5f;
}
注意,枚举跟抽象方法配合使用。
这个类提供类常见的30个缓动函数的实现,类似于工具类,方便使用。
ObjectAnimator animator1 = new ObjectAnimator();
animator1.setTarget(textView1);
animator1.setPropertyName("translationY");
animator1.setFloatValues(0f, -100f); animator1.setDuration(1000);
animator1.setInterpolator(new LinearInterpolator());
animator1.setEvaluator(EasingFunction.BOUNCE_OUT);//这里将EasingFunction.BOUNCE_OUT作为TypeEvaluator来使用
animator1.start();
ObjectAnimator animator2 = new ObjectAnimator();
animator2.setTarget(textView2);
animator2.setPropertyName("translationY");
animator2.setFloatValues(0f, -100f);
animator2.setDuration(1000);
animator2.setInterpolator(EasingFunction.BOUNCE_OUT); //这里将EasingFunction.BOUNCE_OUT作为Interpolator来使用
animator2.setEvaluator(new FloatEvaluator());
animator2.start();
ObjectAnimator animator1 = new ObjectAnimator();
animator1.setTarget(textView1);
animator1.setPropertyName("translationY");
animator1.setFloatValues(0f, -100f); animator1.setDuration(1000);
animator1.setInterpolator(new LinearInterpolator());
animator1.setEvaluator(new AbstractFunction() {
//自定义为TypeEvaluator
@Override
public float getValue(float input) {
return input * 2 + 3;
}
});
animator1.start();
ObjectAnimator animator2 = new ObjectAnimator();
animator2.setTarget(textView1);
animator2.setPropertyName("translationY");
animator2.setFloatValues(0f, -100f); animator1.setDuration(1000);
animator2.setInterpolator(new AbstractFunction() {
//自定义为TypeEvaluator
@Override
public float getValue(float input) {
return input * 2 + 3;
}
});
animator2.setEvaluator(new FloatEvaluator());
animator2.start();
Canvas的动画顺序是从后往前,是倒着来的!!很重要!!!!
Matrix调用一系列set,pre,post方法时,可视为将这些方法插入到一个队列.当然,按照队列中从头至尾的顺序调用执行.
其中pre表示在队头插入一个方法,post表示在队尾插入一个方法.而set表示把当前队列清空,并且总是位于队列的最中间位置.当执行了一次set后:pre方法总是插入到set前部的队列的最前面,post方法总是插入到set后部的队列的最后面
Matrix m = new Matrix();
m.setRotate(45);
m.setTranslate(80, 80);
只有
m.setTranslate(80, 80)
有效,因为m.setRotate(45);
被清除.
Matrix m = new Matrix();
m.setTranslate(80, 80);
m.postRotate(45);
先执行
m.setTranslate(80, 80);
后执行m.postRotate(45);
Matrix m = new Matrix();
m.setTranslate(80, 80);
m.preRotate(45);
先执行
m.preRotate(45);
后执行m.setTranslate(80, 80);
Matrix m = new Matrix();
m.preScale(2f,2f);
m.preTranslate(50f, 20f);
m.postScale(0.2f, 0.5f);
m.postTranslate(20f, 20f);
执行顺序:
m.preTranslate(50f, 20f)
-->m.preScale(2f,2f)
-->m.postScale(0.2f, 0.5f)
-->m.postTranslate(20f, 20f)
注意:m.preTranslate(50f, 20f)
比m.preScale(2f,2f)
先执行,因为它查到了队列的最前端.
Matrix m = new Matrix();
m.postTranslate(20, 20);
m.preScale(0.2f, 0.5f);
m.setScale(0.8f, 0.8f);
m.postScale(3f, 3f);
m.preTranslate(0.5f, 0.5f);
执行顺序:
m.preTranslate(0.5f, 0.5f)
-->m.setScale(0.8f, 0.8f)
-->m.postScale(3f, 3f)
注意:m.setScale(0.8f, 0.8f)
清除了前面的m.postTranslate(20, 20)
和m.preScale(0.2f, 0.5f)
;
Canvas.setMatrix(matrix)
:用 Matrix
直接替换 Canvas
当前的变换矩阵,即抛弃 Canvas
当前的变换,改用 Matrix
的变换(注:不同的系统中 setMatrix(matrix)
的行为可能不一致,所以还是尽量用 concat(matrix)
吧);Canvas.concat(matrix)
:用 Canvas
当前的变换矩阵和 Matrix
相乘,即基于 Canvas
当前的变换,叠加上 Matrix
中的变换。三维变换,我们来说下Camera
的坐标系
图中黄色圆点就是虚拟相机的位置,其他标识都是x、y、z轴的旋转方向。
三维旋转,rotateX(deg)
、rotateY(deg)
、rotateZ(deg)
、rotate(x, y, z)
接下来说一个例子,原图如下:
canvas.save();
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
另外,Camera
和 Canvas
一样也需要保存和恢复状态才能正常绘制,不然在界面刷新之后绘制就会出现问题。所以完整的代码应该是这样的:
canvas.save();
camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore(); // 恢复 Camera 的状态
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
效果如下:
我们发现,旋转的结果不是堆成的。这是因为,Camera
的旋转操作都是基于原点的,而且不能改变,所以我们需要借助canvas
的translate()
方法,如下:
canvas.save();
camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
canvas.translate(centerX, centerY); // 旋转之后把投影移动回来
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
canvas.translate(-centerX, -centerY); // 旋转之前把绘制内容移动到轴心(原点)
camera.restore(); // 恢复 Camera 的状态
canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();
Canvas 的几何变换顺序是反的,所以要把移动到中心的代码写在下面,把从中心移动回来的代码写在上面。
设置虚拟相机的位置
注意!这个方法有点奇葩,它的参数的单位不是像素,而是 inch
,英寸。 英寸和像素的换算单位被写死为了 72 像素。在 Camera
中,相机的默认位置是 (0, 0, -8)(英寸)
。8 x 72 = 576
,所以它的默认位置是 (0, 0, -576)(像素)
。
这个方法主要是用来解决这个问题:
如果绘制的内容过大,当它翻转起来的时候,就有可能出现图像投影过大的「糊脸」效果。而且由于换算单位被写死成了
72像素
,而不是和设备dpi
相关的,所以在像素越大的手机上,这种「糊脸」效果会越明显。
解决方法就是:使用 setLocation()
方法来把相机往后移动