Android 自定义 View—贝塞尔曲线绘制及属性动画 (一)

3,051 阅读9分钟
原文链接: www.jianshu.com

最近上班可真是忙得很,好不容易有点属于自己的时间了,不用加班,其实有时候感觉忙点也挺好,起码不会有无所事事、空虚的感觉,忙里偷闲才是最开心的。闲暇时间也没用来挥霍,最近又重新温习了下自定义View,贝塞尔曲线的绘制及属性动画的使用等。好了,说了这么多还没见到图啊,无图无真相,看完下面这波图就开始挽起袖子撸代码了。

实现效果:


送心效果

这个效果不太重要,关键是如何去实现的方式。

实现

首先我们观察这个图上的View,整体可以看作是一个大容器,一个个心型图像可以看作是一个个ImageView,从容器底部中间部分冒出来的,因此我们可以自定义一个View继承自RelativeLayout我们动态的去把每个图片addView到我们这个View上。

...创建一个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范围内随机生成的。

接下来我们开始写动画吧,首先是刚开始的图片显示动画由小变大,透明度逐渐变为1:

/**
 * 设置刚添加上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 可以定制我们需要的变化规则,我们可以利用初始点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;
}

有初始动画,有贝塞尔动画,顺序执行即可完成整个过程:

/**
 * 动画效果
 * @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); //终止点
    /**
     * 开始执行贝塞尔动画
     */
    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;
}

/**
 * 合并执行两个动画
 * @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();
}

执行完一次动画之后从容器中移除此ImageView~

在写一个方法去调用动画即可:

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

在activity调用该控件的 startAnimation()方法我们就可以看到一个心飘啊飘的到顶部了。

现在我需要一点击不断的出现很多心的效果,再次调用该方法暂停动画,因此加入一个定时器:

/**
 * 定时器,可以自动执行动画
 */
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() {
                // 需要做的事:发送消息
                Message message = handler.obtainMessage();
                message.what = 1;
                handler.sendMessage(message);
            }
        };
        timer.schedule(task, 0, 150); // 执行task,经过150ms循环执行
    }
}


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();
}

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

/**
 * 定时器,可以自动执行动画
 */
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() {
                // 需要做的事:发送消息
                Message message = handler.obtainMessage();
                message.what = 1;
                handler.sendMessage(message);
            }
        };
        timer.schedule(task, 0, 150); // 执行task,经过150ms循环执行
    }
}


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); //终止点
    /**
     * 开始执行贝塞尔动画
     */
    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;
}

/**
 * 合并执行两个动画
 * @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—贝塞尔曲线绘制及属性动画 (二)

不对的地方望大家指出,相互学习,谢谢~