Android自定義View—貝塞爾曲線繪制及屬性動畫 (一)

最近上班可真是忙得很,好不容易有點屬于自己的時間了,不用加班,其實有時候感覺忙點也挺好,起碼不會有無所事事、空虛的感覺,忙里偷閑才是最開心的。閑暇時間也沒用來揮霍,最近又重新溫習了下自定義View,貝塞爾曲線的繪制及屬性動畫的使用等。好了,說了這么多還沒見到圖啊,無圖無真相,看完下面這波圖就開始挽起袖子擼代碼了。

實現效果:

送心效果

這個效果不太重要,關鍵是如何去實現的方式。

實現

首先我們觀察這個圖上的View,整體可以看作是一個大容器,一個個心型圖像可以看作是一個個ImageView,從容器底部中間部分冒出來的,因此我們可以自定義一個View繼承自RelativeLayout我們動態(tài)的去把每個圖片addView到我們這個View上。

...創(chuàng)建一個ImageView的屬性
LayoutParams lp ;
...
//dWidth dHeight 是每張圖片的長寬,這里所有心型圖片尺寸一致。
dWidth = drawable[0].getIntrinsicWidth();
dHeight = drawable[0].getIntrinsicHeight();
lp = new LayoutParams(dWidth,dHeight);
lp.addRule(ALIGN_PARENT_BOTTOM);
lp.addRule(CENTER_HORIZONTAL);

//添加ImageView 
ImageView image = new ImageView(getContext());
image.setImageDrawable(drawable[random.nextInt(5)]);
image.setLayoutParams(lp);
addView(image);

好了到此都很簡單,現在我們已經可以實現把ImageView添加到容器底部了,接下來就實現動畫移動飄動的效果。

通關觀察可以看到心是從底部移動到頂部,運動的軌跡是曲線,并且到頂部的位置也是隨機的,因此我們很容易想到只要讓ImageView沿著一條曲線運動即可實現,于是我們想到了貝塞爾曲線,我們用二階還是三階的呢?

二階貝塞爾曲線
二階貝塞爾曲線公式

這是二階貝塞爾曲線,我們先不管公式,我們就看繪制的曲線路徑跟我們效果圖上ImageView 運動的路徑是不是不一致啊,接下來看三階曲線:

三階貝塞爾曲線

三階貝塞爾曲線公式

我們可以看到三階貝塞爾曲線是有2個控制點,只要圖上2個控制點位置改變一下就可以達到S型運動軌跡的感覺。

回到圖片移動問題上來,我們都知道Android給我們提供了繪制貝塞爾曲線的方法,我們可以通過調用Path的某些方法繪制不同貝塞爾曲線,但是在這個例子里面我們不是要繪制貝塞爾曲線,而是需要這個路徑即可。我們獲取到這個運動曲線上的每個點,獲取x,y點然后把ImageView 的x,y設置成它。

運動草圖

我簡單繪制了下運動的情況,畫的不好請不要說我,因為我已經盡力了
啊。通過此圖可以看到起點是固定的,終點也基本上算是定下來的,只是橫坐標是在width范圍內隨機生成的。

接下來我們開始寫動畫吧,首先是剛開始的圖片顯示動畫由小變大,透明度逐漸變?yōu)?:

/**
 * 設置剛添加上imageview的屬性動畫,由小變大,逐漸清晰
 * @param image
 * @return
 */
public AnimatorSet getInitAnimationSet(final ImageView image){
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(image,"scaleX",0.4f,1f);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(image,"scaleY",0.4f,1f);
    ObjectAnimator alpha = ObjectAnimator.ofFloat(image,"alpha",0.4f,1f);

    AnimatorSet animate = new AnimatorSet();
    animate.playTogether(scaleX,scaleY,alpha);
    animate.setDuration(500);
    return animate ;
}
....
//變化點PointF的時候調用此方法
ValueAnimator.ofObject(TypeEvaluator evaluator, Object... values)

ValueAnimator.ofObject可以生成一個ValueAnimator對象,TypeEvaluator 可以定制我們需要的變化規(guī)則,我們可以利用初始點PointF0經過貝塞爾三階曲線變換到PointF3終止點,中間的控制點是PointF1和PointF2,于是我們自定義一個TypeEvaluator :

