SurfaceView 基本使用
SurfaceView 基本使用
Android中提供了View进行绘图处理,View可以满足大部分的绘图需求,但是有时候,View却显得力不从心,所以Android提供了SurfaceView
给Android开发者,以满足更多的绘图需求。下面就让我们一起来了解一下SurfaceView。
1. SurfaceView的作用
View是通过刷新来重绘视图,Android 系统通过发出VSYNC
信号来进行屏幕的重绘,刷新的时间间隔是16ms,
如果我们可以在16ms以内将绘制工作完成,则没有任何问题,如果我们绘制过程逻辑很复杂,并且我们的界面更新还非常频繁,这时候就会造成界面的卡顿,影响用户体验,为此Android提供了SurfaceView
来解决这一问题。
SurfaceView 继承自
View
,但拥有独立的绘制表面,它不与其宿主窗口共享同一个绘图表面,可以单独在一个线程进行绘制,并不会占用主线程的资源。这样,绘制就会比较高效,游戏,视频播放,还有最近热门的直播,都可以用SurfaceView来实现。SurfaceView 有两个子类 GLSurfaceView 和 VideoView。
SurfaceView
和View
的区别:
1)View
主要适用于主动更新的情况下,而SurfaceView
主要适用于被动更新,例如频繁地刷新。
2)View
在主线程中对画面进行刷新,而SurfaceView
通常会通过一个子线程来进行页面的刷新。
3)View在绘图时没有使用双缓冲机制,而SufaceView
在底层实现机制中就已经实现了双缓冲机制。
2. SurfaceView的使用
使用 SurfaceView 基本可以按照三个步骤来实现,分别是:创建、初始化、使用。下面我们分别来介绍着三个步骤。
2.1 创建SurfaceView
我们需要自定义一个类MySurfaceView 继承自SurfaceView,并实现两个接口SurfaceHolder.CallBack
和Runnable,代码如下所示:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback,Runnable {public MySurfaceView(Context context) {this(context, null);}public MySurfaceView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}// SurfaceView 创建时调用@Overridepublic void surfaceCreated(SurfaceHolder holder) {}// SurfaceView 改变时调用@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}// SurfaceView 销毁时调用@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}@Overridepublic void run() {}
}
SurfaceVHolder.Callback 的三个方法分别在SurfaceView创建、改变、销毁的时候进行调用,Runnalbe 的run()
方法中写我们子线程中执行的绘图逻辑即可。
2.2 初始化SurfaceView
在自定义的SurfaceView中,通常需要定义3个成员变量:
1)SurfaceHolder mSurfaceHolder 可以控制SurfaceView
的大小,格式,可以监控或者改变SurfaceView。
2)Canvas mCanvas 画布
3)boolean isDrawing 子线程标志位,用来控制子线程。
初始化SurfaceView主要就是定义上述三个成员变量并初始化SurfaceHolder并注册对应的回到方法,代码如下所示:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {private SurfaceHolder mSurfaceHolder;private Canvas mCanvas;private boolean mIsDrawing;...public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}/*** 初始化*/private void init() {// 获取 SurfaceHolder对象mSurfaceHolder = getHolder();//注册 SurfaceHoldermSurfaceHolder.addCallback(this);// 设置可以获取焦点setFocusable(true);// 设置触摸可以获取焦点setFocusableInTouchMode(true);// 保持屏幕常亮this.setKeepScreenOn(true);}...
}
2.3 使用SurfaceView
使用SurfaceView也可以分为三个步骤:
1)通过lockCanvas()
方法获得Canvas对象。
2) 在子线程中使用Canvas对象进行绘制。
3)使用unlockCanvasAndPost()
方法将画布内容进行提交。
注意: lockCanvas()
方法获得的Canvas对象仍然是上次绘制的对象,由于我们是不断进行绘制,但是每次得到的Canvas对象都是第一次创建的Canvas对象。
绘制要充分利用SurfaceView
的三个回调方法,在surfaceCreate()
方法中开启子线程进行绘制。在子线程中,使用一个while(isDrawing)
循环来不停地绘制。具体的绘制过程,由lockCanvas()
方法进行绘制,并通过unlockCanvasAndPost(mCanvas)
进行画布内容的提交。
实现代码如下所示:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {private SurfaceHolder mSurfaceHolder;private Canvas mCanvas;private boolean mIsDrawing;public MySurfaceView(Context context) {this(context, null);}public MySurfaceView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}/*** 初始化*/private void init() {// 获取 SurfaceHolder对象mSurfaceHolder = getHolder();//注册 SurfaceHoldermSurfaceHolder.addCallback(this);// 设置可以获取焦点setFocusable(true);// 设置触摸可以获取焦点setFocusableInTouchMode(true);// 保持屏幕常亮this.setKeepScreenOn(true);}// SurfaceView 创建时调用@Overridepublic void surfaceCreated(SurfaceHolder holder) {mIsDrawing = true;// 通过线程池来执行Executors.newCachedThreadPool().execute(this);}// SurfaceView 改变时调用@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}// SurfaceView 销毁时调用@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false;}@Overridepublic void run() {while (mIsDrawing) {drawing();}}private void drawing() {try {// 获取Canvas 画布mCanvas = mSurfaceHolder.lockCanvas();// 下面进行内容的绘制...} finally {if (mCanvas != null) {mSurfaceHolder.unlockCanvasAndPost(mCanvas);}}}
}
mSurfaceHolder.unlockCanvasAndPost(mCanvas)
将这行代码放入finally
代码块中,目的是为了确保内容都能够被提交。
3. 使用案例
自定义SurfaceView 实现一个简易画板,初始代码如下所示:
package com.lx.surfaceview.widget;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;import java.util.concurrent.Executors;/*** create by lx* date 2020/8/24.* description:*/
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {private SurfaceHolder mSurfaceHolder;private Canvas mCanvas;private boolean mIsDrawing;// 画笔private Paint mPaint;// 路径private Path mPath;// 上次的坐标private float mLastX, mLastY;public MySurfaceView(Context context) {this(context, null);}public MySurfaceView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}/*** 初始化*/private void init() {// 获取 SurfaceHolder对象mSurfaceHolder = getHolder();//注册 SurfaceHoldermSurfaceHolder.addCallback(this);// 设置可以获取焦点setFocusable(true);// 设置触摸可以获取焦点setFocusableInTouchMode(true);// 保持屏幕常亮this.setKeepScreenOn(true);// 初始化画笔mPaint = new Paint();mPaint.setStrokeWidth(10);mPaint.setColor(Color.BLACK);mPaint.setStyle(Paint.Style.STROKE);mPaint.setAntiAlias(true);// 初始化路径mPath = new Path();}// SurfaceView 创建时调用@Overridepublic void surfaceCreated(SurfaceHolder holder) {mIsDrawing = true;// 通过线程池来执行Executors.newCachedThreadPool().execute(this);}// SurfaceView 改变时调用@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}// SurfaceView 销毁时调用@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {mIsDrawing = false;}@Overridepublic void run() {while (mIsDrawing) {drawing();}}private void drawing() {try {// 获取Canvas 画布mCanvas = mSurfaceHolder.lockCanvas();// 下面进行内容的绘制mCanvas.drawColor(Color.WHITE);mCanvas.drawPath(mPath, mPaint);} finally {if (mCanvas != null) {mSurfaceHolder.unlockCanvasAndPost(mCanvas);}}}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 获取当前触摸事件的坐标float x = event.getX();float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:// 按下mLastX = x;mLastY = y;mPath.moveTo(mLastX, mLastY);break;case MotionEvent.ACTION_MOVE:// 移动float dx = Math.abs(x - mLastX);float dy = Math.abs(y - mLastY);if (dx >= 3 || dy >= 3) {mPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);}mLastX = x;mLastY = y;break;case MotionEvent.ACTION_UP:// 抬起break;}return true;}
}
运行效果如下图:
4. 优化
由于 while (isDrawing)
是个死循环,drawing
方法一直在执行,就导致一直在绘制,这样会一直占用CPU资源。
1. 使用sleep休眠,在run方法中进行休眠,代码如下:
public void run() {while (mIsDrawing) {long start = System.currentTimeMillis();drawing();long end = System.currentTimeMillis();if (end - start < 100) {try {Thread.sleep(100 - (end - start));} catch (InterruptedException e) {e.printStackTrace();}}}}
使用sleep休眠的方式虽然可以降低CPU占用率,但是每次绘制时都会休眠(100ms - drawing时间),绘制过程中有种不跟手的感觉。
2. 使用wait和notify方法,当没有绘制时让线程等待,当有绘制时唤醒线程,代码如下所示:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {...private boolean mIsWait = true;@Overridepublic void run() {drawing();synchronized (MySurfaceView.this) {while (mIsDrawing) {if (mIsWait) {try {MySurfaceView.this.wait();} catch (InterruptedException e) {e.printStackTrace();}}drawing();}}}...@Overridepublic boolean onTouchEvent(MotionEvent event) {// 获取当前触摸事件的坐标float x = event.getX();float y = event.getY();switch (event.getAction()) {case MotionEvent.ACTION_DOWN:synchronized (MySurfaceView.this) {mIsWait = false;MySurfaceView.this.notify();}// 按下mLastX = x;mLastY = y;mPath.moveTo(mLastX, mLastY);break;case MotionEvent.ACTION_MOVE:// 移动float dx = Math.abs(x - mLastX);float dy = Math.abs(y - mLastY);if (dx >= 3 || dy >= 3) {mPath.quadTo(mLastX, mLastY, (mLastX + x) / 2, (mLastY + y) / 2);}mLastX = x;mLastY = y;break;case MotionEvent.ACTION_UP:// 抬起mIsWait = true;break;}return true;}
}
手指落下,将mIsWait设置为false表示不需要等待,并唤醒线程。
手指离开,将mIsWait设置为true表示需要等待。