现在的位置: 首页 > 综合 > 正文

View和SurfaceView的区别

2017年10月19日 ⁄ 综合 ⁄ 共 6575字 ⁄ 字号 评论关闭

一、View简介

View一般在onDraw方法里面绘图,onDraw在UI主线程执行。onDraw默认只在View初始化的时候调用一遍,所以View不会自动刷新画面,一般要调用invalidate或者postInvalidate来重新执行onDraw里面的代码进行刷新画面。UI主线程一般用来渲染组件、处理组件与用户之间的交互事件,比如说按钮的点击事件、文本框的输入事件。如果你的画图任务相当繁重,那么onDraw方法里面的代码要执行好长一段时间,就可能会造成UI主线程阻塞。比如,如果在绘图的同时,用户刚好点击了某个按钮,那UI主线程会怎么处理点击事件呢?肯定是在绘图完毕后再处理点击事件,因为onDraw方法跟按钮点击事件处理都是在UI主线程内操作的。这样就给用户造成按钮点击延时处理的坏体验。

绘图原理:

主线程绘图代码和效果:

protected
void
onDraw(Canvas canvas) {

    // 初始化画笔

    Paint paint = new Paint();

    // 设置抗锯齿,
文字清晰点,
不过相对比较耗性能

    paint.setAntiAlias(true);

    // 设置画笔颜色为红色

    paint.setColor(Color.RED);

    // 设置文字大小

    paint.setTextSize(50);

    // 画文字,
所画的内容都会缓存到Surface

    canvas.drawText("ITCAST", 100, 100, paint);

}

绘图效果:

既然在主线程内绘图可能会造成线程阻塞,那我们可不可以在onDraw里面开启一个新线程进行绘图呢?

在后台线程绘图代码和效果:

protected
void
onDraw(final
Canvas canvas) {

    new Thread(){

       public
void
run() {

           // 初始化画笔

           Paint paint = new Paint();

           // 设置抗锯齿,
文字清晰点,
不过相对比较耗性能

           paint.setAntiAlias(true);

           // 设置画笔颜色为红色

           paint.setColor(Color.RED);

           // 设置文字大小

           paint.setTextSize(50);

           // 画文字,
所画的内容都会缓存到Surface

           canvas.drawText("ITCAST", 100, 100, paint);

       }

    }.start();

}

可以发现屏幕上并没有显示任何文字,说明绘图不成功,因为android只允许在UI主线程内改变UI界面,绘图完毕后渲染到屏幕上的实质就是改变UI界面。

 

二、SurfaceView简介

SurfaceView也可以在onDraw方法里面绘图,即直接在UI主线程绘图并渲染,因为SurfaceView是View的子类。上面已经介绍,绘图完毕后只能在UI主线程内渲染到屏幕上。为了不阻塞主线程,我们可以考虑采取这样的方案:在后台线程执行繁重的绘图任务,把所绘制的东西缓存起来;绘图完毕后,再回到UI主线程,一次性把所绘制的东西渲染到屏幕上。(本质就是后台线程绘图,UI主线程渲染)

只使用View的onDraw方法是无法实现这种方案的,而SurfaceView可以实现这种方案。

先看看SurfaceView的工作原理图:

首先,可以确定的2点:1.使用Canvas对象进行绘图;2.一定是在主线内渲染屏幕。由图可以看出,Canvas在后台线程绘制的东西先缓存到Surface中,然后让Surface回到UI主线程渲染到屏幕上。因此,要有Surface对象存在,才能够在屏幕上显示东西。顺着下面的问题来了解SurfaceView的整个工作原理。

1)Surface是什么?

Surface是SurfaceView里面的一个成员变量,它的创建和销毁过程都是自动的。Surface里面有个Canvas成员变量,将来我们就是要得到这个Canvas对象进行绘图。不过只能通过SurfaceHolder对象来控制和访问Surface。Surface的主要作用是缓存Canvas绘制的东西,并渲染到屏幕上。

2)SurfaceHolder对象怎么获取?

