谁说程序猿不浪漫的啊,每次看到别人在黑程序猿心中就有一种无奈,仅仅是他们看到的是程序猿不好的一面。今天我将用这个案例告诉那些人,程序猿也是一个非常浪漫。非常有情调的人。
在程序猿心中他们仅仅想做最高效的事情,没有什么比效率更重要了。那就開始今天程序猿的告白之旅。
我们都知道属性动画有个强大的地方,它实现让某个控件依照我们指定的运动轨迹来运动。
也就是说它能够依照一个抛物线来运动,也能够依照一个线性的线来运动,还能够依照我们今天所讲的贝塞尔曲线的轨迹来运动。为什么他能够依照某一个轨迹来运动呢??首先我们来分析一下今天这个Demo的实现原理吧
分析:我们要实现一颗爱心在整个布局底部中间位置动态产生,产生的效果过程中须要三种动画:X方向的缩放动画,Y方向的缩放动画,整个透明度的动画。
产生后动画的爱心,必须依照贝塞尔曲线的轨迹来上升移动。而且在移动的过程中,透明度慢慢减小,直至爱心到达布局顶部正好消失。那怎么去实现呢?
大致的思路例如以下:首先前面三种的动画非常easy,通过一个AnimatorSet动画集合,将那三种动画放在一起,然后通过一个playTogether(...)让他们同一时候动画就可以搞定。
因为我们须要动态添加该布局中爱心。所以最好是写一个自己定义的ViewGroup继承于Relative,然后在布局中固定好爱心布局位置。当然你不用自己定义一个View直接操纵也能够的。然后给爱心产生过程中加入最初三种属性动画。
到这里我们的第一步就完毕了。然后接着就是实现贝塞尔曲线轨迹,我们知道在属性动画中它能够随意操作一个属性的变化,而且在属性动画中有个ValueAnimator它能够得到一个动态变化的值范围,他本身不能作用于某个动画。但他确是实现动画的本质。
ObjectAnimator则是它的子类,仅仅是在他的基础上进行再一次的封装。
说完这个不得不说TypeEvaluator<T>它是一个估值器的接口,他能够动态计算出该轨迹中随意一个点的位置坐标,说白他能够得到该曲线的轨迹。
实现贝塞尔曲线思路是这种:分别定义起点和终点,然后通过TypeEvaluator<PointF>估值器来得到到整条的曲线轨迹中每一个点坐标。然后通过动画的监听事件不断获得最新的点的坐标,把该坐标实时更新到爱心控件的X,Y坐标就可以。从实现按曲线移动的效果。
由以上贝塞尔曲线原理图,来进一步分析各个点,而且给出贝塞尔曲线公式:
贝塞尔曲线是一种应用很广曲线,能够它应用到各个领域包含一些大型的工业设计,计算机图形绘制等领域:
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。曲线起始于P0走向P1,并从P2的方向来到P3。
一般不会经过P1或P2;这两个点仅仅是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前,走向P2方向的“长度有多长”。
公式:
P0点:不管是哪个爱心,P0都为起点始终都是一样的。
P1点:在X轴上范围:0~loveLayout.mWidth,假设要保证P1点在P2点的以下,那么P1点的Y轴上范围是:1/2mHeight~mHeight(注:mHeight是整个布局的高度)
P2点:在X轴上范围0~loveLayout.mWidth,须要保证P2点在P1上面,那么P2点的Y轴上的范围是:0~1/2mHeight
P3点:细致发现每颗爱心达到顶部,所以仅仅是X轴的坐标是不一样,Y轴上坐标都是0
好了,我们经过非常长一段时间的分析,基本上都分析清楚了,如今就可以開始我们的编码了:
布局:
自己定义ViewGroup的LoveLayout
package com.mikyou.myview;import java.util.ArrayList;import java.util.List;import java.util.Random;import android.animation.AnimatorSet;import android.animation.ObjectAnimator;import android.animation.ValueAnimator;import android.animation.ValueAnimator.AnimatorUpdateListener;import android.content.Context;import android.graphics.PointF;import android.graphics.drawable.Drawable;import android.util.AttributeSet;import android.widget.ImageView;import android.widget.RelativeLayout;import com.mikyou.loveforyou.R;import com.mikyou.tools.BezierEvalutor;public class LoveLayout extends RelativeLayout{ //用于存放不同图片的爱心 private Drawable firstDrawable; private Drawable secondDrawable; private Drawable threeDrawable; private int dHeight;//爱心的高度 private int dWidth;//爱心的宽度 private int mWidth;//整个布局的宽度 private int mHeight;//整个布局的高度 ListmDrawablesList=new ArrayList (); private LayoutParams params; private Random random=new Random();//定义一个随机数对象。用于表示P1。P2,P3点的X,Y坐标的在某个范围随机变化 public LoveLayout(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private void initView() { firstDrawable=getResources().getDrawable(R.drawable.pic1); mDrawablesList.add(firstDrawable); secondDrawable=getResources().getDrawable(R.drawable.pic2); mDrawablesList.add(secondDrawable); threeDrawable=getResources().getDrawable(R.drawable.pic3); mDrawablesList.add(threeDrawable); //得到爱心图片的宽高 dHeight=firstDrawable.getIntrinsicHeight(); dWidth=firstDrawable.getIntrinsicWidth(); params=new LayoutParams(dWidth, dHeight); //给爱心控件动态布局。使得爱心始终在布局最底部的中间位置 params.addRule(CENTER_HORIZONTAL,TRUE); params.addRule(ALIGN_PARENT_BOTTOM, TRUE); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //得到本布局的宽高 mWidth=getMeasuredWidth(); mHeight=getMeasuredHeight(); } public void addLove(){//加入心 ImageView mImageView=new ImageView(getContext()); mImageView.setImageDrawable(mDrawablesList.get(random.nextInt(3)));//通过随机对象,随机在这三张爱心图片产生随意一张图片 mImageView.setLayoutParams(params); addView(mImageView); //属性动画控制坐标 AnimatorSet set= getAnimator(mImageView);//通过getAnimator得到整个爱心全部动画集合 set.start(); } //构造3个属性动画 private AnimatorSet getAnimator(ImageView mImageView) { //1,alpha动画;2, ObjectAnimator alphaAnimator= ObjectAnimator.ofFloat(mImageView, "alpha", 0.3f,1.0f); ObjectAnimator scaleXAnimator= ObjectAnimator.ofFloat(mImageView, "scaleX", 0.2f,1.0f); ObjectAnimator scaleYAnimator= ObjectAnimator.ofFloat(mImageView, "scaleY", 0.2f,1.0f); AnimatorSet mAnimatorSet=new AnimatorSet(); mAnimatorSet.setDuration(500); //三个动画同一时候集合 mAnimatorSet.playTogether(alphaAnimator,scaleXAnimator,scaleYAnimator); mAnimatorSet.setTarget(mImageView); //贝塞尔曲线动画,不断改动ImageView的坐标,PointF(x,y) ValueAnimator bezierValueAnimator=getBeziValueAnimator(mImageView);//getBeziValueAnimator得到贝赛尔曲线轨迹位移动画 AnimatorSet bezierAnimatorSet =new AnimatorSet(); //按顺序播放动画 bezierAnimatorSet.playSequentially(mAnimatorSet,bezierValueAnimator);//然后按顺序播放这些动画集合 //bezierAnimatorSet.setDuration(3000); bezierAnimatorSet.setTarget(mImageView); return bezierAnimatorSet;//返回一个总体爱心全部动画的集合 } /** * @author mikyou * getBeziValueAnimator * 构造一个贝塞尔曲线动画 * */ private ValueAnimator getBeziValueAnimator( final ImageView mImageView) { //贝塞尔曲线动画,不断改动ImageView的坐标,PointF(x,y) PointF pointF2=getPointF(2);//getPointF方法依据传进来的数字来标记四个点,P0,P1,P2,P3 PointF pointF1=getPointF(1); PointF pointF0=new PointF(mWidth/2-dWidth/2, mHeight-dHeight);//创建P0点。起点 PointF pointF3=new PointF(random.nextInt(mWidth), 0);//创建P3点。终点 BezierEvalutor mBezierEvalutor=new BezierEvalutor(pointF1, pointF2);//创建一个估值器,然后并把P1。P2点传入 /** * @author zhongqihong * 创建一个ValueAnimator,并把起点P0和终点P3传入,然后在BezierEvalutor重写的方法evalute中得到P0,P3 * 然后通过上一步利用BezierEvalutor构造器将P1,P2两个点传入。所以这就是说 * 在BezierEvalutor重写的方法evalute能够得到P0,P1,P2,P3点对象,然后通过贝塞尔的公式 * 就可以计算出该轨迹上的随意一点的坐标,并实时返回一个PontF点的对象,然后通过addUpdateListener * 监听事件实时获得最新点的坐标然后将这些最新点坐标去更新爱心的ImageVIew控件的X,Y坐标 * */ ValueAnimator animator=ValueAnimator.ofObject(mBezierEvalutor, pointF0,pointF3); animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { PointF pointF=(PointF) animation.getAnimatedValue(); //通过addUpdateListener监听事件实时获得从mBezierEvalutor估值器对象evalute方法实时计算出最新点的坐标 。 mImageView.setX(pointF.x);//然后去更新该爱心ImageView的X,Y坐标 mImageView.setY(pointF.y); mImageView.setAlpha(1-animation.getAnimatedFraction());//getAnimatedFraction()就是mBezierEvalutor估值器对象中evaluate方法t即时间因子,从0~1变化,所以爱心透明度应该是从1~0变化正好到了顶部,t变为1。透明度变为0,即爱心消失 } }); animator.setTarget(mImageView); animator.setDuration(3000); return animator; } private PointF getPointF(int i) { PointF pointF=new PointF(); pointF.x=random.nextInt(mWidth);//0~loveLayout.Width //为了美观,建议尽量保证P2在P1上面,那怎么做呢?
?
//仅仅须要将该布局的高度分为上下两部分,让p1仅仅能在以下部分范围内变化(1/2height~height),让p2仅仅能在上面部分范围内变化(0~1/2height),由于坐标系是倒着的; //0~loveLayout.Height/2 if (i==1) { pointF.y=random.nextInt(mHeight/2)+mHeight/2;//P1点Y轴坐标变化 }else if(i==2){//P2点Y轴坐标变化 pointF.y=random.nextInt(mHeight/2); } return pointF; } }
BezierEvalutor自己定义估值器接口类:
package com.mikyou.tools;import android.animation.TypeEvaluator;import android.graphics.PointF;/** * @author mikyou * 自己定义估值器 * */public class BezierEvalutor implements TypeEvaluator{ PointF p1; PointF p2; public BezierEvalutor(PointF p1, PointF p2) { super(); this.p1 = p1; this.p2 = p2; } @Override public PointF evaluate(float t, PointF p0, PointF p3) { //时间因子t: 0~1 PointF point=new PointF(); //实现贝塞尔公式: point.x=p0.x*(1-t)*(1-t)*(1-t)+3*p1.x*t*(1-t)*(1-t)+3*p2.x*(1-t)*t*t+p3.x*t*t*t;//实时计算最新的点X坐标 point.y=p0.y*(1-t)*(1-t)*(1-t)+3*p1.y*t*(1-t)*(1-t)+3*p2.y*(1-t)*t*t+p3.y*t*t*t;//实时计算最新的点Y坐标 return point;//实时返回最新计算出的点对象 }}
最后我们仅仅须要在MainActivity中去调用那个LoveLayout类中的addLove方法就可以。为了让他自己主动产生爱心,所以我就开启一个子线程来管理产生爱心,然后通过Handler对象来更新主线程中的UI。
package com.mikyou.loveforyou;import com.mikyou.myview.LoveLayout;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;public class MainActivity extends Activity { private LoveLayout mLoveLayout; private Handler handler=new Handler(){ public void handleMessage(android.os.Message msg) { if (msg.what==0x123) { mLoveLayout.addLove(); } }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { mLoveLayout=(LoveLayout)findViewById(R.id.id_love_layout); new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub try { while(true){ Thread.sleep(400); handler.sendEmptyMessage(0x123); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }).start(); }}
最后执行效果: