李华明Himi 原创,转载务必在明显处注明:
很多童鞋说我的代码运行后,点击home或者back后会程序异常,如果你也这样遇到过,那么你肯定没有仔细读完Himi的博文,第十九篇Himi专门写了关于这些错误的原因和解决方法,这里我在博客都补充说明下,省的童鞋们总疑惑这一块;请点击下面联系进入阅读:
【Android游戏开发十九】(必看篇)SurfaceView运行机制详解—剖析Back与Home按键及切入后台等异常处理!
之前在【Android2D游戏开发之四】中我给大家介绍了一张13帧的png的图,利用设置可视区域的方式来实现动画效果,但是这些属于我们自己来实现动画的方式,其实Android给我们的有两类自定义动画方式:
第一类:Frame By Frame 帧动画( 不推荐游戏开发中使用)
所谓帧动画,就是顺序播放事先做好的图像,类似于放电影;
分析: 此种方式类似我之前的那种利用设置可视区域的方式来实现动画效果,不仅类似而且还不如!所以此种方式在此不予分析;
第二类:Tween Animation 渐变动画
即通过对对象不断做图像变换(平移、缩放、旋转)产生动画效果!实现方式其实就是预先定义一组指令,这些指令指定了图形变换的类型、触发时间、持续时间。这些指令可以是以 XML 文件方式定义,也可以是以源代码方式定义。程序沿着时间线执行这些指令就可以实现动画 效果。
总结:那么在Android 游戏开发中我们优先选用两种方式:第一种设置可视区域的方式来实现动画效果(帧动画),需要童鞋们手动实现,那么在之前我的博文【Android2D游戏开发之四】中已经有了相应的源码!大家可以去下载研究;那么这里就主要为大家详细分析 Tween Animation!
在讲述SurfaceView添加动画之前,我们先来看看在View中如何实现Tween Animation以及Tween 中的四种效果;
MyViewAnimation .java
package com.himi.frameAnimation; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.view.KeyEvent; import android.view.View; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.RotateAnimation; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; /** *@author Himi *@AlphaAnimation 渐变透明度动画效果 *@ScaleAnimation 渐变尺寸伸缩动画效果 *@TranslateAnimation 画面转换位置移动动画效果 *@RotateAnimation 画面转移旋转动画效果 */ public class MyViewAnimation extends View { private Paint paint; private Bitmap bmp; private int x = 50; private Animation mAlphaAnimation; private Animation mScaleAnimation; private Animation mTranslateAnimation; private Animation mRotateAnimation; public MyViewAnimation(Context context) { super(context); paint = new Paint(); paint.setAntiAlias(true); bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon); this.setFocusable(true);//只有当该View获得焦点时才会调用onKeyDown方法 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(Color.BLACK); paint.setColor(Color.WHITE); canvas.drawText("Himi", x, 50, paint);//备注1 canvas.drawText("方向键↑ 渐变透明度动画效果", 80, this.getHeight() - 80, paint); canvas.drawText("方向键↓ 渐变尺寸伸缩动画效果", 80, this.getHeight() - 60, paint); canvas.drawText("方向键← 画面转换位置移动动画效果", 80, this.getHeight() - 40, paint); canvas.drawText("方向键→ 画面转移旋转动画效果", 80, this.getHeight() - 20, paint); canvas.drawBitmap(bmp, this.getWidth() / 2 - bmp.getWidth() / 2, this.getHeight() / 2 - bmp.getHeight() / 2, paint); x += 1; } public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {//渐变透明度动画效果 mAlphaAnimation = new AlphaAnimation(0.1f, 1.0f); //第一个参数fromAlpha 为动画开始时候透明度 //第二个参数toAlpha 为动画结束时候透明度 //注意:取值范围[0-1];[完全透明-完全不透明] mAlphaAnimation.setDuration(3000); ////设置时间持续时间为3000 毫秒=3秒 this.startAnimation(mAlphaAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {//渐变尺寸伸缩动画效果 mScaleAnimation = new ScaleAnimation(0.0f, 1.5f, 0.0f, 1.5f, Animation .RELATIVE_TO_PARENT, 0.5f, Animation.RELATIVE_TO_PARENT, 0.0f); //第一个参数fromX为动画起始时X坐标上的伸缩尺寸 //第二个参数toX为动画结束时X坐标上的伸缩尺寸 //第三个参数fromY为动画起始时Y坐标上的伸缩尺寸 //第四个参数toY 为动画结束时Y 坐标上的伸缩尺寸 //注意: //0.0表示收缩到没有 //1.0表示正常无伸缩 //值小于1.0表示收缩 //值大于1.0表示放大 //-----我这里1-4参数表明是起始图像大小不变,动画终止的时候图像被放大1.5倍 //第五个参数pivotXType 为动画在X 轴相对于物件位置类型 //第六个参数pivotXValue 为动画相对于物件的X 坐标的开始位置 //第七个参数pivotXType 为动画在Y 轴相对于物件位置类型 //第八个参数pivotYValue 为动画相对于物件的Y 坐标的开始位置 //提示:位置类型有三种,每种效果大家自己尝试哈~这里偷下懒~ //毕竟亲眼看到效果的区别才记忆深刻~ //Animation.ABSOLUTE 、Animation.RELATIVE_TO_SELF、Animation.RELATIVE_TO_PARENT mScaleAnimation.setDuration(2000); this.startAnimation(mScaleAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {//画面转换位置移动动画效果 mTranslateAnimation = new TranslateAnimation(0, 100, 0, 100); //第一个参数fromXDelta为动画起始时X坐标上的移动位置 //第二个参数toXDelta为动画结束时X坐标上的移动位置 //第三个参数fromYDelta为动画起始时Y坐标上的移动位置 //第四个参数toYDelta 为动画结束时Y 坐标上的移动位置 mTranslateAnimation.setDuration(2000); this.startAnimation(mTranslateAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {//画面转移旋转动画效果 mRotateAnimation = new RotateAnimation(0.0f, 360.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); //第一个参数fromDegrees为动画起始时的旋转角度 //第二个参数toDegrees 为动画旋转到的角度 //第三个参数pivotXType 为动画在X 轴相对于物件位置类型 //第四个参数pivotXValue 为动画相对于物件的X 坐标的开始位置 //第五个参数pivotXType 为动画在Y 轴相对于物件位置类型 //第六个参数pivotYValue 为动画相对于物件的Y 坐标的开始位置 mRotateAnimation.setDuration(3000); this.startAnimation(mRotateAnimation); } return super.onKeyDown(keyCode, event); } }
补充:有童鞋说对三种相对位置不太理解,那么我简单说补充下:
//Animation.ABSOLUTE 相对位置是屏幕左上角,绝对位置! //Animation.RELATIVE_TO_SELF 相对位置是自身View;取值为0,是自身左上角,取值为1是自身的右下角; //Animation.RELATIVE_TO_PARENT 相对父类View的位置
当设定了位置类型之后,会让你传入X或者Y的值,这里的X,Y可以理解成为一个点坐标!比如是旋转动画,那么这个(X,Y)就是旋转中心点!
OK,对于Tween Animation下的每种动画效果的实例化的每个参数都解释的很详细了!其实动画的实现不光用代码可以实现,在xml中注册实现也是可以的,这里就不多写了,大家可以自己去尝试写一下,那么在view中我们播放一种特效动画,只要实例化其对象,然后设置下参数,然后startAnimation()就好了,步骤很简单,只是每个动画实例化的参数确有着千变万化的改法,这些我也没法子一一来给大家演示,大家可以自己改改参数看看实际的效果!当然对于每种动画我们不光有设置播放的时候,还有一些属性和方法可以调用,比如Animation.restart()重放动画,getTransformation()此方法返回假,说明动画完成等等很多属性,请各位童鞋自定实验 o(∩_∩)o 哈哈~
顺便先解释下MyViewAnimation .java 类中onDraw()方法里的(备注1)!其实这里我是想跟大家说明下Android Animation实现机制
【启动任意一种动画效果之前 和 之后 的对比图】
很明显、"Himi"字样在动画开始前和开始后出现了移动,而且在MyViewAnimation.java中我没有使用Runnable接口,也没有调用刷新的函数,那么我来给各位童鞋解释下原因:
动画的每种变换其实内部都是一次矩阵运算。在Android 中,Canvas 类中包含当前矩阵,当调用 Canvas.drawBitmap (bmp, x, y, Paint) 绘制时,android 会先把 bmp 做一次矩阵运算,然后将运算的结果显示在 Canvas 上,然后不断修改 Canvas 的矩阵并刷新屏幕,View 里的对象就会不停的做图形变换,动画就形成了。
还有一点提醒大家:动画的播放是对整个游戏画布进行的操作,这一点要知道哟~
那么下面就要给大家介绍如何在我们的SurfaceView中运用Tween Animation!
MySurfaceViewAnimation.java
package com.himi.frameAnimation; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.SurfaceHolder.Callback; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.RotateAnimation; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; /** *@author Himi */ public class MySurfaceViewAnimation extends SurfaceView implements Callback, Runnable { private Thread th = new Thread(this); private SurfaceHolder sfh; private Canvas canvas; private Paint paint; private Bitmap bmp; /// private Animation mAlphaAnimation; private Animation mScaleAnimation; private Animation mTranslateAnimation; private Animation mRotateAnimation; public MySurfaceViewAnimation(Context context) { super(context); Log.v("Himi", "MySurfaceView"); this.setKeepScreenOn(true); bmp = BitmapFactory.decodeResource(getResources(), R.drawable.icon); sfh = this.getHolder(); sfh.addCallback(this); paint = new Paint(); paint.setAntiAlias(true); setFocusable(true); setFocusableInTouchMode(true); // this.setBackgroundResource(R.drawable.icon);//备注2 } public void surfaceCreated(SurfaceHolder holder) { Log.v("Himi", "surfaceCreated"); th.start(); } public void draw() { try { canvas = sfh.lockCanvas(); if (canvas != null) { canvas.drawColor(Color.BLACK); paint.setColor(Color.WHITE); canvas.drawText("方向键↑ 渐变透明度动画效果", 80, this.getHeight() - 80, paint); canvas.drawText("方向键↓ 渐变尺寸伸缩动画效果", 80, this.getHeight() - 60, paint); canvas.drawText("方向键← 画面转换位置移动动画效果", 80, this.getHeight() - 40, paint); canvas.drawText("方向键→ 画面转移旋转动画效果", 80, this.getHeight() - 20, paint); canvas.drawBitmap(bmp, this.getWidth() / 2 - bmp.getWidth() / 2, this.getHeight() / 2 - bmp.getHeight() / 2, paint); } } catch (Exception e) { Log.v("Himi", "draw is Error!"); } finally { sfh.unlockCanvasAndPost(canvas); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {//渐变透明度动画效果 mAlphaAnimation = new AlphaAnimation(0.1f, 1.0f); mAlphaAnimation.setDuration(3000); this.startAnimation(mAlphaAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {//渐变尺寸伸缩动画效果 mScaleAnimation = new ScaleAnimation(0.0f, 2.0f, 1.5f, 1.5f, Animation.RELATIVE_TO_PARENT, 0.5f, Animation.RELATIVE_TO_PARENT, 0.0f); mScaleAnimation.setDuration(2000); this.startAnimation(mScaleAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {//画面转换位置移动动画效果 mTranslateAnimation = new TranslateAnimation(0, 100, 0, 100); mTranslateAnimation.setDuration(2000); this.startAnimation(mTranslateAnimation); } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {//画面转移旋转动画效果 mRotateAnimation = new RotateAnimation(0.0f, 360.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateAnimation.setDuration(3000); this.startAnimation(mRotateAnimation); } return super.onKeyDown(keyCode, event); } public void run() { // TODO Auto-generated method stub while (true) { draw(); try { Thread.sleep(100); } catch (Exception ex) { } } } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.v("Himi", "surfaceChanged"); } public void surfaceDestroyed(SurfaceHolder holder) { Log.v("Himi", "surfaceDestroyed"); } }
动画代码实现跟View中的做法一样,运行模拟器发现按键没效果,不是按键没触发是本来就存在问题, - -。但是!大家可以把此类里有一行,也就是(备注2)的注释打开,我们给设置背景图,然后在模拟器上的运行效果如下图:
很明显的看到,我们的动画正常运行了,虽然效果并不是我们想到的!但是这里可以说明一点问题:
SurfaceView 本身具备双缓冲机制!!!!!
有些文章里说“给SurfaceView添加双缓冲”,其实是在画蛇添足 - -,而且介绍的时候拿着单线程与双线程例子来解释双缓冲更高效的实现方法;我想弱弱的问什么是双缓冲??? 如果SurfaceView不具备双缓冲,那敢问上面这张截图如何解释????
其实要实现双缓冲,只需要是新建一个Bitmap和Canvas,用这个新建的Canvas把正弦波画到新建的Bitmap,画完再通过sfh.lockCanvas获取SurfaceView对应的Canvas,用这个Canvas把新建的Bitmap画到SurfaceView上去,这才叫双缓冲; 还有双缓存和多线程没关系!
那么View中动画的实现机制是在不断的刷屏不断的重复调用重写的onDraw()方法、而在Surfaceview的那张截图确实也正常的动画操作了,原因又何在?而且我们设置的背景图覆盖我们draw出来的字体!!效果很不理想;那么经过考虑我决定利用布局把View和SurfaceView都一并显示,用View主要去完成动画部分,(那么关于如何一并显示,或者说同时在SurfaceView中添加组件,在之前的【Android 2D开发之六】 和 【Android 2D开发之七】都有了详细讲解,那么在这里),当然一并显示也会有问题,比如我们存在了view和Surfaceiew,那么按键的时候触发的哪个?或者说如何去控制这两个View?放心,我下面就跟大家一一来讲解!
下面先让我们把我们的view 和 Surfaceview 先同时显示出来:【黑色的是MyView (View),白色是MySurfaceView(SurfaceView)】
先上张运行截图: (图4)
main.xml中的代码
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" > <com.himi.MySurfaceView android:id="@+id/view3d" android:layout_width="fill_parent" android:layout_height="fill_parent"/> <com.himi.MyView android:id="@+id/myview" android:layout_width="fill_parent" android:layout_height="fill_parent"/> </RelativeLayout> </LinearLayout>
xml中我们注册了我们自定义的view-MyView 和 SurfaceView-MySurfaceView;
需要强调的有两点:
1 : 当我们xml中注册我们的View时,我们View类中的构造函数必须要用
public MyView(Context context, AttributeSet attrs) {} 两个参数的形式,以前的文章有讲解。
2 : 当我们在Xml中注册两个View的时候,它们显示的次序就是根据xml注册的顺序来显示,比如上面我们先注册了MySurfaceView,然后注册的MyView ,那么显示的时候会把后添加进去的MyView显示在最上层!
下面我们来看MySurfaceView.java中的代码:
package com.himi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.SurfaceHolder.Callback; /** * * @author Himi * */ public class MySurfaceView extends SurfaceView implements Callback, Runnable { public static MySurfaceView msrv ;//----备注1 private int move_x = 2, x = 20; private Thread th; private SurfaceHolder sfh; private Canvas canvas; private Paint p; public MySurfaceView(Context context, AttributeSet attrs) { super(context, attrs); msrv=this; p = new Paint(); p.setAntiAlias(true); sfh = this.getHolder(); sfh.addCallback(this); th = new Thread(this); this.setKeepScreenOn(true); this.setFocusable(true);// ----备注2 } public void surfaceCreated(SurfaceHolder holder) { th.start(); } public void draw() { canvas = sfh.lockCanvas(); if(canvas!=null){ canvas.drawColor(Color.WHITE); canvas.drawText("我是 - Surfaceview", x + move_x, 280, p); sfh.unlockCanvasAndPost(canvas); } } private void logic() { x += move_x; if (x > 200 || x < 80) { move_x = -move_x; } } @Override public boolean onKeyDown(int key, KeyEvent event) { //备注2 return super.onKeyDown(key, event); } public void run() { // TODO Auto-generated method stub while (true) { draw(); logic(); try { Thread.sleep(100); } catch (Exception ex) { } } } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } public void surfaceDestroyed(SurfaceHolder holder) { } }
代码都很熟悉了, 主要我们来给大家解释下备注1,备注2:
备注1:
我在两个MyView 和 MySurfaceView中都定义了本类一个静态对象,然后在初始化的时候都利用=this的形式进行了实例化;
注意:=this; 的这种实例形式要注意!只能在当前程序中仅存在一个本类对象才可使用!
为什么要实例两个View的实例而且定义成静态,这样做主要为了类之间方便调用和操作!比如在我们这个项目中,我这样做是为了在MainActivity中去管理两个View按键焦点!下面我会给出MainActivity的代码,大家一看便知;
备注2:
我在两个MyView 和 MySurfaceView中都对获取按键焦点注释掉了,而是在别的类中的调用其View的静态实例对象就可以任意类中对其设置!这样就可以很容易去控制到底谁来响应按键了。
这里还要强调一下:当xml中注册多个 View的时候,当我们点击按键之后,Android会先判定哪个View setFocusable(true)设置焦点了,如果都设置了,那么Android 会默认响应在xml中第一个注册的view ,而不是两个都会响应。那么为什么不同时响应呢?我解释下:
上面这截图是Android SDK Api的树状图,很明显SurfaceView继承了View,它俩是基继承关系,那么不管是子类还是基类一旦响应了按键,其基类或者父类就不会再去响应;
下面我们来看MainActivity.java:
package com.himi; import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import android.view.Window; import android.view.WindowManager; /** * * @author Himi * */ public class MainActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.requestWindowFeature(Window.FEATURE_NO_TITLE); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); setContentView(R.layout.main); MySurfaceView.msrv.setFocusable(false);//备注1 MyView.mv.setFocusable(true);//备注1 } @Override public boolean onKeyDown(int keyCode, KeyEvent event) {//备注2 return super.onKeyDown(keyCode, event); } }
备注1:
这里是当程序运行的时候我们默认让我们的MyView(View)来响应按键。通过类名调用对应的View实例,然后设置获取焦点的函数;
备注2:
这里要注意:不管你在xml中注册了多少个View ,也不管View是否都设置了获取焦点,只要你在 MainActivity 中重写onKeyDown()函数,Android 就会调用此函数。
那么直接在SurfaceView中进行实现动画的想法这里没有得到很好的解决,而是我利用布局的方式来一同显示的方式,希望各位童鞋如果有好的方法,在SurfaceView中直接能使用动画的建议和想法,希望留言给我,大家一起学习 讨论,谢谢 下面给出项目源码:
源码下载地址: http://www.himigame.com/android-game/331.html
(欢迎各位童鞋订阅本博客,因为咱的更新速度可是很快的~娃哈哈)