使用SurfaceView的getHolder()

3)在什么时候开启后台绘图线程?

最好在Surface被创建的时候,开启绘图线程。因为没有Surface,绘制再多的东西也无法渲染到屏幕上。

4)在什么时候销毁后台绘图线程?

最好在Surface被销毁的时候,销毁绘图线程。因为没有Surface,绘制再多的东西也无法渲染到屏幕上。

5)既然要在创建Surface时开启绘图线程,在销毁Surface时销毁绘图线程。那我们怎么知道Surface在什么时候创建,什么时候销毁呢?

那就需要监听Surface的生命周期,使用SurfaceHolder对象的addCallback()来添加Surface的生命周期监听器——SurfaceHolder.Callback

Callback里面对应的方法

//Surface的大小发生改变时调用

public void surfaceChanged(SurfaceHolderholder,int format,int width,int height){}

//Surface创建时调用,一般在这里开启绘图线程

public void surfaceCreated(SurfaceHolderholder){}

//Surface销毁时调用,一般在这里销毁绘图线程

public void surfaceDestroyed(SurfaceHolderholder) {}

 

再来看看如果搭建SurfaceView的游戏开发框架

1>    新建一个继承SurfaceView的类——GameView

 

2> 得到SurfaceHolder对象,并监听Surface的生命周期

SurfaceHolder holder;

public GameView(Context context) {

    super(context);

    // 获取SurfaceHolder对象

    holder = getHolder();

    // 监听Surface的生命周期

    holder.addCallback(this);

}

可以看到addCallback传的参数是this,所以GameView必须实现Callback接口

public
class
GameView extends SurfaceViewimplements Callback

实现相应的方法

/**

 * Surface的大小发生改变时调用

 */

public
void
surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {}

/**

 * Surface创建时调用,一般在这里开启绘图线程

 */

public
void
surfaceCreated(SurfaceHolder holder) {}

/**

 * Surface销毁时调用,一般在这里销毁绘图线程

 */

public
void
surfaceDestroyed(SurfaceHolder holder) {}

 

3> 开启绘图线程、销毁绘图线程

先保留一个线程成员变量

/**

* 绘图线程

*/

Thread thread;

/**

 * Surface的大小发生改变时调用

 */

public
void
surfaceChanged(SurfaceHolder holder, int format,
int width,int height) {}

/**

 * Surface创建时调用,一般在这里开启绘图线程

 */

public
void
surfaceCreated(SurfaceHolder holder) {

    // 开启绘图线程

    thread = new Thread(this);

    thread.start();

}

/**

 * Surface销毁时调用,一般在这里销毁绘图线程

 */

public
void
surfaceDestroyed(SurfaceHolder holder) {

    if (thread !=
null &&
thread
.isAlive()) {

       try {

           // 销毁绘图线程

           thread.join();

       } catch(InterruptedException e) {

           e.printStackTrace();

       }

    }

}

Thread里面传的是this,所以GameView必须实现Runnable接口

public
class
GameView extends SurfaceViewimplements Callback, Runnable

实现run方法

/**

 * 这是后台绘图线程,
在这里得到SurfaceCanvas进行绘图

 */

public
void
run() {}

 

4> 在run方法中尝试进行绘图

public
void
run() {

// 通过Holder对象获得SurfaceCanvas成员变量

    Canvas canvas = holder.lockCanvas();

    // 初始化画笔

    Paint paint = new Paint();

    // 设置抗锯齿,
文字清晰点,
不过相对比较耗性能

    paint.setAntiAlias(true);

    // 设置画笔颜色为红色

    paint.setColor(Color.RED);

    // 设置文字大小

    paint.setTextSize(50);

    // 画文字,
所画的内容都会缓存到Surface

    canvas.drawText("ITCAST", 100, 100, paint);

    // 画完之后,
再将缓存的东西渲染到屏幕上

    holder.unlockCanvasAndPost(canvas);

}

效果:

