欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android App中使用SurfaceView制作多线程动画的实例讲解

程序员文章站 2024-02-29 14:46:34
1. surfaceview的定义 通常情况程序的view和用户响应都是在同一个线程中处理的,这也是为什么处理长时间事件(例如访问网络)需要放到另外的线程中去(防止阻塞当...

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在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:

Android App中使用SurfaceView制作多线程动画的实例讲解

Android App中使用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工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。