热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

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

这篇文章主要介绍了AndroidApp中使用SurfaceView制作多线程动画的实例讲解,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在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:

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 

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
        && (this.x + this.width)  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:显示的图片
* @param duration:图片显示的时间
* @param repeat:是否重复动画过程
*/ 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工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。


推荐阅读
  • 本文探讨了如何通过最小生成树(MST)来计算严格次小生成树。在处理过程中,需特别注意所有边权重相等的情况,以避免错误。我们首先构建最小生成树,然后枚举每条非树边,检查其是否能形成更优的次小生成树。 ... [详细]
  • QUIC协议:快速UDP互联网连接
    QUIC(Quick UDP Internet Connections)是谷歌开发的一种旨在提高网络性能和安全性的传输层协议。它基于UDP,并结合了TLS级别的安全性,提供了更高效、更可靠的互联网通信方式。 ... [详细]
  • 本文详细介绍如何使用arm-eabi-gdb调试Android平台上的C/C++程序。通过具体步骤和实用技巧,帮助开发者更高效地进行调试工作。 ... [详细]
  • 深入理解 Oracle 存储函数:计算员工年收入
    本文介绍如何使用 Oracle 存储函数查询特定员工的年收入。我们将详细解释存储函数的创建过程,并提供完整的代码示例。 ... [详细]
  • 本文总结了2018年的关键成就,包括职业变动、购车、考取驾照等重要事件,并分享了读书、工作、家庭和朋友方面的感悟。同时,展望2019年,制定了健康、软实力提升和技术学习的具体目标。 ... [详细]
  • 在计算机技术的学习道路上,51CTO学院以其专业性和专注度给我留下了深刻印象。从2012年接触计算机到2014年开始系统学习网络技术和安全领域,51CTO学院始终是我信赖的学习平台。 ... [详细]
  • CSS 布局:液态三栏混合宽度布局
    本文介绍了如何使用 CSS 实现液态的三栏布局,其中各栏具有不同的宽度设置。通过调整容器和内容区域的属性,可以实现灵活且响应式的网页设计。 ... [详细]
  • Linux 系统启动故障排除指南:MBR 和 GRUB 问题
    本文详细介绍了 Linux 系统启动过程中常见的 MBR 扇区和 GRUB 引导程序故障及其解决方案,涵盖从备份、模拟故障到恢复的具体步骤。 ... [详细]
  • 本文介绍了如何使用jQuery根据元素的类型(如复选框)和标签名(如段落)来获取DOM对象。这有助于更高效地操作网页中的特定元素。 ... [详细]
  • 本文将详细介绍如何使用剪映应用中的镜像功能,帮助用户轻松实现视频的镜像效果。通过简单的步骤,您可以快速掌握这一实用技巧。 ... [详细]
  • 深入理解Cookie与Session会话管理
    本文详细介绍了如何通过HTTP响应和请求处理浏览器的Cookie信息,以及如何创建、设置和管理Cookie。同时探讨了会话跟踪技术中的Session机制,解释其原理及应用场景。 ... [详细]
  • 本文介绍如何在 Xcode 中使用快捷键和菜单命令对多行代码进行缩进,包括右缩进和左缩进的具体操作方法。 ... [详细]
  • 本文介绍了一款用于自动化部署 Linux 服务的 Bash 脚本。该脚本不仅涵盖了基本的文件复制和目录创建,还处理了系统服务的配置和启动,确保在多种 Linux 发行版上都能顺利运行。 ... [详细]
  • 在Linux系统中配置并启动ActiveMQ
    本文详细介绍了如何在Linux环境中安装和配置ActiveMQ,包括端口开放及防火墙设置。通过本文,您可以掌握完整的ActiveMQ部署流程,确保其在网络环境中正常运行。 ... [详细]
  • Android 渐变圆环加载控件实现
    本文介绍了如何在 Android 中创建一个自定义的渐变圆环加载控件,该控件已在多个知名应用中使用。我们将详细探讨其工作原理和实现方法。 ... [详细]
author-avatar
mobiledu2502922507
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有