Android App中使用SurfaceView制作多线程动画的实例讲解
1. surfaceview的定义
通常情况程序的view和用户响应都是在同一个线程中处理的,这也是为什么处理长时间事件(例如访问网络)需要放到另外的线程中去(防止阻塞当前ui线程的操作和绘制)。但是在其他线程中却不能修改ui元素,例如用后台线程更新自定义view(调用view的在自定义view中的ondraw函数)是不允许的。
如果需要在另外的线程绘制界面、需要迅速的更新界面或则渲染ui界面需要较长的时间,这种情况就要使用surfaceview了。surfaceview中包含一个surface对象,而surface是可以在后台线程中绘制的。surfaceview的性质决定了其比较适合一些场景:需要界面迅速更新、对帧率要求较高的情况。使用surfaceview需要注意以下几点情况:
surfaceview和surfaceholder.callback函数都从当前surfaceview窗口线程中调用(一般而言就是程序的主线程)。有关资源状态要注意和绘制线程之间的同步。
在绘制线程中必须先合法的获取surface才能开始绘制内容,在surfaceholder.callback.surfacecreated() 和surfaceholder.callback.surfacedestroyed()之间的状态为合法的,另外在surface类型为surface_type_push_buffers时候是不合法的。
额外的绘制线程会消耗系统的资源,在使用surfaceview的时候要注意这点。
2. surfaceview的使用
首先继承surfaceview,并实现surfaceholder.callback接口,实现它的三个方法:surfacecreated,surfacechanged,surfacedestroyed。
(1)surfacecreated(surfaceholder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。
(2)surfacechanged(surfaceholder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。
(3)surfacedestroyed(surfaceholder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
还需要获得surfaceholder,并添加回调函数,这样这三个方法才会执行。
只要继承surfaceview类并实现surfaceholder.callback接口就可以实现一个自定义的surfaceview了,surfaceholder.callback在底层的surface状态发生变化的时候通知view,surfaceholder.callback具有如下的接口:
(1)surfacecreated(surfaceholder holder):当surface第一次创建后会立即调用该函数。程序可以在该函数中做些和绘制界面相关的初始化工作,一般情况下都是在另外的线程来绘制界面,所以不要在这个函数中绘制surface。
(2)surfacechanged(surfaceholder holder, int format, int width,int height):当surface的状态(大小和格式)发生变化的时候会调用该函数,在surfacecreated调用后该函数至少会被调用一次。
(3)surfacedestroyed(surfaceholder holder):当surface被摧毁前会调用该函数,该函数被调用后就不能继续使用surface了,一般在该函数中来清理使用的资源。
通过surfaceview的getholder()函数可以获取surfaceholder对象,surface 就在surfaceholder对象内。虽然surface保存了当前窗口的像素数据,但是在使用过程中是不直接和surface打交道的,由surfaceholder的canvas lockcanvas()或则canvas lockcanvas(rect dirty)函数来获取canvas对象,通过在canvas上绘制内容来修改surface中的数据。如果surface不可编辑或则尚未创建调用该函数会返回null,在 unlockcanvas() 和 lockcanvas()中surface的内容是不缓存的,所以需要完全重绘surface的内容,为了提高效率只重绘变化的部分则可以调用lockcanvas(rect dirty)函数来指定一个dirty区域,这样该区域外的内容会缓存起来。在调用lockcanvas函数获取canvas后,surfaceview会获取surface的一个同步锁直到调用unlockcanvasandpost(canvas canvas)函数才释放该锁,这里的同步机制保证在surface绘制过程中不会被改变(被摧毁、修改)。
当在canvas中绘制完成后,调用函数unlockcanvasandpost(canvas canvas)来通知系统surface已经绘制完成,这样系统会把绘制完的内容显示出来。为了充分利用不同平台的资源,发挥平台的最优效果可以通过surfaceholder的settype函数来设置绘制的类型,目前接收如下的参数:
(1)surface_type_normal:用ram缓存原生数据的普通surface
(2)surface_type_hardware:适用于dma(direct memory access )引擎和硬件加速的surface
(3)surface_type_gpu:适用于gpu加速的surface
(4)surface_type_push_buffers:表明该surface不包含原生数据,surface用到的数据由其他对象提供,在camera图像预览中就使用该类型的surface,有camera负责提供给预览surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockcanvas来获取canvas对象了。
访问surfaceview的底层图形是通过surfaceholder接口来实现的,通过getholder()方法可以得到这个surfaceholder对象。你应该实现surfacecreated(surfaceholder)和surfacedestroyed(surfaceholder)方法来知道在这个surface在窗口的显示和隐藏过程中是什么时候创建和销毁的。
注意:一个surfaceview只在surfaceholder.callback.surfacecreated() 和 surfaceholder.callback.surfacedestroyed()调用之间是可用的,其他时间是得不到它的canvas对象的(null)。
3. surfaceview实战
下面通过一个小demo来学习surfaceview在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:
首先创建核心类gameview.java,源码如下:
public class gameview extends surfaceview implements surfaceholder.callback { //屏幕宽高 public static int screen_width; public static int screen_height; private context mcontext; private surfaceholder mholder; //最大帧数 (1000 / 30) private static final int draw_interval = 30; private drawthread mdrawthread; private frameanimation []spriteanimations; private sprite msprite; private int spritewidth = 0; private int spriteheight = 0; private float spritespeed = (float)((500 * screen_width / 480) * 0.001); private int row = 4; private int col = 4; public gamesurfaceview(context context) { super(context); this.mcontext = context; mholder = this.getholder(); mholder.addcallback(this); initresources(); msprite = new sprite(spriteanimations,0,0,spritewidth,spriteheight,spritespeed); } private void initresources() { bitmap[][] spriteimgs = generatebitmaparray(mcontext, r.drawable.sprite, row, col); spriteanimations = new frameanimation[row]; for(int i = 0; i < row; i ++) { bitmap []spriteimg = spriteimgs[i]; frameanimation spriteanimation = new frameanimation(spriteimg,new int[]{150,150,150,150},true); spriteanimations[i] = spriteanimation; } } public bitmap decodebitmapfromres(context context, int resourseid) { bitmapfactory.options opt = new bitmapfactory.options(); opt.inpreferredconfig = bitmap.config.rgb_565; opt.inpurgeable = true; opt.ininputshareable = true; inputstream is = context.getresources().openrawresource(resourseid); return bitmapfactory.decodestream(is, null, opt); } public bitmap createbitmap(context context, bitmap source, int row, int col, int rowtotal, int coltotal) { bitmap bitmap = bitmap.createbitmap(source, (col - 1) * source.getwidth() / coltotal, (row - 1) * source.getheight() / rowtotal, source.getwidth() / coltotal, source.getheight() / rowtotal); return bitmap; } public bitmap[][] generatebitmaparray(context context, int resourseid, int row, int col) { bitmap bitmaps[][] = new bitmap[row][col]; bitmap source = decodebitmapfromres(context, resourseid); this.spritewidth = source.getwidth() / col; this.spriteheight = source.getheight() / row; for (int i = 1; i <= row; i++) { for (int j = 1; j <= col; j++) { bitmaps[i - 1][j - 1] = createbitmap(context, source, i, j, row, col); } } if (source != null && !source.isrecycled()) { source.recycle(); source = null; } return bitmaps; } public void surfacechanged(surfaceholder holder, int format, int width, int height) { } public void surfacecreated(surfaceholder holder) { if(null == mdrawthread) { mdrawthread = new drawthread(); mdrawthread.start(); } } public void surfacedestroyed(surfaceholder holder) { if(null != mdrawthread) { mdrawthread.stopthread(); } } private class drawthread extends thread { public boolean isrunning = false; public drawthread() { isrunning = true; } public void stopthread() { isrunning = false; boolean workisnotfinish = true; while (workisnotfinish) { try { this.join();// 保证run方法执行完毕 } catch (interruptedexception e) { // todo auto-generated catch block e.printstacktrace(); } workisnotfinish = false; } } public void run() { long deltatime = 0; long ticktime = 0; ticktime = system.currenttimemillis(); while (isrunning) { canvas canvas = null; try { synchronized (mholder) { canvas = mholder.lockcanvas(); //设置方向 msprite.setdirection(); //更新精灵位置 msprite.updateposition(deltatime); drawsprite(canvas); } } catch (exception e) { e.printstacktrace(); } finally { if (null != mholder) { mholder.unlockcanvasandpost(canvas); } } deltatime = system.currenttimemillis() - ticktime; if(deltatime < draw_interval) { try { thread.sleep(draw_interval - deltatime); } catch (interruptedexception e) { e.printstacktrace(); } } ticktime = system.currenttimemillis(); } } } private void drawsprite(canvas canvas) { //清屏操作 canvas.drawcolor(color.black); msprite.draw(canvas); } }
gameview.java中包含了一个绘图线程drawthread,在线程的run方法中锁定canvas、绘制精灵、更新精灵位置、释放canvas等操作。因为精灵素材是一张大图,所以这里进行了裁剪生成一个二维数组。使用这个二维数组初始化了精灵四个方向的动画,下面看sprite.java的源码。
public class sprite { public static final int down = 0; public static final int left = 1; public static final int right = 2; public static final int up = 3; public float x; public float y; public int width; public int height; //精灵行走速度 public double speed; //精灵当前行走方向 public int direction; //精灵四个方向的动画 public frameanimation[] frameanimations; public sprite(frameanimation[] frameanimations, int positionx, int positiony, int width, int height, float speed) { this.frameanimations = frameanimations; this.x = positionx; this.y = positiony; this.width = width; this.height = height; this.speed = speed; } public void updateposition(long deltatime) { switch (direction) { case left: //让物体的移动速度不受机器性能的影响,每帧精灵需要移动的距离为:移动速度*时间间隔 this.x = this.x - (float) (this.speed * deltatime); break; case down: this.y = this.y + (float) (this.speed * deltatime); break; case right: this.x = this.x + (float) (this.speed * deltatime); break; case up: this.y = this.y - (float) (this.speed * deltatime); break; } } /** * 根据精灵的当前位置判断是否改变行走方向 */ public void setdirection() { if (this.x <= 0 && (this.y + this.height) < gamesurfaceview.screen_height) { if (this.x < 0) this.x = 0; this.direction = sprite.down; } else if ((this.y + this.height) >= gamesurfaceview.screen_height && (this.x + this.width) < gamesurfaceview.screen_width) { if ((this.y + this.height) > gamesurfaceview.screen_height) this.y = gamesurfaceview.screen_height - this.height; this.direction = sprite.right; } else if ((this.x + this.width) >= gamesurfaceview.screen_width && this.y > 0) { if ((this.x + this.width) > gamesurfaceview.screen_width) this.x = gamesurfaceview.screen_width - this.width; this.direction = sprite.up; } else { if (this.y < 0) this.y = 0; this.direction = sprite.left; } } public void draw(canvas canvas) { frameanimation frameanimation = frameanimations[this.direction]; bitmap bitmap = frameanimation.nextframe(); if (null != bitmap) { canvas.drawbitmap(bitmap, x, y, null); } } }
精灵类主要是根据当前位置判断行走的方向,然后根据行走的方向更新精灵的位置,再绘制自身的动画。由于精灵的动画是一帧一帧的播放图片,所以这里封装了frameanimation.java,源码如下:
public class frameanimation{ /**动画显示的需要的资源 */ private bitmap[] bitmaps; /**动画每帧显示的时间 */ private int[] duration; /**动画上一帧显示的时间 */ protected long lastbitmaptime; /**动画显示的索引值,防止数组越界 */ protected int step; /**动画是否重复播放 */ protected boolean repeat; /**动画重复播放的次数*/ protected int repeatcount; /** * @param bitmap:显示的图片<br/> * @param duration:图片显示的时间<br/> * @param repeat:是否重复动画过程<br/> */ public frameanimation(bitmap[] bitmaps, int duration[], boolean repeat) { this.bitmaps = bitmaps; this.duration = duration; this.repeat = repeat; lastbitmaptime = null; step = 0; } public bitmap nextframe() { // 判断step是否越界 if (step >= bitmaps.length) { //如果不无限循环 if( !repeat ) { return null; } else { lastbitmaptime = null; } } if (null == lastbitmaptime) { // 第一次执行 lastbitmaptime = system.currenttimemillis(); return bitmaps[step = 0]; } // 第x次执行 long nowtime = system.currenttimemillis(); if (nowtime - lastbitmaptime <= duration[step]) { // 如果还在duration的时间段内,则继续返回当前bitmap // 如果duration的值小于0,则表明永远不失效,一般用于背景 return bitmaps[step]; } lastbitmaptime = nowtime; return bitmaps[step++];// 返回下一bitmap } }
frameanimation根据每一帧的显示时间返回当前的图片帧,若没有超过指定的时间则继续返回当前帧,否则返回下一帧。
接下来需要做的是让activty显示的view为我们之前创建的gameview,然后设置全屏显示。
public void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); getwindow().setflags(windowmanager.layoutparams.flag_fullscreen, windowmanager.layoutparams.flag_fullscreen); requestwindowfeature(window.feature_no_title); getwindow().setflags(windowmanager.layoutparams.flag_keep_screen_on, windowmanager.layoutparams.flag_keep_screen_on); displaymetrics outmetrics = new displaymetrics(); this.getwindowmanager().getdefaultdisplay().getmetrics(outmetrics); gamesurfaceview.screen_width = outmetrics.widthpixels; gamesurfaceview.screen_height = outmetrics.heightpixels; gamesurfaceview gameview = new gamesurfaceview(this); setcontentview(gameview); }
现在运行android工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。
下一篇: Java成员变量的隐藏(实例讲解)