最近上班可真是忙得很,好不容易有点属于自己的时间了,不用加班,其实有时候感觉忙点也挺好,起码不会有无所事事、空虚的感觉,忙里偷闲才是最开心的。闲暇时间也没用来挥霍,最近又重新温习了下自定义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 {
/**
* 这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 {
/**
* 这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
还有一姊妹篇Android自定义View—贝塞尔曲线绘制及属性动画 (二)
不对的地方望大家指出,相互学习,谢谢~