public class BezierEvaluator implements TypeEvaluator<PointF> {
        /**
         * 這2個點是控制點
         */
        private PointF point1 ;
        private PointF point2 ;
        public BezierEvaluator(PointF point1 ,PointF point2 ) {
            this.point1 = point1 ;
            this.point2 = point2 ;
        }
        /**
         * @param t
         * @param point0 初始點
         * @param point3 終點
         * @return
         */
        @Override
        public PointF evaluate(float t, PointF point0, PointF point3) {
            PointF point = new PointF();
            point.x = point0.x*(1-t)*(1-t)*(1-t)
                      +3*point1.x*t*(1-t)*(1-t)
                      +3*point2.x*t*t*(1-t)*(1-t)
                      +point3.x*t*t*t ;
            point.y = point0.y*(1-t)*(1-t)*(1-t)
                     +3*point1.y*t*(1-t)*(1-t)
                     +3*point2.y*t*t*(1-t)*(1-t)
                     +point3.y*t*t*t ;
            return point;
        }
    }

至于2個控制點的確定,保證一個點在上面一個點在下面即可:

private PointF getPointF(int scale) {
        PointF pointF = new PointF();
        pointF.x = random.nextInt((mWidth - 100));//減去100 是為了控制 x軸活動范圍,看效果 
        //再Y軸上 為了確保第二個點 在第一個點之上,我把Y分成了上下兩半 這樣動畫效果好一些  也可以用其他方法
        pointF.y = random.nextInt((mHeight - 100))/scale;
        return pointF;
}

有初始動畫,有貝塞爾動畫,順序執(zhí)行即可完成整個過程:

/**
 * 動畫效果
 * @param image
 */
private AnimatorSet getRunAnimatorSet(final ImageView image) {
    AnimatorSet runSet = new AnimatorSet();
    PointF point0 = new PointF((mWidth-dWidth)/2,mHeight-dHeight); //起始點
    PointF point3 = new PointF(random.nextInt(getWidth()),0); //終止點
    /**
     * 開始執(zhí)行貝塞爾動畫
     */
    TypeEvaluator evaluator = new BezierEvaluator(getPointF(2),getPointF(1));
    ValueAnimator bezier = ValueAnimator.ofObject(evaluator,point0,point3);
    bezier.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //這里獲取到貝塞爾曲線計算出來的的x y值 賦值給view 這樣就能讓愛心隨著曲線走啦
            PointF pointF = (PointF) animation.getAnimatedValue();
            image.setX(pointF.x);
            image.setY(pointF.y);
            image.setAlpha(1-animation.getAnimatedFraction());
        }
    });
    runSet.play(bezier);
    runSet.setDuration(3000);
    return runSet;
}

/**
 * 合并執(zhí)行兩個動畫
 * @param image
 */
public void start(final ImageView image){
    AnimatorSet finalSet = new AnimatorSet();
    finalSet.setInterpolator(interpolators[random.nextInt(4)]);//實現隨機變速
    finalSet.playSequentially(getInitAnimationSet(image), getRunAnimatorSet(image));
    finalSet.setTarget(image);
    finalSet.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            removeView(image);
        }
    });
    finalSet.start();
}

執(zhí)行完一次動畫之后從容器中移除此ImageView~

在寫一個方法去調用動畫即可:

/**
 * 創(chuàng)建可移動的View
 */
public void startAnimation(){
    ImageView image = new ImageView(getContext());
    image.setImageDrawable(drawable[random.nextInt(5)]);
    image.setLayoutParams(lp);
    addView(image);
    start(image);
}

在activity調用該控件的 startAnimation()方法我們就可以看到一個心飄啊飄的到頂部了。

現在我需要一點擊不斷的出現很多心的效果,再次調用該方法暫停動畫,因此加入一個定時器:

/**
 * 定時器,可以自動執(zhí)行動畫
 */
