学而实习之 不亦乐乎

Android 中 SurfaceView 的使用

2023-07-14 20:01:20

一、SurfaceView 概述

在 Android 系统中,有一种特殊的视图,称为 SurfaceView。SurfaceView 本身是一个View,符合一切 View 的特性,需要通过Canvas画布绘制。

SurfaceView 拥有独立的 Surface(绘图表面)
SurfaceView 是用 Zorder 排序的,他默认在宿主 Window 的后面,SurfaceView 通过在 Window 上面“挖洞”(设置透明区域)进行显示。

SurfaceView与View的区别

  1. View的绘图效率不高,主要用于动画变化较少的程序
  2. SurfaceView 绘图效率较高,用于界面更新频繁的程序
  3. SurfaceView 拥有独立的 Surface(绘图表面),即它不与其宿主窗口共享同一个 Surface。

一般来说,每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。

因此 SurfaceView 的 UI 就可以在一个独立的线程中进行绘制,可以不会占用主线程资源。

二、双缓冲机制

SurfaceView使用双缓冲机制,播放视频时画面更流畅,那什么是双缓冲机制呢?

在运用时可以理解为:SurfaceView 在更新视图时用到了两张 Canvas,一张 frontCanvas 和一张 backCanvas ,每次实际显示的是 frontCanvas ,backCanvas 存储的是上一次更改前的视图。当你在播放这一帧的时候,它已经提前帮你加载好后面一帧了,所以播放起视频很流畅。

当使用 lockCanvas() 获取画布时,得到的实际上是 backCanvas 而不是正在显示的 frontCanvas ,之后你在获取到的 backCanvas 上绘制新视图,再 unlockCanvasAndPost(canvas)此视图,那么上传的这张 canvas 将替换原来的 frontCanvas 作为新的 frontCanvas ,原来的 frontCanvas 将切换到后台作为 backCanvas 。例如,如果你已经先后两次绘制了视图A和B,那么你再调用 lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你将重绘的 A 视图上传,那么 A 将取代 B 作为新的 frontCanvas 显示在SurfaceView 上,原来的B则转换为backCanvas。
相当与多个线程,交替解析和渲染每一帧视频数据。

三、使用场景

SurfaceView 一方面可以实现复杂而高效的 UI,另一方面又不会导致用户输入得不到及时响应。常用于画面内容更新频繁的场景,比如游戏、视频播放和相机预览。

使用 SurfaceView

1、获取 SurfaceHolder 对象,其是 SurfaceView 的内部类。添加回调监听 Surface 生命周期。

mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);

2、surfaceCreated 回调后启动绘制线程

只有当native层的Surface创建完毕之后,才可以调用lockCanvas(),否则失败。

@Override
public void surfaceCreated(SurfaceHolder holder) {
    mDrawThread = new DrawThread();
    mDrawThread.start();
}

3、绘制

Canvas canvas = mSurfaceHolder.lockCanvas();
// 使用canvas绘制内容 
...
mSurfaceHolder.unlockCanvasAndPost(canvas);

使用SurfaceView不显示问题
发生这种问题的原因是多层嵌套被遮挡
解决方法是根据具体情况调用如下api接口:

setZOrderOnTop(boolean onTop) // 在最顶层,会遮挡一切view
setZOrderMediaOverlay(boolean isMediaOverlay)// 如已绘制SurfaceView则在surfaceView上一层绘制。

看下他们的源码:

public void setZOrderMediaOverlay(boolean isMediaOverlay) {
    mSubLayer = isMediaOverlay
        ? APPLICATION_MEDIA_OVERLAY_SUBLAYER : APPLICATION_MEDIA_SUBLAYER;
}

public void setZOrderOnTop(boolean onTop) {
    if (onTop) {
        mSubLayer = APPLICATION_PANEL_SUBLAYER;
    } else {
        mSubLayer = APPLICATION_MEDIA_SUBLAYER;
    }
}

两个方法都是给mSubLayer赋值,所以需要注意这两个接口同时调用后一个会覆盖前一个的效果。

黑色背景问题

//设置背景透明
mHolder.setFormat(PixelFormat.TRANSPARENT);