当前位置: 移动技术网 > IT编程>移动开发>Android > Android App中使用SurfaceView制作多线程动画的实例讲解

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

2019年07月24日  | 移动技术网IT编程  | 我要评论

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

2016428160517972.png (330×250)

2016428160544089.png (338×177)

首先创建核心类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工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网