public void startAutoAnimation(){
    isPlayingAnim = !isPlayingAnim ;
    if (isPlayingAnim){
        if (timer!=null){
            timer.cancel();
        }
        if (task!=null){
            task.cancel();
        }
    }else {
        timer = new Timer();
        task = new TimerTask() {
            @Override
            public void run() {
                // 需要做的事:發(fā)送消息
                Message message = handler.obtainMessage();
                message.what = 1;
                handler.sendMessage(message);
            }
        };
        timer.schedule(task, 0, 150); // 執(zhí)行task,經過150ms循環(huán)執(zhí)行
    }
}


Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (msg.what==1){
            ImageView image = new ImageView(getContext());
            image.setImageDrawable(drawable[random.nextInt(5)]);
            image.setLayoutParams(lp);
            addView(image);
            start(image);
        }
    }
};

好了,至此,大功告成,附上完整代碼,這里很多屬性可以抽取出來定義在xml布局里面寫,我是圖方便快捷寫死在控件里面了。

最后附上完整源代碼:

package com.wzh.ffmpeg.study.view;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;

import com.wzh.ffmpeg.study.R;

import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

/**
* author:Administrator on 2017/3/15 09:18
* description:文件說明
* version:版本
*/
public class BezierView extends RelativeLayout {
private Interpolator[] interpolators ;
private Drawable drawable[];
/**
 * 圖片的寬高
 */
private int dWidth = 0 ;
private int dHeight = 0 ;
private LayoutParams lp ;
private Random random ;
/**
 * 父控件寬高
 */
private int mWidth = 0 ;
private int mHeight = 0 ;
private Timer timer = null;
private TimerTask task = null ;
private boolean isPlayingAnim = true ;

public BezierView(Context context) {
    this(context,null);
}

public BezierView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs,0);
}

public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
}

/**
 * 初始化數據
 */
private void init() {
    drawable = new Drawable[5];
    drawable[0] = ContextCompat.getDrawable(getContext(), R.drawable.red);
    drawable[1] = ContextCompat.getDrawable(getContext(),R.drawable.yellow);
    drawable[2] = ContextCompat.getDrawable(getContext(),R.drawable.deep_red);
    drawable[3] = ContextCompat.getDrawable(getContext(),R.drawable.blue);
    drawable[4] = ContextCompat.getDrawable(getContext(),R.drawable.green);

    interpolators = new Interpolator[4];
    interpolators[0] = new AccelerateInterpolator();
    interpolators[1] = new DecelerateInterpolator();
    interpolators[2] = new AccelerateDecelerateInterpolator();
    interpolators[3] = new LinearInterpolator();

    dWidth = drawable[0].getIntrinsicWidth();
    dHeight = drawable[0].getIntrinsicHeight();

    lp = new LayoutParams(dWidth,dHeight);
    lp.addRule(ALIGN_PARENT_BOTTOM);
    lp.addRule(CENTER_HORIZONTAL);

    random = new Random();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //再此處才能準確獲取到控件的寬高
    mWidth = getMeasuredWidth();
    mHeight = getMeasuredHeight();
}

/**
 * 創(chuàng)建可移動的View
 */
public void startAnimation(){
    ImageView image = new ImageView(getContext());
    image.setImageDrawable(drawable[random.nextInt(5)]);
    image.setLayoutParams(lp);
    addView(image);
    start(image);
}

/**
 * 定時器,可以自動執(zhí)行動畫
 */
public void startAutoAnimation(){
    isPlayingAnim = !isPlayingAnim ;
    if (isPlayingAnim){
        if (timer!=null){
            timer.cancel();
        }
        if (task!=null){
            task.cancel();
        }
    }else {
        timer = new Timer();
        task = new TimerTask() {
            @Override
            public void run() {
                // 需要做的事:發(fā)送消息
                Message message = handler.obtainMessage();
                message.what = 1;
                handler.sendMessage(message);
            }
        };
        timer.schedule(task, 0, 150); // 執(zhí)行task,經過150ms循環(huán)執(zhí)行
    }
}


Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (msg.what==1){
            ImageView image = new ImageView(getContext());
            image.setImageDrawable(drawable[random.nextInt(5)]);
            image.setLayoutParams(lp);
            addView(image);
            start(image);
        }
    }
};

/**
 * view銷毀之后調用,釋放資源
 */
@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    if (timer!=null){
        timer.cancel();
    }
    if (task!=null){
        task.cancel();
    }
}

