有些事情,去做了,要把收获记录下来,记录下来的才是自己真正得到的
这节我们来学一下android中的动画,这可是做游戏必备的,不过说起做游戏,可能很多人很感兴趣,但是其实真正的游戏大多是用游戏引擎做的,所谓游戏引擎,就是一个专门用来做游戏的框架或者类库,用它可以很方便地实现一些游戏效果。不过我们这里学习的android动画,也可以做一些小游戏了。
Android主要支持两种动画,渐变动画和逐帧动画。但是之前,我们总是点击一个list的item跳到另一个Activity,然后来演示每一种功能,这次我们换一换,介绍一个新的东西,TabHost。
我们先来看下tabHost的效果:
怎么样,还是不错的吧?那这是怎么做到的呢?看代码:
public class MainActivity extends TabActivity {
private TabHost tabHost = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
tabHost = this.getTabHost();
TabHost.TabSpec tabSpec = null;
Intent intent = null;
Resources resources = getResources();
intent = new Intent();
intent.setClass(MainActivity.this, FrameAnimActivity.class);
tabSpec = tabHost.newTabSpec("frame animation")
.setIndicator("逐帧动画", resources.getDrawable(R.drawable.icon_frame))
.setContent(intent);
tabHost.addTab(tabSpec);
intent = new Intent();
intent.setClass(MainActivity.this, TweenAnimActivity.class);
tabSpec = tabHost.newTabSpec("tween animation")
.setIndicator("渐变动画", resources.getDrawable(R.drawable.icon_tween))
.setContent(intent);
tabHost.addTab(tabSpec);
}
}
首先,我们的Activity继承了TabActivity,然后我们通过getTabHost得到TabHost,然后,我们new了一个tabSpec,设置tag、indicator、drawable和content,最后把他加到tabhost中,这样一个tab就设置好了。
下面我们先来看一下逐帧动画,其实这个过程我们似曾相识,我们在res下建立anim文件夹,再在anim文件夹下面建立动画文件frame.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/person01" android:duration="300" />
<item android:drawable="@drawable/person02" android:duration="300" />
<item android:drawable="@drawable/person03" android:duration="300" />
<item android:drawable="@drawable/person04" android:duration="300" />
</animation-list>
这个文件内容很好理解,我们让四幅画逐帧播放,每帧间隔300ms,这里android:oneshot="false"表示循环播放,如果为true则只播放一次。但是这个动画文件怎么引用呢?如果我们像progressBar的indeterminateDrawable这样引用:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@anim/frame"
/>
动画是不会有反应的,因为我们还没有启动动画,我们还需要加上:
animationDrawable = (AnimationDrawable) imageView.getBackground();
animationDrawable.start();
把这两句话加在适当的位置,一个是用来获取AnimationDrawable的,一个是用来启动动画的。我们来看一下效果:
但是这个效果有个明显的缺点,当我们停止后,再开始,动画并不是接着开始,而是重新开始。我看了下源码,是因为里面的mCurFrame在stop的时候变成-1了,貌似是因为isRunning这个函数要用mCurFrame==-1来判断是否停止。而且AnimationDrawable这个类也没有类似setFrame这种函数,想要在停止后让动画从停止的那一帧继续下去还真是不易。
我个人觉得这个类是android的一个败笔,既然为逐帧动画设计的类,怎么可用性这么差呢?这里吐槽一下,有高手知道原因的也请告诉我,这个AnimationDrawable类的设计出发点?这个类除了使用这种xml加载动画的方法,还有一个方法,用纯代码来加载:
animationSourceDrawable = new AnimationDrawable();
for(int i=1; i<=4; i++){
String str = String.format("person%02d", i);
int id = getResources().getIdentifier(str, "drawable", getPackageName());
animationSourceDrawable.addFrame(getResources().getDrawable(id), 300);
}
animationSourceDrawable.setOneShot(false);
不过用代码来加载还不如用xml方便,你可能会发现很多坑,所以还是尽量用xml加载吧。
下面我们自己来实现这个所谓的停止后继续功能,其实逐帧动画很简单,无非就是隔一定时间换一张图片,我们完全可以用异步任务来完成这项功能。
这里,主要是我们异步任务在onStop的时候应该干什么?我们之前学习过异步任务,但是似乎没有提到它的停止,其实它的停止没有我们想象中那么简单,一般是用一个标志位boolean来标志线程是否需要停止,然后在doInBackground中判断标志位。熟悉Java多线程的同学一定不陌生。在这里,我们为了简单,让异步任务一开始就执行起来,然后用标志位来判断是否要启动动画就ok了。这里我把异步任务的代码贴出来,其他部分请参考我的实例程序。
public class AnimationAsyncTask extends AsyncTask<Void, Void, Void> {
private Context context = null;
private boolean isRunAnimation = false;
public AnimationAsyncTask(Context context) {
// TODO Auto-generated constructor stub
this.context = context;
}
public void setRunAnimation(boolean isRun) {
isRunAnimation = isRun;
}
@Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
while(true) {
try {
if(isRunAnimation) {
publishProgress();
}
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
protected void onProgressUpdate(Void... values) {
// TODO Auto-generated method stub
super.onProgressUpdate(values);
((FrameAnimActivity)context).stepAsyncFrame();
}
}
但是这么一个功能用异步任务似乎有点大材小用了,其实还有一个更简单的方式,Handler,handler有两种用法,这里我们先来介绍一种。
handler = new Handler();
handlerRunnable = new Runnable() {
public void run() {
// TODO Auto-generated method stub
if(isHandlerAnimationRun) {
stepHandlerFrame();
handler.postDelayed(this, 300);
}
}
};
我们定义一个Runnable,在里面再调用handler的postDelayed方法,这个方法需要一个Runnable和一个时间,意思是,延迟300ms再调用Runnable中的run方法。在这里,我们把handlerRunnable自身传进去,这样就无限循环了,当然要在isHandlerAnimationRun为true的时候。
startHandlerFrameBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// TODO Auto-generated method stub
isHandlerAnimationRun = true;
handler.postDelayed(handlerRunnable, 300);
startHandlerFrameBtn.setVisibility(View.GONE);
stopHandlerFrameBtn.setVisibility(View.VISIBLE);
}
});
stopHandlerFrameBtn.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// TODO Auto-generated method stub
isHandlerAnimationRun = false;
startHandlerFrameBtn.setVisibility(View.VISIBLE);
stopHandlerFrameBtn.setVisibility(View.GONE);
}
});
这是我们的按钮绑定监听的代码,这个效果和用异步任务的几乎一样,但是,这种方法的Handler是在同一个线程中调用run方法,也就是说,handlerRunnable的run方法是UI线程在调用,所以我们才可以在里面调用stepHandlerFrame来改变imageView的图片。比起异步任务来,我们少了一个线程的开销,岂不是很爽?关于Handler,以后会有专题,这里我们只要知道一个大概就ok。
好了,逐帧动画就先讲到这里,下面我们来看看渐变动画。渐变动画大概可以分为alpha渐变(透明度渐变)、scale渐变(大小变化)、translate渐变(位置变化)和rotate渐变(旋转变化)。我们先来看alpha渐变:同样是定义anim资源
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="1000"
/>
这里的资源表示从完全不透明到完全透明,动画时间为1s。我们怎么使用这个资源呢?
Animation anim = AnimationUtils.loadAnimation(TweenAnimActivity.this, R.anim.alpha);
imageView.startAnimation(anim);
动画可以应用在任何View上面。其他动画我们也来看看吧。Scale:
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"
/>
fromXScale和toXScale指定了x方向从多大的size到多大的size,Y方向类似,这里说一下pivotX,这是指定缩放的锚点,这里是相对自身,也就是说这个动画已View的中心为锚点进行缩放。如果你对锚点不了解,你可以想象一下Word里面的图形,我们缩放的时候,是不是默认中心有一个小圆圈,这个小圆圈是不会变的,其他的地方都按比例缩放,这个小圆圈就是锚点。网上有些人说可以用0.5,但是我在我手机上试了不行,建议大家用50%,保证适配性。
再看看Translate:
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXDelta="-50%p"
android:toXDelta="50%p"
android:fromYDelta="-50%p"
android:toYDelta="50%p"
android:duration="3000"
/>
这里的fromXDelta使用了-50%p,这表示50%是相对于父控件来说的,这个配置的意思是,向左上退父控件长宽的一半,然后移动到右下父控件长宽的一半,注意,这里并不是从父控件的-50%开始移动,而是以View现在的位置算起,往后退父控件的50%这么个长度,大家可以下载我的例子运行看看。
最后来看看rotate:
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:duration="1000"
/>
这样就是转360度咯,但是请注意,我们没有设置锚点,这时,默认锚点是0,0,也就是已左上角为中心旋转,我们如果想以小人为中心旋转怎么办呢?相信你已经想到了:
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"
/>
说到动画还有一个东西要说一下,就是Interpolator,这个东西是专门来指定动画运动的速度的,怎么说呢?我们在translate上加一个试试:android:interpolator="@android:anim/accelerate_interpolator",我们看看效果,是不是发现移动并不是匀速移动了,而是越来越快呢?这就是accelerate_interpolator的效果,当然还有很多其他的Interpolator,不一一介绍了,还可以自定义Interpolator,以后有需求再介绍吧,毕竟不是重点。
除了用xml来配置,我们也可以使用纯代码来设置动画,也很简单,我们以alpha为例:
Animation anim = new AlphaAnimation(1.0f, 0.0f);
anim.setDuration(1000);
imageView.startAnimation(anim);
其他代码类似,大家可以参考我的例子程序,这里不赘述了。
我们能不能在动画开始和结束时做一些动作呢?答案是可以的,我们可以用到AnimationListener,我们在rotate中加上一个AnimationListener。
anim.setAnimationListener(new AnimationListener() {
public void onAnimationStart(Animation animation) {
// TODO Auto-generated method stub
System.out.println("animation start");
}
public void onAnimationRepeat(Animation animation) {
// TODO Auto-generated method stub
System.out.println("animation repeat");
}
public void onAnimationEnd(Animation animation) {
// TODO Auto-generated method stub
System.out.println("animation end");
}
});
这个Listener很简单,一看就明白,有3个回调,分别是动画开始,动画重复,动画结束,基本上满足了我们的要求。对了,我们怎么让动画重复呢?可以调用anim.setRepeatCount(2);,这样就是重复2次,注意这里总共转3圈哦!那无限重复呢?肯定是用一个特殊宏咯,Animation.INFINITE。
这个AnimationListener还是非常有用的,比如说你的动画开始时要一个提示,每次重复动画提示也变化,动画结束提示就消失,这些功能都能用AnimationListener来实现,非常方便。
但是到这里,我们还是有很多需求没有被满足,比如我想一边移动一边变大,怎么办呢?这就要用到set了。
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:fromXScale="1.0"
android:toXScale="2.0"
android:fromYScale="1.0"
android:toYScale="2.0"
android:duration="1000"
/>
<translate
android:fromXDelta="-40%p"
android:toXDelta="40%p"
android:fromYDelta="-40%p"
android:toYDelta="40%p"
android:duration="1000"
/>
</set>
这时候我们再来加载动画:
AnimationSet anim = (AnimationSet) AnimationUtils.loadAnimation(TweenAnimActivity.this, R.anim.set);
imageView.startAnimation(anim);
加载过程还是一样的,我们发现小人在移动的同时变大了。
用代码同样可以实现这一过程,它是类似这样的代码:
Animation alpha = new AlphaAnimation(1.0f, 0.0f);
alpha.setDuration(1000);
Animation rotate = new RotateAnimation(0.0f, 360.0f);
rotate.setDuration(1000);
AnimationSet set = new AnimationSet(false);
set.addAnimation(alpha);
set.addAnimation(rotate);
imageView.startAnimation(set);
最后讲一下我们的一个新的布局,大家先看下我的丑陋界面:
下面的按钮是怎么布局的呢?其实就是TableLayout,这是表格布局,之前没有讲过的,它用起来其实很简单:
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TableRow >
<Button
android:id="@+id/alphaButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/alpha"
/>
<Button
android:id="@+id/alphaSourceButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/alphaSource"
/>
</TableRow>
<TableRow >
<Button
android:id="@+id/scaleButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/scale"
/>
<Button
android:id="@+id/scaleSourceButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/scaleSource"
/>
</TableRow>
<TableRow >
<Button
android:id="@+id/translateButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/translate"
/>
<Button
android:id="@+id/translateSourceButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/translateSource"
/>
</TableRow>
<TableRow >
<Button
android:id="@+id/rotateButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/rotate"
/>
<Button
android:id="@+id/rotateSourceButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/rotateSource"
/>
</TableRow>
<TableRow >
<Button
android:id="@+id/setButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/set"
/>
<Button
android:id="@+id/setSourceButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/setSource"
/>
</TableRow>
</TableLayout>
后面的高深用法以后用到再讲好啦!
总结一下:
1、 TabHost的基本用法
2、 AnimationDrawable的用法,个人觉得不好用
3、 逐帧动画的替代方法,用异步任务或者Handler
4、 四种Tween动画的xml配置
5、 Interpolator
6、 利用代码实现四种Tween动画
7、 AnimationListener
8、 AnimationSet
9、 TableLayout的基本用法
这一节知识点很多,讲得较简单,希望大家好好研究下例子程序,例子程序在:http://download.csdn.net/detail/yeluoxiang/7473345,欢迎大家下载。
噢,在测试的时候发现一个bug,我们用异步任务来实现Frame Animation的时候,线程并没有关闭,如果我们在doInBackground中输出:System.out.println(Thread.currentThread().getId()+ ":running");,我们打开程序,然后按返回键退出,再打开,多弄几次,你看看输出:
好几个线程在后面跑,这样一来,过不了多久系统资源就被大量浪费了。所以我们在Activity destroy的时候,要关闭异步任务。
代码做了调整,加上了isRunning变量,标识线程是否正在运行。增加了stop方法,具体如下:
public class AnimationAsyncTask extends AsyncTask<Void, Void, Void> {
private Context context = null;
private boolean isRunAnimation = false;
private boolean isRunning = false;
public AnimationAsyncTask(Context context) {
// TODO Auto-generated constructor stub
this.context = context;
isRunning = true;
}
public void setRunAnimation(boolean isRun) {
isRunAnimation = isRun;
}
@Override
protected Void doInBackground(Void... params) {
// TODO Auto-generated method stub
while(isRunning) {
try {
if(isRunAnimation) {
publishProgress();
}
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId() + ":running");
}
return null;
}
@Override
protected void onProgressUpdate(Void... values) {
// TODO Auto-generated method stub
super.onProgressUpdate(values);
((FrameAnimActivity)context).stepAsyncFrame();
}
public void stop() {
isRunning = false;
}
}
然后在FrameAnimActivity中加入:
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
animationAsyncTask.stop();
super.onDestroy();
}
请大家在代码中自己调整。