Android 屬性動畫-繪制貝塞爾曲線路徑
以前對屬性動畫的知識,隻是停留在值動畫和一般的移動、漸變、縮放,原來它還可以自定義,利用反射來回調自己的方法,真是設計的6
而且一直想了解路徑動畫是怎麼計算路徑的,看了别人的demo終于明白了,做下記錄和分析。
1、效果圖如下:
首先,來補充一下知識點,屬性動畫的設計原理
ObjectAnimator extends ValueAnimator屬性動畫內建值動畫
值動畫就是在指定時間内,把一個初始值根據估值器(變化規則)變成結束值
看下面的例子就會明白
ObjectAnimator objectAnimator = ObjectAnimator.ofInt(new MyObj(btn),"setX",0,300);//這裡一般我們會傳入view對象,
但是現在傳入一個我們自定義的對象
objectAnimator.setDuration(5000);
class MyObj{
private View view;
public MyObj(View view){
this.view =view;
}
// 可以看到我們這裡的方法,和熟悉“setX”很像,就是利用反射來的,如果我們以前傳入“alpha”,就是調用view的setAlpha方法設定屬性
// 對于參數int x,這裡就和我們傳入的0,300對應,當然,還可以自定義估值器,和變化的對象,這裡參數也就一一對應
public void setSetX(int x){
view.setTranslationX(x);
}
}
補充完了之後,再來看重點,路徑動畫關鍵在于路徑是怎麼計算的,下面是看了别人的,改了一點點
1、自定義一個viewpath,包含viewpath規定的幾個類型,這幾個類型是用來描述點的,後面就會知道
public class ViewPath {
public static final int MOVE = 0;
public static final int LINE = 1;
public static final int QUAD = 2;
public static final int CURVE = 3;
private ArrayList<ViewPoint> mPoints;
public ViewPath() {
mPoints = new ArrayList<>();
}
public void moveTo(float x, float y){
mPoints.add(ViewPoint.moveTo(x,y,MOVE));
}
public void lineTo(float x,float y){
mPoints.add(ViewPoint.lineTo(x,y,LINE));
}
public void curveTo(float x,float y,float x1,float y1,float x2,float y2){
mPoints.add(ViewPoint.curveTo(x,y,x1,y1,x2,y2,CURVE));
}
public void quadTo(float x,float y,float x1,float y1){
mPoints.add(ViewPoint.quadTo(x,y,x1,y1,QUAD));
}
public Collection<ViewPoint> getPoints(){
return mPoints;
}
}
//這裡的viewpoint,如果是普通點,那麼他隻有x,y,如果是二階貝塞爾曲線的點,x,y就是控制點,x1,y1就是結束點,
如果是三階貝塞爾曲線的點,(x,y)(x1,y1)就是控制點,x2,y2就是結束點。
public class ViewPoint {
float x ,y;
float x1,y1;
float x2,y2;
int operation;
public ViewPoint() {
}
public ViewPoint(float x, float y) {
this.x = x;
this.y = y;
}
public static ViewPoint moveTo(float x, float y, int operation){
return new ViewPoint(x,y,operation);
}
public static ViewPoint lineTo(float x, float y, int operation){
return new ViewPoint(x,y,operation);
}
public static ViewPoint curveTo(float x, float y,float x1,float y1,float x2,float y2, int operation){
return new ViewPoint(x,y,x1,y1,x2,y2,operation);
}
public static ViewPoint quadTo(float x, float y,float x1,float y1, int operation){
return new ViewPoint(x,y,x1,y1,operation);
}
private ViewPoint(float x, float y, int operation) {
this.x = x;
this.y = y;
this.operation = operation;
}
public ViewPoint(float x, float y, float x1, float y1, int operation) {
this.x = x;
this.y = y;
this.x1 = x1;
this.y1 = y1;
this.operation = operation;
}
public ViewPoint(float x, float y, float x1, float y1, float x2, float y2, int operation) {
this.x = x;
this.y = y;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.operation = operation;
}
}
關鍵的來了,估值器(多個點,分别和開始點和結束點,根據結束點來計算規則)
public class ViewPathEvaluator implements TypeEvaluator<ViewPoint> {
public ViewPathEvaluator() {
}
@Override
public ViewPoint evaluate(float t, ViewPoint startValue, ViewPoint endValue) {
float x ,y;
float startX,startY;
//判斷結束點的類型,根據後一個點類型,來計算開始點和結束點的變化
if(endValue.operation == ViewPath.LINE){
startX = (startValue.operation==ViewPath.QUAD)?startValue.x1:startValue.x;
startX = (startValue.operation == ViewPath.CURVE)?startValue.x2:startX;
startY = (startValue.operation==ViewPath.QUAD)?startValue.y1:startValue.y;
startY = (startValue.operation == ViewPath.CURVE)?startValue.y2:startY;
x = startX + t * (endValue.x - startX);
y = startY+ t * (endValue.y - startY);
}else if(endValue.operation == ViewPath.CURVE){
//判斷開始點的類型,找到它真正的起始點,我就改了下這裡,原來别人的代碼的少判斷了一種情況
startX = (startValue.operation==ViewPath.QUAD)?startValue.x1:startValue.x;
startY = (startValue.operation==ViewPath.QUAD)?startValue.y1:startValue.y;
startX = (startValue.operation==ViewPath.CURVE)?startValue.x2:startX;
startY = (startValue.operation==ViewPath.CURVE)?startValue.y2:startY;
float oneMinusT = 1 - t;
//三階貝塞爾函數
x = oneMinusT * oneMinusT * oneMinusT * startX +
3 * oneMinusT * oneMinusT * t * endValue.x +
3 * oneMinusT * t * t * endValue.x1+
t * t * t * endValue.x2;
y = oneMinusT * oneMinusT * oneMinusT * startY +
3 * oneMinusT * oneMinusT * t * endValue.y +
3 * oneMinusT * t * t * endValue.y1+
t * t * t * endValue.y2;
}else if(endValue.operation == ViewPath.MOVE){
x = endValue.x;
y = endValue.y;
}else if(endValue.operation == ViewPath.QUAD){
startX = (startValue.operation==ViewPath.CURVE)?startValue.x2:startValue.x;
startY = (startValue.operation==ViewPath.CURVE)?startValue.y2:startValue.y;
startX = (startValue.operation==ViewPath.QUAD)?startValue.x1:startX;
startY = (startValue.operation==ViewPath.QUAD)?startValue.y1:startY;
//二階貝塞爾函數
float oneMinusT = 1 - t;
x = oneMinusT * oneMinusT * startX +
2 * oneMinusT * t * endValue.x +
t * t * endValue.x1;
y = oneMinusT * oneMinusT * startY +
2 * oneMinusT * t * endValue.y +
t * t * endValue.y1;
}else {
x = endValue.x;
y = endValue.y;
}
return new ViewPoint(x,y);
}
}
前面的看懂之後,後面就簡單了,再來看下圖,把各個點找到效果就出來了
public class BezierPath extends View {
Paint paint1 = new Paint();
Paint paint2 = new Paint();
Paint paint3 = new Paint();
Paint paint4 = new Paint();
int radus = 300;
int time = 5000;
int width;
int height;
private ValueAnimator redAnim1;
private ValueAnimator redAnim2;
private ValueAnimator redAnim3;
private ValueAnimator redAnim4;
private ViewPoint cpoint =new ViewPoint();
private ViewPoint cpoint2=new ViewPoint();
private ViewPoint cpoint3=new ViewPoint();
private ViewPoint cpoint4=new ViewPoint();
private AnimatorSet animatorSet2;
private BezierPathListener bezierPathListener;
public BezierPath(Context context) {
super(context);
init();
}
public BezierPath(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BezierPath(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void init() {
paint1.setStyle(Paint.Style.FILL);
paint1.setAntiAlias(true);
paint1.setColor(Color.RED);
paint2.setStyle(Paint.Style.FILL);
paint2.setAntiAlias(true);
paint2.setColor(Color.GREEN);
paint3.setStyle(Paint.Style.FILL);
paint3.setAntiAlias(true);
paint3.setColor(Color.BLUE);
paint4.setStyle(Paint.Style.FILL);
paint4.setAntiAlias(true);
paint4.setColor(Color.GRAY);
}
public void initPath() {
//千萬不要覺得下面很複雜,就是找貝爾塞的控制點和結束點而已,很簡單
//我們的ViewPath,其實可以繪制任何直線路徑和貝塞爾曲線路徑了,自己在調用lineTo傳入點等就行了
ViewPath viewPath = new ViewPath();
viewPath.moveTo(width / 2, height / 2);
cpoint.x = width/2;
cpoint.y = height/2;
viewPath.quadTo(width / 2 - radus , height / 2 - radus , width / 2, height / 2 - radus);
viewPath.quadTo(width / 2 + radus , height / 2 - radus , width / 2, height / 2);
redAnim1 = ValueAnimator.ofObject(new ViewPathEvaluator(), viewPath.getPoints().toArray());
redAnim1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
cpoint = (ViewPoint) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
redAnim1.setDuration(time);
ViewPath viewPath2 = new ViewPath();
viewPath2.moveTo(width / 2, height / 2);
cpoint2.x = width/2;
cpoint2.y = height/2;
viewPath2.quadTo(width / 2 + radus , height / 2 - radus , width / 2+radus, height / 2 );
viewPath2.quadTo(width / 2 + radus , height / 2 + radus , width / 2, height / 2);
redAnim2 = ValueAnimator.ofObject(new ViewPathEvaluator(), viewPath2.getPoints().toArray());
redAnim2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
cpoint2 = (ViewPoint) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
redAnim2.setDuration(time);
ViewPath viewPath3 = new ViewPath();
viewPath3.moveTo(width / 2, height / 2);
cpoint3.x = width/2;
cpoint3.y = height/2;
viewPath3.quadTo(width / 2+radus , height / 2 + radus , width / 2, height / 2+radus );
viewPath3.quadTo(width / 2 - radus , height / 2 + radus , width / 2, height / 2);
redAnim3 = ValueAnimator.ofObject(new ViewPathEvaluator(), viewPath3.getPoints().toArray());
redAnim3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
cpoint3 = (ViewPoint) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
redAnim3.setDuration(time);
redAnim3.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
isAnimationing = true;
}
@Override
public void onAnimationEnd(Animator animator) {
isAnimationing = false;
if(null!=bezierPathListener){
bezierPathListener.onAnimationEnd();
}
}
@Override
public void onAnimationCancel(Animator animator) {
isAnimationing = false;
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
ViewPath viewPath4 = new ViewPath();
viewPath4.moveTo(width / 2, height / 2);
cpoint4.x = width/2;
cpoint4.y = height/2;
viewPath4.quadTo(width / 2-radus , height / 2 + radus , width / 2-radus, height / 2 );
viewPath4.quadTo(width / 2 - radus , height / 2 - radus , width / 2, height / 2);
redAnim4 = ValueAnimator.ofObject(new ViewPathEvaluator(), viewPath4.getPoints().toArray());
redAnim4.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
cpoint4 = (ViewPoint) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
redAnim4.setDuration(time);
animatorSet2 = new AnimatorSet();
animatorSet2.playTogether(redAnim1,redAnim2,redAnim3,redAnim4);
animatorSet2.setDuration(time);
}
public boolean isAnimationing = false;
boolean isInitPath = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
if (width > 0) {
if (!isInitPath) {
isInitPath = true;
initPath();
}
}
}
@Override
public void draw(final Canvas canvas) {
canvas.drawCircle(cpoint.x-20, cpoint.y-20, 25, paint1);
canvas.drawCircle(cpoint2.x+20, cpoint2.y-20, 25, paint2);
canvas.drawCircle(cpoint3.x+20, cpoint3.y+20, 25, paint3);
canvas.drawCircle(cpoint4.x-20, cpoint4.y+20, 25, paint4);
}
public void startAnimation(){
if (!isAnimationing) {
animatorSet2.start();
}
}
public void setListener(BezierPathListener bezierPathListener){
this.bezierPathListener =bezierPathListener;
}
public interface BezierPathListener{
void onAnimationEnd();
}
}