Android SurfaceView 源码分析及使用
概述
SurfaceView 是 Android 中一种比较特殊的视图(View),它跟平时时候的 TextView、Button 最大的区别是它跟它的视图容器并不是在同一个视图层上,它的 UI 显示也可以不在一个独立的线程中完成,所以对 SurfaceView 的绘制并不会影响到主线程的运行。综合这些特点,SurfaceView 一般用来实现动态的或者比较复杂的图像还有动画的显示。
SurfaceView 的 MVC 框架
要使用 SurfaceView ,就必须了解它的另外两个组件:Surface 和 SurfaceHolder.
Surface、SurfaceHolder 和 SurfaceView 三者之间的关系实质上就是广为人知的 MVC,即 Model-View-Controller。Model 就是模型的意思,或者说是数据模型,或者更简单地说就是数据,也就是这里的 Surface;View 即视图,代表用户交互界面,也就是这里的 SurfaceView;SurfaceHolder 很明显可以理解为 MVC 中的 Controller(控制器)。
应用场景
区别于普通的控件,SurfaceView 可以运行于主线程之外,不需要及时响应用户的输入,也不会造成响应的 ANR 问题。SurfaceView 一般用在游戏、视频、摄影等一些复杂 UI 且高效的图像的显示,这类的图像处理都需要开单独的线程来处理。
分析源码
分析 Surface,SurfaceHolder,SurfaceView 三个类
Surface
android.view.SurfaceView
/**
* Handle onto a raw buffer that is being managed by the screen compositor.
*/
public class Surface implements Parcelable {
// code......
}
首先来看 Surface 这个类,它实现了 Parcelable 接口进行序列化(这里主要用来在进程间传递 surface 对象),用来处理屏幕显示缓冲区的数据,源码中对它的注释为:
Handle onto a raw buffer that is being managed by the screen compositor.
Surface 是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。
- 由屏幕显示内容合成器 (screen compositor) 所管理的原生缓冲器的句柄(类似句柄)
- 名词解释:句柄,英文:HANDLE,数据对象进入内存之后获取到内存地址,但是所在的内存地址并不是固定的,需要用句柄来存储内容所在的内存地址。从数据类型上来看它只是一个 32 位 (或 64 位) 的无符号整数。
- Surface 充当句柄的角色,用来获取源生缓冲区以及其中的内容
- 源生缓冲区(raw buffer)用来保存当前窗口的像素数据
- 于是可知 Surface 就是 Android 中用来绘图的的地方,具体来说应该是 Surface 中的 Canvas
Surface 中定义了画布相关的 Canvas 对象
private final Canvas mCanvas = new CompatibleCanvas();
Java 中,绘图通常在一个 Canvas 对象上进行的,Surface 中也包含了一个 Canvas 对象,这里的 CompatibleCanvas 是 Surface.java 中的一个内部类,其中包含一个矩阵对象 Matrix(变量名 mOrigMatrix)。矩阵 Matrix 就是一块内存区域,针对 View 的各种绘画操作都保存在此内存中。
Surface 内部有一个 CompatibleCanvas 的内部类,这个内部类的作用是为了能够兼容 Android 各个分辨率的屏幕,根据不同屏幕的分辨率处理不同的图像数据。
private final class CompatibleCanvas extends Canvas {
// A temp matrix to remember what an application obtained via {@link getMatrix}
private Matrix mOrigMatrix = null;
@Override
public void setMatrix(Matrix matrix) {
if (mCompatibleMatrix == null || mOrigMatrix == null || mOrigMatrix.equals(matrix)) {
// don't scale the matrix if it's not compatibility mode, or
// the matrix was obtained from getMatrix.
super.setMatrix(matrix);
} else {
Matrix m = new Matrix(mCompatibleMatrix);
m.preConcat(matrix);
super.setMatrix(m);
}
}
@SuppressWarnings("deprecation")
@Override
public void getMatrix(Matrix m) {
super.getMatrix(m);
if (mOrigMatrix == null) {
mOrigMatrix = new Matrix();
}
mOrigMatrix.set(m);
}
}
两个重要的方法
需要说明的是,这里的 lockCanvas 并不是实际使用 SurfaceView 来进行绘图时 SurfaceHolder 对象调用的 lockCanvas 以及 unlockCanvasAndPost 方法。实际例子中使用的方法是在 SurfaceView 内部封装过对这两个方法封装之后的。
- lockCanvas(…)
+ Gets a Canvas for drawing into this surface. 获取进行绘画的 Canvas 对象
+ After drawing into the provided Canvas, the caller must invoke unlockCanvasAndPost to post the new contents to the surface. 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法把画布解锁,然后把画好的图像 Post 到当前屏幕上去显示
+ 当一个 Canvas 在被绘制的时候,它是出于被锁定的状态,就是说必须等待正在绘制的这一帧绘制完成之后并解锁画布之后才能进行别的操作
+ 实际锁住 Canvas 的过程是在 jni 层完成的
- unlockCanvasAndPost(…)
+ *Posts the new contents of the Canvas to the surface and releases the Canvas.* 将新绘制的图像内容传给 surface 之后这个 Canvas 对象会被释放掉(实际释放的过程是在 jni 层完成的)
Surface 的 lockCanvas 和 unlockCanvasAndPost 两个方法最终都是调用 jni 层的方法来处理,有兴趣可以看下相关的源码:
/frameworks/native/libs/gui/Surface.cpp
/frameworks/base/core/jni/android_view_Surface.cpp
SurfaceHolder
android.view.SurfaceHolder
SurfaceHolder 实际上是一个接口,它充当的是 Controller 的角色。
public interface SurfaceHolder {
}
来看下注释是怎么说的
- Abstract interface to someone holding a display surface. 一个针对 Surface 的抽象接口
- Allows you to control the surface size and format, edit the pixels in the surface, and monitor changes to the surface. 赤裸裸的 Controller 角色,可以控制 Surface 的大小和格式,监控 Surface 的变化(在回调函数中对 Surface 的变化做相应的处理)
- When using this interface from a thread other than the one running its SurfaceView, you will want to carefully read the methods Callback.surfaceCreated() 如果用子线程来处理 SurfaceView 的绘制,需要用到接下来要介绍的关键接口 Callback 中的 surfaceCreated 方法。可以看到之前给的例子中就是在 surfaceCreated 方法中开启的绘制动画的线程
关键接口 Callback
Callback 是 SurfaceHolder 内部的一个接口,例子中就实现了这个接口来控制绘制动画的线程。
接口中有以下三个方法
- public void surfaceCreated(SurfaceHolder holder);
+ Surface 第一次被创建时被调用,例如 SurfaceView 从不可见状态到可见状态时
+ 在这个方法被调用到 surfaceDestroyed 方法被调用之前的这段时间,Surface 对象是可以被操作的,拿 SurfaceView 来说就是如果 SurfaceView 只要是在界面上可见的情况下,就可以对它进行绘图和绘制动画
+ 这里还有一点需要注意,Surface 在一个线程中处理需要渲染的图像数据,如果你已经在另一个线程里面处理了数据渲染,就不需要在这里开启线程对 Surface 进行绘制了
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height);
+ Surface 大小和格式改变时会被调用,例如横竖屏切换时如果需要对 Sufface 的图像和动画进行处理,就需要在这里实现
+ 这个方法在 surfaceCreated 之后至少会被调用一次
- public void surfaceDestroyed(SurfaceHolder holder);
+ Surface 被销毁时被调用,例如 SurfaceView 从可见到不可见状态时
+ 在这个方法被调用过之后,就不能够再对 Surface 对象进行任何操作,所以需要保证绘图的线程在这个方法调用之后不再对 Surface 进行操作,否则会报错
SurfaceView
android.view.SurfaceView
SurfaceView,就是用来显示 Surface 数据的 View,通过 SurfaceView 来看到 Surface 的数据。
public class SurfaceView extends View {
// code.....
}
分析一下源码中对 SurfaceView 的注释
- Provides a dedicated drawing surface embedded inside of a view hierarchy. 在屏幕显示的视图层中嵌入了一块用做图像绘制的 Surface 视图
- the SurfaceView punches a hole in its window to allow its surface to be displayed. SurfaceView 在屏幕上挖了个洞来来世它所绘制的图像
- 挖洞是什么鬼?
- 这里引入一个 Z 轴的概念,SurfaceView 视图所在层级的 Z 轴位置是小于用来其宿主 Activity 窗口的 Layer 的 Z 轴的,就是说其实 SurfaceView 实际是显示在 Activity 所在的视图层下方的
- 那么问题就来了,为什么还是能看到 SurfaceView?形象一点的说法就是你在墙上凿了一个方形的洞,然后在洞上装了块玻璃,你就能看到墙后面的东西了。SurfaceView 就做了这样的事情,它把 Activity 所在的层当作了墙
- The Surface will be created for you while the SurfaceView’s window is visible. 这里说明了动画是什么时候开始的,当 SurfaceView 可见时,就可以开始在 Canvas 上绘制图像,并把图像数据传递给 Surface 用来显示在 SurfaceView 上
- you should implement SurfaceHolder.Callback#surfaceCreated and SurfaceHolder.Callback#surfaceDestroyed to discover when the Surface is created and destroyed as the window is shown and hidden. 在使用 SurfaceView 的地方需要实现 SurfaceHolder.CallBack 回调,来对 Surface 的创建和销毁进行监听以及做响应的处理,这里的处理指的是开始对 Canvas 进行绘制并把数据传递给 Surface 来做显示
使用 SurfaceView
例子中展示了如何使用 SurfaceView
- 需要实现 SurfaceHolder.Callback 接口
- 需要在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行动画的逐帧的绘制
- 需要在 SufaceHolder.Callback 的 surfaceDestroyed 方法中结束绘画的线程并调用 SurfaceHolder 的 removeCallbck 方法
- 绘画线程每一帧开始之前需要调用 lockCanvas 方法锁住画布进行绘图
- 绘制完一帧的数据之后需要调用 unlockCanvasAndPost 方法提交数据来显示图像
例子
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.BitmapDrawable;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder.Callback;
public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
private SurfaceHolder mHolder; // 用于控制SurfaceView
private Thread t; // 声明一条线程
private volatile boolean flag; // 线程运行的标识,用于控制线程
private Canvas mCanvas; // 声明一张画布
private Paint p; // 声明一支画笔
float m_circle_r = 10;
public MySurfaceView(Context context) {
super(context);
mHolder = getHolder(); // 获得SurfaceHolder对象
mHolder.addCallback(this); // 为SurfaceView添加状态监听
p = new Paint(); // 创建一个画笔对象
p.setColor(Color.WHITE); // 设置画笔的颜色为白色
setFocusable(true); // 设置焦点
}
/**
* 当SurfaceView创建的时候,调用此函数
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
t = new Thread(this); // 创建一个线程对象
flag = true; // 把线程运行的标识设置成true
t.start(); // 启动线程
}
/**
* 当SurfaceView的视图发生改变的时候,调用此函数
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
/**
* 当SurfaceView销毁的时候,调用此函数
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
flag = false; // 把线程运行的标识设置成false
mHolder.removeCallback(this);
}
/**
* 当屏幕被触摸时调用
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
/**
* 当用户按键时调用
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
surfaceDestroyed(mHolder);
return super.onKeyDown(keyCode, event);
}
@Override
public void run() {
while (flag) {
try {
synchronized (mHolder) {
Thread.sleep(100); // 让线程休息100毫秒
Draw(); // 调用自定义画画方法
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (mCanvas != null) {
// mHolder.unlockCanvasAndPost(mCanvas);//结束锁定画图,并提交改变。
}
}
}
}
/**
* 自定义一个方法,在画布上画一个圆
*/
protected void Draw() {
mCanvas = mHolder.lockCanvas(); // 获得画布对象,开始对画布画画
if (mCanvas != null) {
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(10);
paint.setStyle(Style.FILL);
if (m_circle_r >= (getWidth() / 10)) {
m_circle_r = 0;
} else {
m_circle_r++;
}
Bitmap pic = ((BitmapDrawable) getResources().getDrawable(
R.drawable.qq)).getBitmap();
mCanvas.drawBitmap(pic, 0, 0, paint);
for (int i = 0; i < 5; i++)
for (int j = 0; j < 8; j++)
mCanvas.drawCircle(
(getWidth() / 5) * i + (getWidth() / 10),
(getHeight() / 8) * j + (getHeight() / 16),
m_circle_r, paint);
mHolder.unlockCanvasAndPost(mCanvas); // 完成画画,把画布显示在屏幕上
}
}
}
可能碰到的一些问题
为什么把一个 SurfaceView 放在布局文件中不做任务图像的绘制,它会显示一个黑色的区域?
造成这个现象的原因可以在 SurfaceView 的 draw 和 dispatchDraw 方法中看到,SurfaceView 中,windownType 变量被初始化为WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,所以在创建绘制这个 View 的过程中整个 Canvas 会被涂成黑色
// windowType 的默认值
int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
@Override
public void draw(Canvas canvas) {
if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
// draw() is not called when SKIP_DRAW is set
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
// punch a whole in the view-hierarchy below us
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
}
super.draw(canvas);
}