SurfaceView的介绍
SurfaceView与View的区别
- View是在主线程下对画面进行刷新的操作,而SurfaceView是一个独立线程来进行画面的刷新
- SufaceView有自己独立的绘图图层(View则是与其宿主窗口共享同一个绘图表面),这也就是为什么SurfaceView可以在独立线程中绘制
- SurfaceView底层是实现了双缓冲机制,而View是没有使用双缓冲机制的
- SurfaceView适合进行频繁的,或者刷新数据处理大的场景
相关的核心类
- SurfaceView
- Surface
- SurfaceHolder
一般的使用
一个例子,自动的绘制sin()曲线:
/**
* Created by panda on 2018/8/1
**/
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
private SurfaceHolder mHolder;
//用于绘图的canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
int x = 0;
int y = 400;
private Path mPath = new Path();
private Paint mPaint = new Paint();
public MySurfaceView(Context context) {
super(context);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
mPath.moveTo(x, y);
mPaint.setStyle(Paint.Style.STROKE);
// mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(10);
}
@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) {
try {
mCanvas = mHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
x += 1;
y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
mPath.lineTo(x, y);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != mCanvas) {
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
}
这里需要注意一点,获取到的Canvas对象还是上次的Canvas对象,而不是一个新的Canvas对象。因此,之前的绘图操作都会被保留。如果需要擦除,则可以在绘制前,通过drawColor()方法来进行清屏操作。
还有个例子,写字板功能:
public class MySurfaceViewForPath extends SurfaceView implements SurfaceHolder.Callback,Runnable{
private SurfaceHolder mHolder;
//用于绘图的canvas
private Canvas mCanvas;
//子线程标志位
private boolean mIsDrawing;
private Path mPath;
private Paint mPaint;
public MySurfaceViewForPath(Context context) {
super(context);
initView();
}
public MySurfaceViewForPath(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MySurfaceViewForPath(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
mHolder = getHolder();
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
this.setKeepScreenOn(true);
mPaint = new Paint();
mPath = new Path();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(10);
}
@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 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;
}
return true;
}
@Override
public void run() {
long start = System.currentTimeMillis();
while (mIsDrawing){
draw();
}
long end = System.currentTimeMillis();
if(end-start<100){
try {
Thread.sleep(100-(end-start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath,mPaint);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(null!=mCanvas){
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
SurfaceView的内部原理
https://blog.csdn.net/Luoshengyang/article/details/8661317
主要的流程是:
1、SurfaceView绘图图层的创建
触发的流程就不说了,我们看下最后onAttachedToWindow
方法:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mParent.requestTransparentRegion(this);
mSession = getWindowSession();
mLayout.token = getWindowToken();
mLayout.setTitle("SurfaceView - " + getViewRootImpl().getTitle());
mLayout.packageName = mContext.getOpPackageName();
mViewVisibility = getVisibility() == VISIBLE;
if (!mGlobalListenersAdded) {
ViewTreeObserver observer = getViewTreeObserver();
observer.addOnScrollChangedListener(mScrollChangedListener);
observer.addOnPreDrawListener(mDrawListener);
mGlobalListenersAdded = true;
}
}
主要是要知道2核心点:
- 通知父视图,需要在宿主窗口的绘图层面打开一个透明的区域。surface是Z-orderd的排序,它总是在窗口的后面。调用父view的
requestTransparentRegion()
方法。 - 通过
getWindowSession()
方法来得到实现了IWindowSession接口的Binder代理对象,保存到成员变量mSession中,来实现和WMS进行通信,这里SurfaceView可以请求WMS服务来创建绘制图层。
接着,将会调用顶层视图的视图的dispatchWindowVisibilityChanged()
方法来通知各子view的可见性。这里会调用到SurfaceView.onWindowVisibilityChanged()
:
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
mWindowVisibility = visibility == VISIBLE;
mRequestedVisible = mWindowVisibility && mViewVisibility;
updateWindow(false, false);
}
只有在mWindowVisibility(表示SurfaceView的宿主窗口的可见性)和mViewVisibility(表示SurfaceView自身的可见性),只有当都可见的时候才表示SurfaceView是可见的。
主要是在updateWindow()
完成操作的。
- 成员变量mSurface,Surface对象,描述的就是SurfaceView专有的绘图表面,它其实是个Parcelable类
- 成员变量 mWindow是IWindow接口的实现类,这有什么意义呢?意义是WMS认为Activityc窗口和SurfaceView窗口地位低同等的,都有绘图能力。
- 成员变量mWindowType描述的是SurfaceView的窗口类型
WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA
:显示多媒体WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY
:介于TYPE_APPLICATION_MEDIA
和application窗口之间的SurfaceView。比如可以用来显示字幕等信息。WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
:设置了这个,那么SurfaceView就会在application窗口上面- 可以通过
setWindowType()
、setZOrderMediaOverlay()
和setZOrderOnTop()
来改变mWindowType的值,从而改变SurfaceView的Z-ordered位置
- mHaveFrame:描述SurfaceView的宿主窗口的大小是否已经计算好了,只有宿主窗口大小ready,才能更新自己
- mRequestedHeight/mRequestedWidth:描述SurfaceView最后一次被请求的宽高
- mRequestedFormat:SurfaceView绘制的像素格式,缺省是
PixelFormat.RGB_565
- mUpdateWindowNeeded:SurfaceView是否被WindowManagerService服务通知执行一次UI更新操作。
- mReportDrawNeeded:SurfaceView是否被WindowManagerService服务通知执行一次UI绘制操作
- mLayout,WindowManager.LayoutParams对象,用来传递SurfaceView的布局参数以及属性值给WMS服务,以便WMS服务可以正确地维护它的状态
updateWindow()
大致的一个流程是:
WMS服务在对一个窗口进行布局的时候,如果发现该窗口的绘制表面还未创建,或者需要需要重新创建,那么就会为请求SurfaceFlinger服务为该窗口创建一个新的绘图表面,并且将该绘图表面返回来给调用者。在我们这个情景中,WMS服务返回来的绘图表面就会保存在成员变量mSurface。注意,这一步由于可能会修改SurfaceView的绘图表面,即修改成员变量mSurface的指向的一个Surface对象的内容,因此,就需要在获得成员变量mSurfaceLock所描述的一个锁的情况下执行,避免其它线程同时修改该绘图表面的内容,这是因为我们可能会使用一个独立的线程来来绘制SurfaceView的UI。
2、SurfaceView展现出自己
由于SurfaceView是在application窗口的下面的,所以为了能显示出自己,就需要突破application窗口,主要是调用ViewGroup.requestTransparentRegion()
方法。
实际上就是在其宿主窗口的绘图表面上设置一块透明区域,以便可以将自己显示出来。
SurfaceView会调用到DecorView,DecorView继承自ViewGroup:
public void requestTransparentRegion(View child) {
if (child != null) {
child.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
if (mParent != null) {
mParent.requestTransparentRegion(this);
}
}
}
设置child的(即SurfaceView)的mPrivateFlags的值为View.REQUEST_TRANSPARENT_REGIONS,表示宿主窗口上设置透明区域。然后再调用当前ViewGroup的父容器的requestTransparentRegion()来继续向上请求设置透明区域,知道窗口的顶层视图,这就到了ViewRootImpl类上:
@Override
public void requestTransparentRegion(View child) {
// the test below should not fail unless someone is messing with us
checkThread();
if (mView == child) {
mView.mPrivateFlags |= View.PFLAG_REQUEST_TRANSPARENT_REGIONS;
// Need to make sure we re-evaluate the window attributes next
// time around, to ensure the window has the correct format.
mWindowAttributesChanged = true;
mWindowAttributesChangesFlag = 0;
requestLayout();
}
}
在判断了线程是当前应用程序的主线程,以及传入的child正好是当前正在处理的ViewRootImpl所关联的顶层视图后,就设置当前顶层视图的mPrivateFlags的值为View.REQUEST_TRANSPARENT_REGIONS
由于窗口发生了变化,最后就调用了requestLayout()
对窗口的UI进行重新布局和绘制,这又回到了performTraversals()方法这里。
可以看出,SurfaceView的透明区域的设置实在窗口的完成layout之后draw之前。
再来看ViewGroup.gatherTransparentRegion
:
在视图容器下,会遍历每一个子视图,调用每个View的gatherTransparentRegion()
方法来继续往下收集。
3、绘制SurfaceView
SurfaceView虽然具有独立的绘图表面,不过它仍然是宿主窗口的视图结构中的一个结点,因此,它仍然是可以参与到宿主窗口的绘制流程中去的。
每一个子视图的成员函数draw或者dispatchDraw都会被调用到,以便它们可以绘制自己的UI。
@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);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
// if SKIP_DRAW is cleared, draw() has already punched a hole
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
// punch a whole in the view-hierarchy below us
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
}
super.dispatchDraw(canvas);
}
这里有个判断,如果当前正在处理的SurfaceView不是用来作宿主窗口,即不是WindowManager.LayoutParams.TYPE_APPLICATION_PANEL的时候,SurfaceView类的成员函数draw只是简单地将它所占据的区域绘制为黑色。(这里就解释了为啥不给SurfaceView绘制Color,它是黑色的了)。
绘图的整个过程大致就是如下的顺序进行:
- 在绘图表面的基础上建立一块画布,即获得一个Canvas对象。
- 利用Canvas类提供的绘图接口在前面获得的画布上绘制任意的UI。
- 将已经填充好了UI数据的画布缓冲区提交给SurfaceFlinger服务,以便SurfaceFlinger服务可以将它合成到屏幕上去。
可以概述为:
SurfaceHolder sh = sv.getHolder();
Cavas canvas = sh.lockCanvas()
// Draw something on canvas
// ......
sh.unlockCanvasAndPost(canvas);
上面的代码即可以在主线程调用,也可以在独立的线程调用(上面的demo是在独立线程)。