上几篇我们讲解了Interpolator的作用和自定义Interpolator的实现方式,这一篇我们通过自定义一个Interpolator曲线展示控件来更加深入的了解Interpolator的原理。
原理
之前我们已经讲过Interpolator的作用原理,我们再来回忆一下:动画在执行过程中会不断地调用Interpolator的getInterpolation方法并传入一个当前动画执行时间进度的值input(0——1之间),Interpolator会根据input值计算出一个动画在input时间进度情况下的动画执行进度的值fraction(一般来说fraction取值为0时,意味着当然动画属性值取值为startValue,取值为1时属性值取值为endValue),当然还需要拿fraction去TypeEvaluator中去获取实际的动画属性值。可以发现Interpolator就是一个将动画执行时间和取值相关联起来的一个对象。
实现
Interpolator的作用原理解释完了,我们来看控件的具体实现过程。
public class InterpolatorView extends View {
private static final String TAG = InterpolatorView.class.getSimpleName();
private Interpolator interpolator;
//Interpolation路径
private Path path;
//路径画笔
private Paint pathPaint;
//边框画笔
private Paint paint;
//表示时间和Interpolator取值的小球画笔
private Paint timeGlobulePaint;
//模拟动画的小球画笔
private Paint animationGlobulePaint;
//文字尺寸
private float textSize = 25f;
//动画时间
private int duration = 3000;
//表示时间的小球的位置
private float timeGlobuleCurrentX = 0;
private float timeGlobuleCurrentY = 0;
//表示时间进度的小球的半径
private static final int TIME_GLOBULE_RADIUS = 10;
//模拟动画的小球的位置
private float animationGlobuleCurrentX = 0;
//模拟动画的小球的半径
private static final int ANIMATION_GLOBULE_RADIUS = 20;
public InterpolatorView(Context context) {
this(context, null, 0);
}
public InterpolatorView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public InterpolatorView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
/**
* 设置Interpolator
* @param interpolator Interpolator
*/
public void setInterpolator(Interpolator interpolator){
this.interpolator = interpolator;
if(interpolator == null){
return;
}
resetGlobuleLocation();
initPath();
}
/**
* 重置小球位置
*/
private void resetGlobuleLocation(){
timeGlobuleCurrentX = 0;
timeGlobuleCurrentY = 0;
animationGlobuleCurrentX = 0;
}
/**
* 开始播放模拟动画效果
*/
public void startAnimation(){
if(interpolator == null){
return;
}
//动画小球的移动动画
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, getWidth());
valueAnimator.setDuration(duration);
valueAnimator.setInterpolator(interpolator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = getHeight();
int width = getWidth();
//X轴表示时间流逝的比值(当前时间 / 总时间 * 绘制宽度)
timeGlobuleCurrentX = (float)width * animation.getCurrentPlayTime() / duration;
//Y轴Interpolator返回值(当前进度下的Interpolator返回值 * 绘制高度)
timeGlobuleCurrentY = height - (interpolator.getInterpolation(animation.getCurrentPlayTime() / (float)duration) * height / 2f + height / 8f);
Log.d(TAG,"绘制小球:x = " + timeGlobuleCurrentX + "---- y = " + timeGlobuleCurrentY);
//动画模拟的是TranslateX的动画效果,getAnimatedValue()获取当前X坐标值
animationGlobuleCurrentX = (float) animation.getAnimatedValue();
Log.d(TAG,"模拟动画小球:x = " + animationGlobuleCurrentX);
invalidate();
}
});
valueAnimator.start();
}
/**
* 开始播放模拟动画效果
*/
public void startAnimation(int duration){
this.duration = duration;
startAnimation();
}
/**
* 设置字体尺寸
* @param textSize 字体尺寸
*/
public void setTextSize(float textSize){
this.textSize = textSize;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(path == null){
initPath();
}
int height = getHeight();
int width = getWidth();
//边框
canvas.drawLine(0f,0f,width, 0, paint);
canvas.drawLine(0f,height,width, height, paint);
canvas.drawLine(0f,0f,0, height, paint);
canvas.drawLine(width,0f,width, height, paint);
//横线
canvas.drawLine(0f,height / 4f,width, height / 4f, paint);
canvas.drawLine(0f,height * 3 / 8f,width, height * 3 / 8f, paint);
canvas.drawLine(0f,height - height / 8f,width, height - height / 8f, paint);
//文字
paint.setTextSize(textSize);
canvas.drawText(" 0",0f, height - height / 8f + 30f, paint);
canvas.drawText(" 1",0f, height * 3 / 8f - 4, paint);
canvas.drawText("t",width - 15f, height - height / 8f + 30f, paint);
//绘制Interpolator路径
canvas.drawPath(path, pathPaint);
//绘制表示时间进度和动画进度关系的小球
if(timeGlobuleCurrentX == 0 && timeGlobuleCurrentY == 0){
timeGlobuleCurrentY = height - height / 8f;
}
canvas.drawOval(timeGlobuleCurrentX - TIME_GLOBULE_RADIUS, timeGlobuleCurrentY - TIME_GLOBULE_RADIUS,
timeGlobuleCurrentX + TIME_GLOBULE_RADIUS, timeGlobuleCurrentY + TIME_GLOBULE_RADIUS, timeGlobulePaint);
//绘制模拟动画的小球
float animationGlobuleCurrentY = height / 8f;
canvas.drawOval(animationGlobuleCurrentX - ANIMATION_GLOBULE_RADIUS, animationGlobuleCurrentY - ANIMATION_GLOBULE_RADIUS,
animationGlobuleCurrentX + ANIMATION_GLOBULE_RADIUS, animationGlobuleCurrentY + ANIMATION_GLOBULE_RADIUS, animationGlobulePaint);
}
/**
* 初始化画笔
*/
private void initPaint(){
//Interpolator路径画笔
pathPaint = new Paint();
pathPaint.setColor(Color.RED);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setStrokeWidth(4);
pathPaint.setAntiAlias(true);
//边框画笔
paint = new Paint();
paint.setColor(Color.GRAY);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(2);
paint.setAntiAlias(true);
paint.setTextSize(30);
//时间与进度小球画笔
timeGlobulePaint = new Paint();
timeGlobulePaint.setColor(Color.BLUE);
timeGlobulePaint.setStyle(Paint.Style.FILL);
timeGlobulePaint.setStrokeWidth(0);
timeGlobulePaint.setAntiAlias(true);
//动画小球画笔
animationGlobulePaint = new Paint();
animationGlobulePaint.setColor(Color.GREEN);
animationGlobulePaint.setStyle(Paint.Style.FILL);
animationGlobulePaint.setStrokeWidth(0);
animationGlobulePaint.setAntiAlias(true);
}
/**
* 初始化Interpolation路径
*/
private void initPath(){
path = new Path();
int width = getWidth();
int height = getHeight();
if(height != 0 && width != 0 && interpolator != null){
path.reset();
//以控件Y轴的3/8到7/8处为坐标系绘制曲线
path.moveTo(0,height - (interpolator.getInterpolation(0) * height / 2f + height / 8f));
//遍历x坐标,模拟时间进度,从interpolator中取值
for(int i = 0; i < width; i++){
path.lineTo(i,height - (interpolator.getInterpolation(i / (float)width) * height / 2f + height / 8f));
}
invalidate();
}
}
}
代码注释的很详细了,应该能够看得懂。大体说一下思路,分为三个步骤:
- 1、坐标系的绘制
- 2、Interpolator变化曲线的绘制
- 3、动画小球的绘制
1、除了控件边框的绘制,我们将控件的Y轴分为两部分,上部1/4处为动画小球模拟动画区,下部分为曲线绘制区,曲线绘制区又在上下各预留了1/8的空间,用来绘制当Interpolator取值小于0和大于1的部分,Interpolator取值变化较大时可能显示不完整。
2、通过遍历X轴坐标,来模拟动画时间进度的变化,从而去Interpolator中获取当前进度值。
3、定义了一个ValueAnimator来模拟动画自动执行过程,这部分应该是最简单的就不解释了,需要注意的是在ValueAnimator中会根据当前动画进度来获取在Interpolator曲线上移动的小球的位置。
至此我们定义了一个相对完整的展示Interpolator变化曲线的控件,当然还有几处瑕疵,比如:当Interpolator取值变化太大时,控件显示不完整等问题,后面有时间再考虑完善的问题。
参考博客:
http://inloop.github.io/interpolator/
http://nightfarmer.github.io/2016/09/12/InterpolatorPreview/