目录结构:
在这篇文章中,笔者会详细介绍Android的动画。安卓中的动画大致分为tweened animation(补间动画)、frame-by-frame animation(逐帧动画)、Property Animation(属性动画),这个三个概念之间有点重合,下面介绍三种属性动画的产生时间顺序,其中补间动画和逐帧动画是Android1.0中被加入的,随着时间的推移简单的动画已经不能满足需求了,在Android4.0之后就加入了属性动画。本文还会介绍View、surfaceView和GLSurfaceView之间的比较。
1.补间动画
补间动画(tweened animation)都继承自android.view.animation.Animation抽象类,android.view.animation.Animation有五个直接实现子类:AlphaAnimation,AnimationSet,RotateAnimation,ScaleAnimation,TranslateAnimation。
其中AlphaAnimation,RotateAnimation,ScaleAnimation,TranslateAnimation是效果动画类,而AnimationSet是用于完成一系列组合动画的。1.1 使用java代码实现Alpha、Rotate、Scale、Translate动画
下面这个栗子,演示了Alpha(淡入淡出)、Rotate(旋转)、Scale(缩放)、Translate(移动)的效果:
xml文件布局如下:java代码如下:
public class MainActivity extends Activity { private Button rotateButton = null; private Button scaleButton = null; private Button alphaButton = null; private Button translateButton = null; private ImageView image = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); rotateButton = (Button)findViewById(R.id.rotateButton); scaleButton = (Button)findViewById(R.id.scaleButton); alphaButton = (Button)findViewById(R.id.alphaButton); translateButton = (Button)findViewById(R.id.translateButton); image = (ImageView)findViewById(R.id.image); rotateButton.setOnClickListener(new RotateButtonListener()); scaleButton.setOnClickListener(new ScaleButtonListener()); alphaButton.setOnClickListener(new AlphaButtonListener()); translateButton.setOnClickListener(new TranslateButtonListener()); } class AlphaButtonListener implements OnClickListener{ public void onClick(View v) { //创建一个AnimationSet对象,参数为Boolean型, //true表示使用Animation的interpolator,false则是使用自己的 AnimationSet animationSet = new AnimationSet(true); //创建一个AlphaAnimation对象,参数从完全的透明度,到完全的不透明 AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0); //设置动画执行的时间 alphaAnimation.setDuration(2000); //将alphaAnimation对象添加到AnimationSet当中 animationSet.addAnimation(alphaAnimation); //使用ImageView的startAnimation方法执行动画 image.startAnimation(animationSet); } } class RotateButtonListener implements OnClickListener{ public void onClick(View v) { AnimationSet animationSet = new AnimationSet(true); //参数1:从哪个旋转角度开始 //参数2:转到什么角度 //后4个参数用于设置围绕着旋转的圆的圆心在哪里 //参数3:确定x轴坐标的类型,有ABSOLUT绝对坐标、RELATIVE_TO_SELF相对于自身坐标、RELATIVE_TO_PARENT相对于父控件的坐标 //参数4:x轴的值,0.5f表明是以自身这个控件的一半长度为x轴 //参数5:确定y轴坐标的类型 //参数6:y轴的值,0.5f表明是以自身这个控件的一半长度为x轴 RotateAnimation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f); rotateAnimation.setDuration(2000); animationSet.addAnimation(rotateAnimation); image.startAnimation(animationSet); } } class ScaleButtonListener implements OnClickListener{ public void onClick(View v) { AnimationSet animationSet = new AnimationSet(true); //0f从相对0点开始(若x和y的开始点都是0,那么就是从一个点开始),1f表示目前一个控件长度的位置(若x和y的结束点都是1f,那么就是缩放到原本大小。) //参数1:x轴的初始值 //参数2:x轴收缩后的值 //参数3:y轴的初始值 //参数4:y轴收缩后的值 //参数5:确定x轴坐标的类型 //参数6:x轴的值,0.5f表明是以自身这个控件的一半长度为x轴 //参数7:确定y轴坐标的类型 //参数8:y轴的值,0.5f表明是以自身这个控件的一半长度为x轴 ScaleAnimation scaleAnimation = new ScaleAnimation( 0f, 1f,0f,1f, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f); scaleAnimation.setDuration(2000); animationSet.addAnimation(scaleAnimation); image.startAnimation(animationSet); } } class TranslateButtonListener implements OnClickListener{ public void onClick(View v) { AnimationSet animationSet = new AnimationSet(true); //参数1~2:x轴的开始位置,0f代表从当前x轴点移动,1f代表以右移当前控件长度开始 //参数3~4:y轴的开始位置,0f代表从当前y轴点移动,1f代表下移当前控件长度开始 //参数5~6:x轴的结束位置 //参数7~8:y轴的结束位置 TranslateAnimation translateAnimation = new TranslateAnimation( Animation.RELATIVE_TO_SELF,0f, Animation.RELATIVE_TO_SELF,0f, Animation.RELATIVE_TO_SELF,0f, Animation.RELATIVE_TO_SELF,2f); translateAnimation.setDuration(2000); animationSet.addAnimation(translateAnimation); image.startAnimation(animationSet); } }}
效果图如下:
1.2 通过xml文件实现Alpha、Rotate、Scale、Translate动画
上面是在java代码中使用的使用Animation,这样的方式方便调试、运行,但是代码的重用性却不好,下面通过xml来实现Animation。
1.2.1 步骤
1) 在res文件夹下面建立anim文件夹
2) 创建xml文件,并加入set标签3) 向set标签中加入rotate,alpha,scale或者translate标签4) 使用AnimationUtils类加载xml文件1.2.2 xml实现Animation案例
建立如下图的文件格式
alpha.xml 文件rotate.xml 文件
scale.xml文件
translate.xml文件
java调用代码如下:
public class MainActivity extends Activity { private Button rotateButton = null; private Button scaleButton = null; private Button alphaButton = null; private Button translateButton = null; private ImageView image = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Interpolator t; rotateButton = (Button) findViewById(R.id.rotateButton); scaleButton = (Button) findViewById(R.id.scaleButton); alphaButton = (Button) findViewById(R.id.alphaButton); translateButton = (Button) findViewById(R.id.translateButton); image = (ImageView) findViewById(R.id.image); rotateButton.setOnClickListener(new RotateButtonListener()); scaleButton.setOnClickListener(new ScaleButtonListener()); alphaButton.setOnClickListener(new AlphaButtonListener()); translateButton.setOnClickListener(new TranslateButtonListener()); } class AlphaButtonListener implements OnClickListener { public void onClick(View v) { // 使用AnimationUtils装载动画配置文件 Animation animation = AnimationUtils.loadAnimation( MainActivity.this, R.anim.alpha); // 启动动画 image.startAnimation(animation); } } class RotateButtonListener implements OnClickListener { public void onClick(View v) { Animation animation = AnimationUtils.loadAnimation( MainActivity.this, R.anim.rotate); image.startAnimation(animation); } } class ScaleButtonListener implements OnClickListener { public void onClick(View v) { Animation animation = AnimationUtils.loadAnimation( MainActivity.this, R.anim.scale); image.startAnimation(animation); } } class TranslateButtonListener implements OnClickListener { public void onClick(View v) { Animation animation = AnimationUtils.loadAnimation( MainActivity.this, R.anim.translate); image.startAnimation(animation); } }}
1.3 动画叠加
在上面我们介绍了AlphaAnimation,RotateAnimation,ScaleAnimation,TranslateAnimation,但这些只能是单独表现的动画。如果想把这些动画融合到一起,那么应该使用AnimationSet类,AnimationSet是Animation的派生类,它主要用于将多个动画效果融合在一起。
融合的代码如下:
//定义AnimationSet对象 AnimationSet animationSet = new AnimationSet(true); //定义淡入淡出动画 AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0); //定义旋转动画 RotateAnimation rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f); rotateAnimation.setDuration(1000);//设置旋转动画的结束时间 //将rotateAnimation添加到animationSet中 animationSet.addAnimation(rotateAnimation); //将alphaAnimation添加到animationSet中 animationSet.addAnimation(alphaAnimation); //开始动画 image.startAnimation(animationSet);
如果想通过配置xml文件的方式来实现的话,只需要在<set></set>中多定义一组动画即可
1.4 动画速率
上面我们使用的所有动画速率都是默认的,
Interpolator定义了动画变化的速率,在Animations框架当中定义了一下几种InterpolatorAccelerateDecelerateInterpolator:在动画开始与结束的地方速率改变比较慢,在中间的时候速率快。AccelerateInterpolator:在动画开始的地方速率改变比较慢,然后开始加速CycleInterpolator:动画循环播放特定的次数,速率改变沿着正弦曲线DecelerateInterpolator:在动画开始的地方速率改变比较慢,然后开始减速LinearInterpolator:动画以均匀的速率改变
Interpolator在xml文件中的使用主要分为以下几种情况:
a)在set标签中
b)如果在一个set标签中包含多个动画效果,如果想让这些动画效果共享一个Interpolator。
android:shareInterpolator="true"
c)如果不想共享一个interpolator,则设置android:shareInterpolator="true",并且需要在每一个动画效果处添加interpolator。
Interpolator在java代码中的使用,又可以分为以下几种情况:
a)如果是在代码上设置共享一个interpolator,则可以在AnimationSet设置interpolator。AnimationSet animationSet = new AnimationSet(true);animationSet.setInterpolator(new AccelerateInterpolator());
b)如果不设置共享一个interpolator则可以在每一个Animation对象上面设置interpolator。
//false不使用默认的AnimationSetAnimationSet animationSet = new AnimationSet(false);alphaAnimation.setInterpolator(new AccelerateInterpolator());rotateAnimation.setInterpolator(new DecelerateInterpolator());
2.逐帧动画
Frame-By-Frame Animations(逐帧动画)是一帧一帧的格式显示动画效果。类似于电影胶片拍摄的手法。
逐帧动画依靠AnimationDrawable类,AnimationDrawable对ImageView进行动画时,原来的ImageView中是不能设置初始图片的。2.1 实现小熊快跑动画效果
下面使用小熊快跑这个动画来讲解AnimationDrawable的使用
bear.xml文件activity_main.xml文件
java代码:
public class MainActivity extends Activity { Button startbutton=null; Button endbutton=null; ImageView imageView=null; AnimationDrawable animationDrawable=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startbutton = (Button)findViewById(R.id.startbutton); imageView = (ImageView)findViewById(R.id.image); startbutton.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { imageView.setBackgroundResource(R.anim.bear); animationDrawable = (AnimationDrawable) imageView.getBackground(); animationDrawable.start(); } }); endbutton=(Button)findViewById(R.id.endbutton); endbutton.setOnClickListener(new OnClickListener() { @Override public void onClick(View paramView) { if(animationDrawable!=null){ animationDrawable.stop(); } } }); }}
效果图:
2.2 Movie 类的使用(GIF动图)
说起逐帧动画,肯定大家会想到的就是GIF动图,上面我们使用AnimationDrawable来绘制逐帧动画,但这样未免太过于麻烦,在Android API中还提供了另外一个类就是android.graphics.Movie。接下来看看这个案例:
customGifView文件
import java.io.InputStream;import android.content.Context;import android.graphics.Canvas;import android.graphics.Movie;import android.util.AttributeSet;import android.view.View;public class CustomGifView extends View { private InputStream gifInputStream; private Movie gifMovie; private int movieWidth, movieHeight; private long movieDuration; private long mMovieStart; public CustomGifView(Context context) { super(context); init(context); } public CustomGifView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CustomGifView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context){ setFocusable(true); gifInputStream = context.getResources() .openRawResource(R.drawable.YOUR_GIF); /** * Movie 提供了三个decodeXXX方法,分别是 * decodeByteArray(byte[] data, int offset, int length) * decodeFile(String pathName) * decodeStream(InputStream is) */ gifMovie = Movie.decodeStream(gifInputStream); movieWidth = gifMovie.width(); movieHeight = gifMovie.height(); //持续获得持续时间 movieDuration = gifMovie.duration(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(movieWidth, movieHeight); } public int getMovieWidth(){ return movieWidth; } public int getMovieHeight(){ return movieHeight; } public long getMovieDuration(){ return movieDuration; } @Override protected void onDraw(Canvas canvas) { long now = android.os.SystemClock.uptimeMillis(); if (mMovieStart == 0) { // first time mMovieStart = now; } if (gifMovie != null) { int dur = gifMovie.duration(); if (dur == 0) { dur = 1000; } int relTime = (int)((now - mMovieStart) % dur);//设置要被显示的帧 gifMovie.setTime(relTime); gifMovie.draw(canvas, 0, 0); invalidate(); } } }
可以在XML中使用:
修改硬件加速关闭:
android:hardwareAccelerated="false"
或
View.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
然后就可以看到GIF动图了。
3.LayoutAnimationController
LayoutAnimationController用于对layout中或View Group中的子元素进行动画的,每个子元素使用的都是相同的动画,但是每个子元素的启动时间不一样。如果想要定义自己的延迟启动时间,那么可以重写LayoutAnimationController类的getDelayForView(android.view.View)方法。
下面是使用对ListView使用LayoutAnimationController的案例,通过这个案例我们来讲解一下LayoutAnimationController是如何工作的:list_anim.xml文件list_anim_layout.xml文件
activity_main.xml文件
activity_item.xml文件
mainActivity.java文件
public class MainActivity extends Activity { private Button button = null; private ListView listView = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = (ListView)findViewById(R.id.list); button=(Button)findViewById(R.id.button); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { listView.setAdapter(createListAdapter()); } }); } private ListAdapter createListAdapter() { List> list = new ArrayList >(); HashMap m1 = new HashMap (); m1.put("name", "bauble"); m1.put("sex", "male"); HashMap m2 = new HashMap (); m2.put("name", "Allorry"); m2.put("sex", "male"); HashMap m3 = new HashMap (); m3.put("name", "Allotory"); m3.put("sex", "male"); HashMap m4 = new HashMap (); m4.put("name", "boolbe"); m4.put("sex", "male"); list.add(m1); list.add(m2); list.add(m3); list.add(m4); SimpleAdapter simpleAdapter = new SimpleAdapter( this,list,R.layout.activty_item,new String[]{"name","sex"}, new int[]{R.id.name,R.id.sex}); return simpleAdapter; }}
效果图:
4.属性动画
4.1 基本简介
属性动画是在Android3.0之后加入的,在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在该属性上的动画效果。
ValueAnimator是属性动画机制中最核心的一个类,除了ValueAnimator类,AnimatorSet、ObjectAnimator类也是属性动画机制中的核心类,其中ObjectAnimator是ValueAnimator的派生类。下面笔者会详细介绍这个三个类。4.2 ValueAnimator类
现在开始介绍ValueAnimator类的使用,ValueAnimator提供了一种简单的定时引擎,它可以计算动画的属性值,然后通过手动将变化的值设置到对象属性上。默认情况下,ValueAnimator使用的是非线性的Interpolation(AccelerateDecelerateInterpolator),AccelerateDecelerateInterpolator类对象的Interpolator在开始的时候加速,在动画结束的时候减速。也可以通过setInterpolator(TimeInterpolator)方法来设置自己的Interpolator。
valueAnimator有如下几个比较常用的方法://将开始值以浮点数值的形式过度到结束值public static ValueAnimator ofFloat (float... values)//将开始值以整数的形式过度到结束值public static ValueAnimator ofInt (int... values)//使用指定的TypeEvaluator对象,将 开始值以对象的形式过度到结束值。public static ValueAnimator ofObject (TypeEvaluator evaluator, Object... values)//开始动画public void start();//结束动画public void end();//取消动画public void cancle();//添加更新监听器public void addUpdateListener (ValueAnimator.AnimatorUpdateListener listener)//设置自定义的TimerInterpolatorpublic void setInterpolator (TimeInterpolator value)//设置执行时间public ValueAnimator setDuration (long duration)//设置启动的延迟时间public void setStartDelay (long startDelay)
下面通过改变按钮的宽度来展示ValueAnimator的用法,
xml布局如下:
java代码如下:
public class MainActivity extends Activity { Button button=null; ValueAnimator valueAnimator=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button=(Button)findViewById(R.id.button1); //对指定Button对象的with属性创建ValueAnimator对象 valueAnimator = ValueAnimator.ofInt(button.getLayoutParams().width, 500); //设置持续时间 valueAnimator.setDuration(2000); //添加AnimatorUpdateListener监听器,每当属性值改变就会调用onAnimationUpdate方法 valueAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { //获得变化的值 int currentValue = (Integer) animator.getAnimatedValue(); Log.i("info", currentValue+""); //重新设置属性值 button.getLayoutParams().width=currentValue; //重新显示 button.requestLayout(); } }); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View paramView) { valueAnimator.start(); } }); }}
效果图:
观察上面的代码我们知道,ValueAnimator只是负责值的改变,若想要把改变后的值重新赋给对象,那么这个过程应该由程序员应该手动来完成。幸好官方提供了ObjectAnimator类,来帮我们实现这个过程。4.3 ObjectAnimator类
ObjectAnimator是ValueAnimator的派生类,它对ValueAnimator进行了改进,它可以直接对对象的属性进行动画设置,但是被动画的属性必须要提供set/get方法。
由于ObjectAnimator派生自ValueAnimator,ObjectAnimator能使用ValueAnimator中的大部分方法(除private外)。
下面这个案例展示了利用ObjectAnimator来实现旋转、平移、缩放、淡入淡出
xml文件布局如下:
java的调用代码如下:
public class MainActivity extends Activity { Button move=null; Button rotate=null; Button scale=null; Button alpha=null; ImageView image=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); move= (Button)findViewById(R.id.move); rotate=(Button)findViewById(R.id.rotate); scale=(Button)findViewById(R.id.scale); alpha=(Button)findViewById(R.id.alpha); image=(ImageView)findViewById(R.id.image); move.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(image,"TranslationX",0,100,0);//移动 objectAnimator.setDuration(2000); objectAnimator.start(); } }); rotate.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(image,"Rotation",0,360);//旋转 objectAnimator.setDuration(2000); objectAnimator.start(); } }); scale.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(image,"ScaleX",1,2,1);//缩放 objectAnimator.setDuration(2000); objectAnimator.start(); } }); alpha.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(image,"Alpha",1,0,1);//淡入淡出 objectAnimator.setDuration(2000); objectAnimator.start(); } }); }}
效果图:
下面介绍一些常用的属性的名:属性 | 作用 | 数值类型 |
Alpha | 控制View的透明度 | float |
TranslationX | 控制X方向的位移 | float |
TranslationY | 控制Y方向的位移 | float |
ScaleX | 控制X方向的缩放倍数 | float |
ScaleY | 控制Y方向的缩放倍数 | float |
Rotation | 控制以屏幕方向为轴的旋转度数 | float |
RotationX | 控制以x轴为轴的旋转度数 | float |
RotationY | 控制以Y轴为轴的旋转度数 | float |
4.4 AnimatorSet类
AnimatorSet类可以实现Animation的组合动画,其中的Animation可以按照指定的顺序或交叉方式进行显示。
该类有一些常用方法:AnimatorSet.play(Animator anim) :播放当前动画AnimatorSet.after(long delay) :将现有动画延迟x毫秒后执行AnimatorSet.with(Animator anim) :将现有动画和传入的动画同时执行AnimatorSet.after(Animator anim) :将现有动画插入到传入的动画之后执行AnimatorSet.before(Animator anim) :将现有动画插入到传入的动画之前执行
下面是一个混合旋转和移动动画案例:
xml布局文件:java代码:
public class MainActivity extends Activity { ImageView image=null; Button start=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); image=(ImageView)findViewById(R.id.image); start=(Button)findViewById(R.id.start); start.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ObjectAnimator transX=ObjectAnimator.ofFloat(image, "TranslationX", 0,100,0); ObjectAnimator transY=ObjectAnimator.ofFloat(image, "TranslationY", 0,100,0); ObjectAnimator rotate=ObjectAnimator.ofFloat(image, "Rotation", 0,360); AnimatorSet animatorSet=new AnimatorSet(); Builder builder= animatorSet.play(transX).with(transY);//移动 builder.before(rotate);//旋转 animatorSet.setDuration(2000); //启动 animatorSet.start(); } }); }}
效果图:
4.5 估值器(TypeEvaluator)
接下来我们继续讲解估值器,估值器(TypeEvaluator)是一个接口,该接口允许开发人员自定义动画中的属性值。
TypeEvaluator有一些已知的实现类,例如ArgbEvaluator、FloatEvaluator、IntEvaluator、RectEvaluator、PointEvaluator等等。其中Argb是在用于计算Argb之间的值时使用的,RectEvaluator是在用于计算Rect之间的值时使用的,FloatEvaluator是在用于计算Float之间值使用的,IntEvaluator是在用于计算Int之间值时使用的。
TypeEvaluator接口只有一个抽象方法:
public abstract T evaluate (float fraction, T startValue, T endValue);
TypeEvaluator只有一个抽象方法evaluate。evaluate中包含三个形参,其中startValue形参表示动画的结束值,endValue形参表示动画的结束值。由于startValue,endValue以及方法的返回值都是泛型类型,所以我们可以在TypeEvaluator的实现类中为泛型指定任何类型,比如Integer,Double,或者自定义的类型,显然该类型必须和属性动画中的动画属性保持一致。
evaluate方法中有一个形参比较特殊,它就是fraction,fraction形参是float类型。fraction表示单个动画的完成比例(重复动画可以认为是单个动画的多次运行),它的值是[0~1],当fraction = 0时表示单个动画还未开始,fraction = 1表示单个动画的已经完成。当我们使用属性动画类(ValueAnimator或ObjectAnimator类)运行动画时,属性动画类会根据当前动画的运行时间(Elapsed time)和当前动画的运行速率(Interpolator),得出动画的完成比例,在调用我们的TypeEvaluator的实现类时把它传给fraction参数。因此在我们使用fraction参数计算时,无需额外考虑运行速率。
有了这些概念后,我们来自定义一个CircleEvaluator:
Circle.java文件package com.bean;public class Circle { /** * 圆心的横坐标 */ private float x; /** * 圆心的纵坐标 */ private float y; /** * 圆的半径 */ private float radius; public Circle(float x,float y,float radius) { this.x=x; this.y=y; this.radius=radius; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; }}
CircleEvaluator.java 文件
package com.evaluator;import com.bean.Circle;import android.animation.TypeEvaluator;public class CircleEvaluator implements TypeEvaluator{ @Override public Circle evaluate(float fraction, Circle startValue, Circle endValue) { //圆心的横坐标 float x=startValue.getX()+fraction*(endValue.getX()-startValue.getX()); //圆心的纵坐标 float y=startValue.getY()+fraction*(endValue.getY()-startValue.getY()); //圆的半径 float radius=startValue.getRadius()+fraction*(endValue.getRadius()-startValue.getRadius()); return new Circle(x, y, radius); }}
CircleView.java 文件
package com.entry;import com.bean.Circle;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Paint.Style;import android.view.View;public class CircleView extends View { private Circle circle=null; public CircleView(Context context,Circle circle) { super(context); this.circle=circle; } @Override protected void onDraw(Canvas canvas) { Paint paint=new Paint(); paint.setStyle(Style.STROKE); canvas.drawCircle(circle.getX(), circle.getY(), circle.getRadius(),paint); super.onDraw(canvas); } public Circle getCircle() { return circle; } public void setCircle(Circle circle) { this.circle = circle; }}
MainActivity.java 文件
package com.entry;import com.bean.Circle;import com.evaluator.CircleEvaluator;import android.os.Bundle;import android.animation.ObjectAnimator;import android.animation.ValueAnimator;import android.animation.ValueAnimator.AnimatorUpdateListener;import android.app.Activity;import android.view.Menu;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.RelativeLayout;public class MainActivity extends Activity { RelativeLayout layout=null; Button button=null; CircleView circleView=null; Circle startCircle=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startCircle=new Circle(200,200,60); circleView=new CircleView(this,startCircle); layout=(RelativeLayout)findViewById(R.id.main); layout.addView(circleView); button=(Button)findViewById(R.id.button1); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { CircleEvaluator circleEvaluator= new CircleEvaluator(); Circle endCircle=new Circle(250, 300, 100); ObjectAnimator objectAnimator=ObjectAnimator.ofObject(circleView,"circle",circleEvaluator,startCircle,endCircle); objectAnimator.setDuration(2000);//两秒内完成 objectAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { circleView.invalidate(); } }); objectAnimator.start(); } }); }}
效果图:
4.6 实现贝塞尔曲线动画
在开始之前,介绍一下Path类,Path类中提供了一个quadTo(float x1, float y1, float x2, float y2)方法,该方法可以用于绘制二阶贝塞尔曲线。
笔者介绍三个贝塞尔曲线的网站:,,
贝塞尔曲线的应用非常广泛,栗如:
QQ小红点拖拽效果360火箭发射加入购物车动画一些炫酷的下拉刷新控件阅读软件的翻书效果一些平滑的折线图的制作很多炫酷的动画效果
这里对贝塞尔曲线不做过多的解释,关于贝塞尔曲线读者可以自行度娘。
下面介绍实现贝塞尔曲线动画的思想:a)自定义估值器(TypeEvaluator),在public abstract T evaluate (float fraction, T startValue, T endValue);方法中利用贝塞尔公式计算出下一个图形的属性的值。b)对组件动画对象设置新的值,如果是ObjectValuator的话,这一步可以省略,在上面的分析中已经知道ObjectValuator会自动帮我们完成这一步。c)调用invalidate()重新刷新组件。接下来我们要利用贝塞尔曲线,实现如下这样的功能:结构图:Heart.java 文件
package heart.model;public class Heart { /** * 横坐标 */ private float x=0; /** * 纵坐标 */ private float y=0; /** * 颜色值 */ private int color=0; public Heart(float x,float y,int color){ this.x=x; this.y=y; this.color=color; } public int getColor() { return color; } public void setColor(int color) { this.color = color; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; }}
HeartView.java 文件
package heart.model;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Paint.Style;import android.graphics.Path;import android.view.View;public class HeartView extends View{ private Heart heart=null; public float getAlpha() { return super.getAlpha(); } public void setAlpha(float alpha) { super.setAlpha(alpha); } public Heart getHeart() { return heart; } public void setHeart(Heart heart) { this.heart = heart; } private final float h=40; public HeartView(Context context,Heart heart){ super(context); this.heart=heart; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint p=new Paint(); //设置透明度 p.setAlpha((int) getAlpha()); //设置样式 p.setStyle(Style.FILL); //设置画笔颜色 p.setColor(heart.getColor()); //使用二阶贝塞尔曲线绘制心形图案 Path path=new Path(); float x=heart.getX();//获得起始点的横坐标 float y=heart.getY();//获得起始点的纵坐标 path.moveTo(x,y); path.quadTo(x+h,y-0.268f*h,x,y+h); path.moveTo(x,y+h); path.quadTo(x-h,y-0.268f*h,x,y); canvas.drawPath(path,p); }}
HeartEvaluator.java 文件
package heart.model;import java.util.Random;import android.animation.TypeEvaluator;public class HeartEvaluator implements TypeEvaluator{ private int orientation=1; private static Random r=new Random(System.currentTimeMillis()); public HeartEvaluator(){ orientation=getOrientation(); } /** * 利用三阶贝塞尔曲线公式,得出位置。 */ @Override public Heart evaluate(float t, Heart startValue, Heart endValue) { Heart[] points=createPoints(startValue,endValue); Heart p0=points[0];//开始点 Heart p1=points[1];//第一个辅佐点 Heart p2=points[2];//第二个辅佐点 Heart p3=points[3];//终点 //使用三阶贝塞尔曲线算出横坐标 Double dx= p0.getX()*Math.pow((1-t),3)+ 3*p1.getX()*t*Math.pow((1-t), 2)+ 3*p2.getX()*Math.pow(t, 2)*(1-t)+ p3.getX()*Math.pow(t, 3); float x=Float.parseFloat(dx.toString()); //使用三阶贝塞尔曲线算出纵坐标 Double dy= p0.getY()*Math.pow((1-t),3)+ 3*p1.getY()*t*Math.pow((1-t), 2)+ 3*p2.getY()*Math.pow(t, 2)*(1-t)+ p3.getY()*Math.pow(t, 3); float y=Float.parseFloat(dy.toString()); return new Heart(x,y,startValue.getColor()); } /** * 根据起始点和终点 算出其余两个辅佐点, * 算法自定义 * @param heart0 开始点 * @param heart3 终点 * @return */ public Heart[] createPoints(Heart heart0,Heart heart3){ float wx=Math.abs(heart0.getX()-heart3.getX()); Heart heart1=new Heart(heart3.getX(), heart0.getY()-wx, heart0.getColor()); Heart heart2=new Heart(heart0.getX(),wx,heart3.getColor()); return new Heart[]{heart0,heart1,heart2,heart3}; } /** * 获得飘动的方向 * @return 一个Int类型的数据,数字为1或是-1。 */ private int getOrientation(){ if(r.nextFloat()>=0.5){ return 1; } return -1; }}
在这个HeartEvaluator估值器中,我们使用三阶贝塞尔曲线公式,实现单个图形按如下路径移动:
三阶贝塞尔曲线的公式为:
HeartAnimation.java 文件
package heart.model;import java.util.ArrayList;import java.util.List;import java.util.Random;import java.util.Timer;import java.util.TimerTask;import android.animation.Animator;import android.animation.Animator.AnimatorListener;import android.animation.AnimatorSet;import android.animation.ObjectAnimator;import android.animation.ValueAnimator;import android.animation.ValueAnimator.AnimatorUpdateListener;import android.content.Context;import android.graphics.Color;import android.os.Handler;import android.os.Message;import android.view.ViewGroup;public class HeartAnimation { private int width; private int height; private ListheartViews=null; private static Random random=new Random(); private ViewGroup parent=null; private Context context=null; private static final int COLOR_LIMIT=4; /** * 有参数构造器 * @param width 宽度 * @param height 高度 * @param parent 容器组件 * @param context 上下文对象 */ public HeartAnimation(int width,int height,ViewGroup parent,Context context){ this.width=width; this.height=height; this.parent=parent; this.context=context; heartViews=new ArrayList (); } /** * 设置需要显示图形的个数 * @param count 图形的个数 * @return 返回一个HeartAnimation类型的数据,当前对象。 */ public HeartAnimation setCount(int count){ if(heartViews!=null) heartViews.clear(); for(int i=0;i
笔者在addAnimation方法中,同时播放alpha和Heart的动画,然后监听addUpdateListener,在有新值后,重新设置新值。
Mainctivity.java 文件
package heart.entry;import heart.model.HeartAnimation;import cn.heart.R;import android.os.Bundle;import android.widget.RelativeLayout;public class MainActivity extends BaseActivity { RelativeLayout relativeLayout=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ViewShowListen(R.id.layout, 10001); } @Override public void ViewAfterShow(final int width,final int height) { relativeLayout=(RelativeLayout)findViewById(R.id.layout); HeartAnimation heartAnimation=new HeartAnimation(width, height,relativeLayout,MainActivity.this); heartAnimation.setCount(20).start(); }}
5.View、surfaceView和GLSurfaceView的区别
5.1 View、surfaceView和GLSurfaceView的区别
Android游戏当中主要的除了控制类外就是显示类View。SurfaceView是从View基类中派生出来的显示类。android游戏开发中常用的三种视图是:
view、SurfaceView和GLSurfaceView的区别如下:
View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快。GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。在2D游戏开发中,大致可以分为两种游戏框架,View和SurfaceView。View和SurfaceView区别: View:必须在UI的主线程中更新画面,用于被动更新画面。 surfaceView:UI线程和子线程中都可以。在一个新启动的线程中重新绘制画面,主动更新画面。UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步,涉及到线程同步。所以基于以上,根据游戏特点,一般分成两类。
1 被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。2 主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制上面的所有的案例的,笔者都是用的View来实现的(这并不是最理想的选择,尤其是最后一个贝塞尔曲线动画),在知道了这一节的知识后,读者可以尝试使用SurfaceView来实现那些2D动画。
5.2 SurfaceView
5.2.1 SurfaceView的使用
1.创建SurfaceView,需要创建一个新的扩展了SurfaceView的类,并实现SurfaceHolder.Callback
2.需要重写的方法
(1)public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){}//在surface的大小发生改变时激发(2)public void surfaceCreated(SurfaceHolder holder){}//在创建时激发,一般在这里调用画图的线程。(3)public void surfaceDestroyed(SurfaceHolder holder) {}//销毁时激发,一般在这里将画图的线程停止、释放。整个过程:继承SurfaceView并实现SurfaceHolder.Callback接口 ----> SurfaceView.getHolder()获得SurfaceHolder对象 ---->SurfaceHolder.addCallback(callback)添加回调函数---->SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布----> Canvas绘画 ---->SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。3、SurfaceHolder
这里用到了一个类SurfaceHolder,可以把它当成surface的控制器,用来操纵surface。处理它的Canvas上画的效果和动画,控制表面,大小,像素等。几个需要注意的方法:(1)、abstract void addCallback(SurfaceHolder.Callback callback);// 给SurfaceView当前的持有者一个回调对象。(2)、abstract Canvas lockCanvas();// 锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。(3)、abstract Canvas lockCanvas(Rect dirty);// 锁定画布的某个区域进行画图等..因为画完图后,会调用下面的unlockCanvasAndPost来改变显示内容。// 相对部分内存要求比较高的游戏来说,可以不用重画dirty外的其它区域的像素,可以提高速度。(4)、abstract void unlockCanvasAndPost(Canvas canvas);// 结束锁定画图,并提交改变。栗子:public class MainActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new MyView(this)); } class MyView extends SurfaceView implements SurfaceHolder.Callback,Runnable{ boolean mRunning=false; SurfaceHolder holder=null; Thread td=null; public MyView(Context context) { super(context); holder=getHolder(); holder.addCallback(this); setFocusable(true); setFocusableInTouchMode(true); this.setKeepScreenOn(true); } @Override public void surfaceCreated(SurfaceHolder holder) { mRunning=true;//允许开始绘制 td=new Thread(this); td.start();//开始线程 } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { mRunning=false;//停止 } @Override public void run() { Canvas c = null; int count=0; while(mRunning){ try{ c = holder.lockCanvas();//锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。 c.drawColor(Color.BLACK);//设置画布背景颜色 Paint p = new Paint(); //创建画笔 p.setColor(Color.WHITE); Rect r = new Rect(100, 50, 300, 250); c.drawRect(r, p); c.drawText("这是第"+(count++)+"秒", 100, 310, p); Thread.sleep(1000);//睡眠时间为1秒 }catch(Exception e){ e.printStackTrace(); }finally{ if(c!= null) holder.unlockCanvasAndPost(c);//结束锁定画图,并提交改变。 } } } }}
5.2.2 SurfaceView的分层
在使用Surface类开发时,常常需要使绘制出来的图案背景色透明,以实现背景图片和绘制出来的图案融为一体,具体操作方法如下:
首先继承surfaceview类的子类(即你写的类)的构造方法中设置背景图片:
setBackgroundResource(R.drawable.background);
再加入下面这两行:
setZOrderOnTop(true);//使surfaceview放到最顶层 getHolder().setFormat(PixelFormat.TRANSLUCENT);//使窗口支持透明度
然后在绘制方法(一般为onDraw())中加入:
canvas.drawColor(Color.TRANSPARENT,Mode.CLEAR);//绘制透明色
在Surface开发中,是常常需要进行分层开发的,这时候可能需要在界面上加入好几个的Surface的实现类。
比如:
RelativeLayout rl=(RelativeLayout)findViewById(R.id.idVal); rl.addView(SurfaceSubClass1); rl.addView(SurfaceSubClass2);
如果SurfaceSubClass2覆盖了SurfaceSubClass1的图案,那么这显然不是我们希望的,所以应该把SurfaceSubClass2设置为背景透明。
上面讨论是进行两层的开发,那么如果是需要进行三层或是三层以上分层开发,那么应该怎么办呢?
例如:
RelativeLayout rl=(RelativeLayout)findViewById(R.id.idVal); rl.addView(SurfaceSubClass1);//1 rl.addView(SurfaceSubClass2);//2 rl.addView(SurfaceSubClass3);//3
上面的代码中,读者知道1,2,3条语句的哪一个SurfaceSubClass最先被加载到RelativeLayout(三个SurfaceSubClass都没有设置setZOrderOnTop(true))中吗?答案是SurfaceSubClass3,注意这里的三个SurfaceSubClass被加载到RelativeLayout中的顺序是3-2-1。知道了这一点后,进行多层开发就简单了,比如上面的三个SurfaceSubClass中,SurfaceSubClass3应该是最底层的图案(不需要设置透明),再上面一层应该是SurfaceSubClass2的图案(需要设置为透明,否则看不到底层图案),最上面一层是SurfaceSubClass1的图案(需要设置为透明),注意:这三个SurfaceSubClass都没有设置setZOrderOnTop(true)。