官方提供的Android动画相当丰富,但有时我们需要实现一些特殊的运动轨迹,如缓动曲线时就会束手无策,接下来我们实现一个基于贝
塞尔曲线的自定义动画轨迹
一、ValueAnimator原理
1、Interpolator
Interpolator即插值器,其作用为根据时间计算属性值的变化,它实现了TimeInterpolator接口
Android系统预置的Interpolator的实现类包括:
理解工作原理最好的方法就是:Read the F*cking source code
TimeInterpolator源码
/**
* A time interpolator defines the rate of change of an animation. This allows animations
* to have non-linear motion, such as acceleration and deceleration.
*/
public interface TimeInterpolator {
/**
* Maps a value representing the elapsed fraction of an animation to a value that represents
* the interpolated fraction. This interpolated value is then multiplied by the change in
* value of an animation to derive the animated value at the current elapsed animation time.
*
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);
}
TimeInterpolator接口中规定了一个方法,其接收一个float值,从0开始,至1结束
通过查看线性变化的插值器——LinearInterpolator,可以看出
@HasNativeInterpolator
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createLinearInterpolator();
}
}
getInterpolation()返回值与input相等
加速运动插值器——AcclerateDecelerateInterpolator
@HasNativeInterpolator
public class AccelerateDecelerateInterpolator extends BaseInterpolator
implements NativeInterpolatorFactory {
public AccelerateDecelerateInterpolator() {
}
@SuppressWarnings({"UnusedDeclaration"})
public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
/** @hide */
@Override
public long createNativeInterpolator() {
return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
}
}
同理,其他系统预置插值器均是函数变化的,参考:插值器函数
2、TypeEvaluator
TypeEvaluator即估值器,其作用是根据当前属性改变进度计算改变后的属性,如ValueAnimator.ofFloat()中为了实现初始值到结束值的平滑过渡,系统内置了FloatEvaluator
FloatEvaluator源码
public class FloatEvaluator implements TypeEvaluator {
/**
* This function returns the result of linearly interpolating the start and end values, with
* fraction
representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: result = x0 + t * (v1 - v0)
,
* where x0
is startValue
, x1
is endValue
,
* and t
is fraction
.
*
* @param fraction The fraction from the starting to the ending values
* @param startValue The start value; should be of type float
or
* Float
* @param endValue The end value; should be of type float
or Float
* @return A linear interpolation between the start and end values, given the
* fraction
parameter.
*/
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
FloatEvaluator重写了TypeEvaluator的evaluate方法,其参数分别为fraction 动画完成度 、 startValue 开始值 、 endValue 结束
值 并根据三个值计算改变后的属性
public class IntEvaluator implements TypeEvaluator {
/**
* This function returns the result of linearly interpolating the start and end values, with
* fraction
representing the proportion between the start and end values. The
* calculation is a simple parametric calculation: result = x0 + t * (v1 - v0)
,
* where x0
is startValue
, x1
is endValue
,
* and t
is fraction
.
*
* @param fraction The fraction from the starting to the ending values
* @param startValue The start value; should be of type int
or
* Integer
* @param endValue The end value; should be of type int
or Integer
* @return A linear interpolation between the start and end values, given the
* fraction
parameter.
*/
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
观察IntEvaluator可以看出evaluate计算方法基本相似
3、ValueAnimator机制总结
1、根据时间t及时长duration的比例计算出fraction,传给Interpolator
2、Interpolator根据自己的插值器函数计算出新的fraction,传给TypeEvaluator
3、TypeEvalutor计算出animatedValue
4、控件根据animatedValue,在AnimatorUpdateListener中改变属性值
二、实现Bezier曲线轨迹
贝塞尔曲线相关:贝塞尔曲线开发的艺术
这里我们要使用三阶贝塞尔曲线:
P0为startValue 、 P3为endValue 、 P1、P2为控制点
通过重写TypeEvaluator中evaluate方法实现Bezier曲线轨迹
public class BezierEvaluator implements TypeEvaluator {
private PointF point1;//控制点1
private PointF point2;//控制点2
public BezierEvaluator(PointF point1, PointF point2) {
this.point1 = point1;
this.point2 = point2;
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
final float t = fraction;
float minusT = 1.0f - t;
PointF point = new PointF();
PointF point0 = startValue;
PointF point3 = endValue;
point.x = minusT * minusT * minusT * (point0.x)
+ 3 * minusT * minusT * t * (point1.x)
+ 3 * minusT * t * t * (point2.x)
+ t * t * t * (point3.x);
point.y = minusT * minusT * minusT * (point0.y)
+ 3 * minusT * minusT * t * (point1.y)
+ 3 * minusT * t * t * (point2.y)
+ t * t * t * (point3.y);
return point;
}
}
BezierEvaluator的应用
新建类继承自View
public class TestView extends View implements View.OnTouchListener{
private Paint bPaint;
private Paint pPaint;
private Paint lPaint;
private PointF pointF;
int temp = 0;
private List list = new ArrayList<>();
public TestView(Context context) {
super(context);
init();
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
bPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bPaint.setStyle(Paint.Style.FILL_AND_STROKE);
bPaint.setColor(Color.BLUE);
pPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pPaint.setStyle(Paint.Style.FILL_AND_STROKE);
pPaint.setColor(Color.GRAY);
lPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
lPaint.setStyle(Paint.Style.STROKE);
lPaint.setStrokeWidth(1);
lPaint.setColor(Color.GRAY);
lPaint.setTextSize(20);
this.setOnTouchListener(this);
}
public void startAnimator() {
PointF p0 = list.get(0);
PointF p1 = list.get(1);
PointF p2 = list.get(2);
PointF p3 = list.get(3);
ValueAnimator animator = ValueAnimator.ofObject(
new BezierEvaluator(new PointF(p1.x , p1.y), new PointF(p2.x , p2.y))
,new PointF(p0.x , p0.y), new PointF(p3.x , p3.y));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
pointF = (PointF) animation.getAnimatedValue();
invalidate();
}
});
animator.setDuration(5000);
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for(int i = 0 ; i < list.size() ; i ++){
PointF p = list.get(i);
canvas.drawCircle(p.x , p.y , 5 ,pPaint);
canvas.drawText("[" + (int)p.x + "," + (int)p.y + "]" , 50 , 50 + i * 30 , lPaint);
}
temp ++;
Path path = new Path();
switch (list.size()){
case 2:
canvas.drawLine(list.get(0).x , list.get(0).y , list.get(1).x , list.get(1).y , lPaint);
break;
case 3:
path.moveTo(list.get(0).x , list.get(0).y);
path.quadTo(list.get(1).x , list.get(1).y , list.get(2).x , list.get(2).y);
canvas.drawPath(path , lPaint);
break;
case 4:
path.moveTo(list.get(0).x , list.get(0).y);
path.cubicTo(list.get(1).x , list.get(1).y , list.get(2).x , list.get(2).y , list.get(3).x , list.get(3).y);
canvas.drawPath(path , lPaint);
if(pointF == null){
canvas.drawCircle(list.get(0).x , list.get(0).y , 40 ,bPaint);
startAnimator();
}else{
canvas.drawCircle(pointF.x , pointF.y , 40 , bPaint);
}
break;
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if(list.size() < 4){
PointF pointF = new PointF();
pointF.x = event.getX();
pointF.y = event.getY();
list.add(pointF);
}else if(list.size() == 4){
list.clear();
temp = 0;
pointF = null;
}
invalidate();
return false;
}
}
实现效果
根据以上我们可以做出更实用的东西
public class HeartImageView extends ImageView {
private Bitmap image;
public HeartImageView(Context context) {
this(context, null);
}
public HeartImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
image = BitmapFactory.decodeResource(getResources(), R.drawable.heart);
}
public void setColor(int color) {
setImageBitmap(createColor(color));
}
private Bitmap createColor(int color) {
int heartWidth= image.getWidth();
int heartHeight= image.getHeight();
Bitmap newBitmap=Bitmap.createBitmap(heartWidth, heartHeight, Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(newBitmap);
Paint paint=new Paint();
paint.setAntiAlias(true);
canvas.drawBitmap(image, 0, 0, paint);
canvas.drawColor(color, PorterDuff.Mode.SRC_ATOP);
canvas.setBitmap(null);
return newBitmap;
}
}
public class HeartLayout extends RelativeLayout {
private Context context;
private int width;
private int height;
private int[] colors={Color.RED, Color.YELLOW, Color.GRAY, Color.GREEN, Color.BLUE};
public HeartLayout(Context context) {
this(context , null);
}
public HeartLayout(Context context, AttributeSet attrs) {
this(context, attrs , 0);
}
public HeartLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width=getWidth();
height=getHeight();
}
public void addHeart() {
RelativeLayout.LayoutParams params=new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
params.addRule(RelativeLayout.CENTER_HORIZONTAL);
final HeartImageView imageView=new HeartImageView(context);
imageView.setColor(colors[new Random().nextInt(colors.length)]);
imageView.setVisibility(INVISIBLE);
addView(imageView, params);
imageView.post(new Runnable() {
@Override
public void run() {
int point1x=new Random().nextInt((width));
int point2x=new Random().nextInt((width));
int point1y=new Random().nextInt(height/2)+height/2;
int point2y=point1y-new Random().nextInt(point1y);
int endX=new Random().nextInt(width/2);
int endY=point2y-new Random().nextInt(point2y);
ValueAnimator translateAnimator=ValueAnimator.ofObject(
new BezierEvaluator(new PointF(point1x, point1y), new PointF(point2x, point2y)),
new PointF(width/2-imageView.getWidth()/2, height-imageView.getHeight()),
new PointF(endX, endY));
translateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
PointF pointF= (PointF) animation.getAnimatedValue();
imageView.setX(pointF.x);
imageView.setY(pointF.y);
}
});
translateAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
removeView(imageView);
}
});
TimeInterpolator[] timeInterpolator={new LinearInterpolator(), new AccelerateDecelerateInterpolator(), new DecelerateInterpolator(), new AccelerateInterpolator()};
translateAnimator.setInterpolator(timeInterpolator[new Random().nextInt(timeInterpolator.length)]);
ObjectAnimator translateAlphaAnimator=ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f, 0f);
translateAlphaAnimator.setInterpolator(new DecelerateInterpolator());
AnimatorSet translateAnimatorSet=new AnimatorSet();
translateAnimatorSet.playTogether(translateAnimator, translateAlphaAnimator);
translateAnimatorSet.setDuration(2000);
ObjectAnimator scaleXAnimator=ObjectAnimator.ofFloat(imageView, View.SCALE_X, 0.5f, 1f);
ObjectAnimator scaleYAnimator=ObjectAnimator.ofFloat(imageView, View.SCALE_Y, 0.5f, 1f);
ObjectAnimator alphaAnimator=ObjectAnimator.ofFloat(imageView, View.ALPHA, 0.5f, 1f);
AnimatorSet enterAnimatorSet=new AnimatorSet();
enterAnimatorSet.playTogether(scaleXAnimator, scaleYAnimator, alphaAnimator);
enterAnimatorSet.setDuration(500);
enterAnimatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
imageView.setVisibility(VISIBLE);
}
});
AnimatorSet allAnimator=new AnimatorSet();
allAnimator.playSequentially(enterAnimatorSet, translateAnimatorSet);
allAnimator.start();
}
});
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final HeartLayout layout = (HeartLayout) findViewById(R.id.heart);
layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
layout.addHeart();
return false;
}
});
}
}