虽然可以画东西了,但是我们都知道,这个run方法只执行一遍,画一次就没了。我们游戏中一般都是不断地刷新屏幕,所以我们应该循环调用那段绘图代码进行刷新屏幕。我们尝试让文字动起来,就改变它的y值吧。

 

5> 动态刷新屏幕

先搞个循环标记

/**

 * 是否正在绘图

 */

boolean
running
;

在开启绘图线程前改变循环标记

/**

 * Surface创建时调用,一般在这里开启绘图线程

 */

public
void
surfaceCreated(SurfaceHolder holder) {

    // 开启绘图线程

    running = true;

    thread = new Thread(this);

    thread.start();

}

/**

 * Surface销毁时调用,一般在这里销毁绘图线程

 */

public
void
surfaceDestroyed(SurfaceHolder holder) {

    // 改变循环标记

    running = false;

    if (thread !=
null &&
thread
.isAlive()) {

       try {

           // 销毁绘图线程

           thread.join();

       } catch(InterruptedException e) {

           e.printStackTrace();

       }

    }

}

接下来循环绘图,让文字从上到下移动

int
y
= 0;

public
void
run() {

   while (running) {

        // 通过Holder对象获得SurfaceCanvas成员变量

        Canvascanvas = holder.lockCanvas();

       // 先清理屏幕

        canvas.drawColor(Color.BLACK);

        // 初始化画笔

        Paintpaint = new Paint();

        // 设置抗锯齿,
文字清晰点,
不过相对比较耗性能

        paint.setAntiAlias(true);

        // 设置画笔颜色为红色

        paint.setColor(Color.RED);

        // 设置文字大小

        paint.setTextSize(50);

        // 画文字,
所画的内容都会缓存到Surface

        canvas.drawText("ITCAST", 100,
y++, paint);

       // 画完之后,
再将缓存的东西渲染到屏幕上

       holder.unlockCanvasAndPost(canvas);

    }

}

这里需要注意的是,每次绘图前都要先清理屏幕canvas.drawColor(Color.BLACK);

.

6> 改善下代码

public
void
run() {

    // Canvas放在外面提高性能,不必每次都定义这个变量

    Canvas canvas = null;

    // 将成员变量转为局部变量,也是提高性能

    SurfaceHolder holder = this.holder;

    while(running) {

       // 由于涉及到多线程,这个对象最好同步

       synchronized (holder) {

           // 画完后最好一定释放Canvas,渲染屏幕,避免内存泄露,所以用try-finally

           try {

              // 通过Holder对象获得SurfaceCanvas成员变量

              canvas = holder.lockCanvas();

             

              // 在这里进行绘图

              render(canvas);

             

              // 画完之后可以适当暂停,没必要持续画,人的眼睛有一定的反应时间

              Thread.sleep(100);

           } catch (InterruptedExceptione) {

              e.printStackTrace();

           } finally {

              // 严谨起见,最好判断canvas是否为null,不然unlockCanvasAndPost(null)会抛异常

              if (canvas !=
null) {

                  // 画完之后,
再将缓存的东西渲染到屏幕上

                  holder.unlockCanvasAndPost(canvas);

              }

           }

       }

    }

}

int
y
= 0;

/**

 * 绘图逻辑

 */

public
void
render(Canvas canvas) {

    // 先清理屏幕

    canvas.drawColor(Color.BLACK);

   

    // 初始化画笔

    Paint paint = new Paint();

    // 设置抗锯齿,
文字清晰点,
不过相对比较耗性能

    paint.setAntiAlias(true);

    // 设置画笔颜色为红色

    paint.setColor(Color.RED);

    // 设置文字大小

    paint.setTextSize(50);

    // 画文字,
所画的内容都会缓存到Surface

    canvas.drawText("ITCAST", 100,
y++, paint);

}

 

这就是整个SurfaceView的游戏框架

 

三、View与SurfaceView对比

View:必须在UI的主线程中更新画面,用于被动更新画面

surfaceView:UI线程和子线程中都可以。在一个新启动的线程中重新绘制画面,主动更新画面

抱歉!评论已关闭.