SurfaceView
Android系统提供View进行绘图处理,View可以满足大部分的绘图需求,但是View有一个弊端,Android通过发出VSYNC信号进行屏幕的重绘,刷新的间隔时间为16ms,如果在16ms内View完成所需的操作,用户视觉上则不会产生卡顿的感觉。而如果执行的逻辑操作太多,特别是频繁刷新界面,那么就会不断阻塞主线程,从而导致画面卡顿。
Skipped 1188 frames! The application may be doing too much work on its main thread.
为避免该情况的发生,Android系统提供SurfaceView组件来解决该问题。它和View的区别如下:
View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁刷新。
View在主线程中对画面进行刷新,而SurfaceView通常在子线程进行页面刷新。
View在绘图时没有使用双缓冲机制,而SurfaceView在底层实现机制中已经实现双缓冲机制。
总结:如果自定义View需要频繁刷新,或者刷新数据量比较大则使用SurfaceView。
SurfaceView基础
要想使用SurfaceView需要经过创建、初始化、使用三个步骤
创建SurfaceView
我们需要自定义一个类继承自SurfaceView,并且实现两个接口以及接口定义的方法
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
//创建
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//改变
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
//销毁
}
@Override
public void run() {
//子线程
}
}
三个构造函数的写法和自定义View是相同的,接下来的三个方法分别在SurfaceView创建、改变、销毁的时候进行调用,而run()方法中写我们子线程中执行的绘图逻辑即可。
初始化SurfaceView
我们主要是定义三个成员变量以备后面绘图时使用,然后初始化这三个成员变量并且注册对应的回调方法。代码如下:
private SurfaceHolder mSurfaceHolder;
//绘图的Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView(){
mSurfaceHolder = getHolder();
//注册回调方法
mSurfaceHolder.addCallback(this);
//设置一些参数方便后面绘图
setFocusable(true);
setKeepScreenOn(true);
setFocusableInTouchMode(true);
}
使用SurfaceView
在使用SurfaceView的时候,可以分为如下三个步骤:
(1) 通过lockCanvas()方法获得Canvas对象
(2) 在子线程中使用Canvas对象进行绘制
(3) 使用unlockCanvasAndPost()方法将画布内容进行提交
lockCanvas() 方法获得的Canvas对象仍然是上次绘制的对象,由于不断进行绘制,而每次得到的Canvas对象都是第一次创建的Canvas对象。
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mSurfaceHolder;
//绘图的Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
public SurfaceViewTemplate(Context context) {
this(context, null);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
//开启子线程
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing){
drawSomething();
}
}
//绘图逻辑
private void drawSomething() {
try {
//获得canvas对象
mCanvas = mSurfaceHolder.lockCanvas();
//绘制背景
mCanvas.drawColor(Color.WHITE);
//绘图
}catch (Exception e){
}finally {
if (mCanvas != null){
//释放canvas对象并提交画布
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
/**
* 初始化View
*/
private void initView(){
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
setFocusable(true);
setKeepScreenOn(true);
setFocusableInTouchMode(true);
}
}
在xml文件中的使用和自定义View是相同的,使用全路径名称即可:
<com.legend.view.SurfaceViewTemplate
android:layout_width="match_parent"
android:layout_height="match_parent" />
SurfaceView扩展
下面我们编写两个案例:绘制正弦函数和手绘板
绘制正弦函数
大体的框架都是上面给的那个代码模板,区别只在于初始化画笔,和具体的绘图逻辑,所以这里不再赘述,直接上代码:
public class SurfaceViewSinFun extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mSurfaceHolder;
//绘图的Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
private int x = 0, y = 0;
private Paint mPaint;
private Path mPath;
public SurfaceViewSinFun(Context context) {
this(context, null);
}
public SurfaceViewSinFun(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewSinFun(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(5);
mPath = new Path();
//路径起始点(0, 100)
mPath.moveTo(0, 100);
initView();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing){
drawSomething();
x += 1;
y = (int)(100 * Math.sin(2 * x * Math.PI / 180) + 400);
//加入新的坐标点
mPath.lineTo(x, y);
}
}
private void drawSomething() {
try {
//获得canvas对象
mCanvas = mSurfaceHolder.lockCanvas();
//绘制背景
mCanvas.drawColor(Color.WHITE);
//绘制路径
mCanvas.drawPath(mPath, mPaint);
}catch (Exception e){
}finally {
if (mCanvas != null){
//释放canvas对象并提交画布
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
/**
* 初始化View
*/
private void initView(){
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
setFocusable(true);
setKeepScreenOn(true);
setFocusableInTouchMode(true);
}
}
手写板
主要是涉及到触摸事件,在手指按下时将Path的起始点移动到按下的坐标点,手指移动时将移动的坐标点加入Path中,其他的代码是相同的。代码如下:
public class SurfaceViewHandWriting extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mSurfaceHolder;
//绘图的Canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
//画笔
private Paint mPaint;
//路径
private Path mPath;
private static final String TAG = "pyh";
public SurfaceViewHandWriting(Context context) {
this(context, null);
}
public SurfaceViewHandWriting(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SurfaceViewHandWriting(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setAntiAlias(true);
mPath = new Path();
mPath.moveTo(0, 100);
initView();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsDrawing = true;
new Thread(this).start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsDrawing = false;
}
@Override
public void run() {
while (mIsDrawing) {
long start = System.currentTimeMillis();
drawSomething();
long end = System.currentTimeMillis();
if (end - start < 100) {
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.moveTo(x, y);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(x, y);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* 初始化View
*/
private void initView(){
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
setFocusable(true);
setKeepScreenOn(true);
setFocusableInTouchMode(true);
}
private void drawSomething() {
try {
//获得canvas对象
mCanvas = mSurfaceHolder.lockCanvas();
//绘制背景
mCanvas.drawColor(Color.WHITE);
//绘制路径
mCanvas.drawPath(mPath, mPaint);
}catch (Exception e){
}finally {
if (mCanvas != null){
//释放canvas对象并提交画布
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}