/**
 * 設置剛添加上imageview的屬性動畫,由小變大,逐漸清晰
 * @param image
 * @return
 */
public AnimatorSet getInitAnimationSet(final ImageView image){
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(image,"scaleX",0.4f,1f);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(image,"scaleY",0.4f,1f);
    ObjectAnimator alpha = ObjectAnimator.ofFloat(image,"alpha",0.4f,1f);

    AnimatorSet animate = new AnimatorSet();
    animate.playTogether(scaleX,scaleY,alpha);
    animate.setDuration(500);
    return animate ;
}
/**
 * 動畫效果
 * @param image
 */
private AnimatorSet getRunAnimatorSet(final ImageView image) {
    AnimatorSet runSet = new AnimatorSet();
    PointF point0 = new PointF((mWidth-dWidth)/2,mHeight-dHeight); //起始點
    PointF point3 = new PointF(random.nextInt(getWidth()),0); //終止點
    /**
     * 開始執(zhí)行貝塞爾動畫
     */
    TypeEvaluator evaluator = new BezierEvaluator(getPointF(2),getPointF(1));
    ValueAnimator bezier = ValueAnimator.ofObject(evaluator,point0,point3);
    bezier.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //這里獲取到貝塞爾曲線計算出來的的x y值 賦值給view 這樣就能讓愛心隨著曲線走啦
            PointF pointF = (PointF) animation.getAnimatedValue();
            image.setX(pointF.x);
            image.setY(pointF.y);
            image.setAlpha(1-animation.getAnimatedFraction());
        }
    });
    runSet.play(bezier);
    runSet.setDuration(3000);
    return runSet;
}

/**
 * 合并執(zhí)行兩個動畫
 * @param image
 */
public void start(final ImageView image){
    AnimatorSet finalSet = new AnimatorSet();
    finalSet.setInterpolator(interpolators[random.nextInt(4)]);//實現隨機變速
    finalSet.playSequentially(getInitAnimationSet(image), getRunAnimatorSet(image));
    finalSet.setTarget(image);
    finalSet.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            removeView(image);
        }
    });
    finalSet.start();
}

/**
 * 獲取控制點
 * @param scale
 * @return
 */
private PointF getPointF(int scale) {
    PointF pointF = new PointF();
    pointF.x = random.nextInt((mWidth - 100));//減去100 是為了控制 x軸活動范圍,看效果 
    //再Y軸上 為了確保第二個點 在第一個點之上,我把Y分成了上下兩半 這樣動畫效果好一些  也可以用其他方法
    pointF.y = random.nextInt((mHeight - 100))/scale;
    return pointF;
}
public class BezierEvaluator implements TypeEvaluator<PointF> {
    /**
     * 這2個點是控制點
     */
    private PointF point1 ;
    private PointF point2 ;
    public BezierEvaluator(PointF point1 ,PointF point2 ) {
        this.point1 = point1 ;
        this.point2 = point2 ;
    }
    /**
     * @param t
     * @param point0 初始點
     * @param point3 終點
     * @return
     */
    @Override
    public PointF evaluate(float t, PointF point0, PointF point3) {
        PointF point = new PointF();
        point.x = point0.x*(1-t)*(1-t)*(1-t)
                  +3*point1.x*t*(1-t)*(1-t)
                  +3*point2.x*t*t*(1-t)*(1-t)
                  +point3.x*t*t*t ;
        point.y = point0.y*(1-t)*(1-t)*(1-t)
                 +3*point1.y*t*(1-t)*(1-t)
                 +3*point2.y*t*t*(1-t)*(1-t)
                 +point3.y*t*t*t ;
        return point;
    }
}

}

Acitivity調用

BezierView bse = (BezierView) findViewById(R.id.bse);
bse.startAutoAnimation(); //自動播放動畫效果

其實最主要的就是自定義屬性動畫的屬性,TypeEvaluator<PointF>,這個是最核心的思想。如果要兼容3.0以下版本,那么自己加入nineoldandroids包,可以支持低版本的動畫。

還有一姊妹篇Android自定義View—貝塞爾曲線繪制及屬性動畫 (二)

不對的地方望大家指出,相互學習,謝謝~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容