最近上班可真是忙得很,好不容易有點屬于自己的時間了,不用加班,其實有時候感覺忙點也挺好,起碼不會有無所事事、空虛的感覺,忙里偷閑才是最開心的。閑暇時間也沒用來揮霍,最近又重新溫習了下自定義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—貝塞爾曲線繪制及屬性動畫 (二)
不對的地方望大家指出,相互學習,謝謝~