谁说程序员不浪漫的啊,每次看到别人在黑程序员心中就有一种无奈,只是他们看到的是程序员不好的一面,今天我将用这个案例告诉那些人,程序猿也是一个很浪漫,很有情调的人。在程序员心中他们只想做最高效的事情,没有什么比效率更重要了。那就开始今天程序猿的告白之旅。
我们都知道属性动画有个强大的地方,它实现让某个控件按照我们指定的运动轨迹来运动。也就是说它可以按照一个抛物线来运动,也可以按照一个线性的线来运动,还可以按照我们今天所讲的贝塞尔曲线的轨迹来运动。为什么他可以按照某一个轨迹来运动呢??首先我们来分析一下今天这个Demo的实现原理吧
分析:我们要实现一颗爱心在整个布局底部中间位置动态产生,产生的效果过程中需要三种动画:X方向的缩放动画,Y方向的缩放动画,整个透明度的动画。产生后动画的爱心,必须按照贝塞尔曲线的轨迹来上升移动,并且在移动的过程中,透明度慢慢减小,直至爱心到达布局顶部正好消失。那怎么去实现呢?
大致的思路如下:首先前面三种的动画很简单,通过一个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
好了,我们经过很长一段时间的分析,基本上都分析清楚了,现在即可开始我们的编码了:
布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="#FFF" > <com.mikyou.myview.LoveLayout android:id="@+id/id_love_layout" android:layout_width="match_parent" android:layout_height="match_parent" > </com.mikyou.myview.LoveLayout> </LinearLayout>
自定义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;//整个布局的高度 List<Drawable> mDrawablesList=new ArrayList<Drawable>(); 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> { 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(); } }
最后运行效果: