AndroidSurfaceView(一)

1,196 阅读6分钟

SurfaceView 其优秀的特性让其广泛的应用在 Android 的视频、游戏、摄像头等处,我们一起来夯实一下吧:

View 与 SurfaceView

SurfaceView 构造与 SurfaceHolder.Callback

我们先掌握其用法再去探究他的原理, SurfaceView 很多地方和 View 一样,但也有一些不同之处我们来看看吧~

public class SurfaceView1 extends SurfaceView implements SurfaceHolder.Callback {

    public SurfaceView1(Context context) {
        this(context, null);
    }

    public SurfaceView1(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SurfaceView1(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        Log.e("zxm", "surfaceCreated");
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        Log.e("zxm", "surfaceChanged");
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        Log.e("zxm", "surfaceDestroyed");
    }
}

MainActivity 根据构造我们两种对 surfaceview 的引用方式:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.thunder.surfaceviewdmeo.SurfaceView1
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

另外一种直接 new 出 surfaceview 的实例:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new SurfaceView1(this));
    }
}

run 工程我们看下效果:

image.png

就像一个黑色的幕布

我们分别使用了两种构造函数与其他自定义 view 没有差异,但是多了一个 SurfaceHolder.Callback 的实现,有三个需要实现的方法:

方法含义
surfaceCreated
surfaceChanged
surfaceDestroyed

我们分别在方法实现中打印了日志,看下启动应用时候的日志:

image.png

然后关闭应用

image.png

接下来我们想要在 surfaceview 上去绘制我们想要的图形那起码得有画布吧得有画笔吧,我们先尝试把设置一个白色的画布。

@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    Log.e("zxm", "surfaceChanged");
    Canvas canvas = holder.lockCanvas();
    canvas.drawColor(Color.WHITE);
    holder.unlockCanvasAndPost(canvas);
}

image.png

然后尝试在白色背景的画布上画一个小方块,至于这里为什么有画布上锁和解锁的相关代码,应该是底下的双缓冲绘制机制的线程同步问题,我们会在后面的学习中逐渐深入其中的机制和原理。

public SurfaceView1(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    getHolder().addCallback(this);
    mPaint = new Paint();
    mPaint.setColor(Color.RED);
    mRect = new Rect();
}

@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
    Log.e("zxm", "surfaceCreated");
}

@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
    Log.e("zxm", "surfaceChanged");
    Canvas canvas = holder.lockCanvas();
    canvas.drawColor(Color.WHITE);
    mRect.set(0, 0, 100, 100);
    canvas.drawRect(mRect, mPaint);
    holder.unlockCanvasAndPost(canvas);
}

image.png

我们构造了一个画笔和方法,将他们都声明为成员变量在构造方法中去构造他们的实例。此处为什么直接在 surfaceChanged 方法中构造呢。顾名思义 surfaceChanged 方法可能在 surfaceview 产生变化的时候多次调用,这样的话我们的画笔是完全没有多次重复去创建实例的,这是我们编码的时候需要注意的小细节。

接下来我们探寻一个细节,surfaceChanged 的第一个参数是一个 holder ,SurfaceView 中全局也有个 getHolder() 方法我们看看他们是不是同一个 holder

image.png

17:21:16.675 7105-7105/com.thunder.surfaceviewdmeo E/zxm: holder : android.view.SurfaceView$3@ec94fdb
17:21:16.675 7105-7105/com.thunder.surfaceviewdmeo E/zxm: getHolder : android.view.SurfaceView$3@ec94fdb

和我们猜想的一样是同一个 holder 对象。

save 与 restore

我们继续来深入理解和学习,先在屏幕上绘制一条横向的红线

private void draw() {
    Canvas canvas = getHolder().lockCanvas();
    canvas.drawLine(0, getHeight() >> 1, getWidth(), getHeight() >> 1, mPaint);
    getHolder().unlockCanvasAndPost(canvas);
}

image.png

上图的位运算的实际含义就是 numder / 2。这个时候来思考一个问题如果我需要画一个十字交叉的红线有什么办法。

image.png

canvas.drawLine(0, getHeight() >> 1, getWidth(), getHeight() >> 1, mPaint);
canvas.drawLine(getWidth() >> 1, 0, getWidth() >> 1, getHeight(), mPaint);

再绘制一条竖向的不就行了吗?确实很简单还有没有别的办法呢?

canvas.save();
canvas.rotate(90, getWidth() >> 1, getHeight() >> 1);//px py 为以哪个位置作为旋转基点
canvas.drawLine(0, getHeight() >> 1, getWidth(), getHeight() >> 1, mPaint);
canvas.restore();
canvas.drawLine(0, getHeight() >> 1, getWidth(), getHeight() >> 1, mPaint);

image.png

当然有就是对当前的画布去进行操作,有旋转位移等操作。但是需要切记的是画布操作完以后需要恢复,不然后续的 drawXXX 方法绘制的图形全部都是在画布操作以后的状态上去绘制。

image.png

例如上面代码,注释掉 restore 后两个 drawLine 重叠在一起:

image.png

另外需要注意的是,如果对画布进行旋转位置操作。我们需要先去做了画布操作以后然后去绘制,否则也是不生效的,就举上面的例子:

image.png

我理解的是每次 canvas 去 drawXXX 系列方法的时候都是产生了一个 layer 按照代码的先后执行顺序向上盖,这个时候如果操过画布的位置完成了 layer 的样式和位置绘制。这层 layer 就定格在此处,后续我们需要恢复 restore 画布原来的位置然后去产生下一个 layer。

让 SurfaceView 动起来

  • 完成组合元素的盖压
  • 完成组合元素的移动动画和暂停控制

我们先来看下效果

20211022-104410.gif

gif 录制的压缩算法和帧率太低,导致了展示效果不佳。下面会贴上源代码和大概实现描述,有需要的小伙伴自行 Get

模拟了一个 vip 的标签从负屏幕展示出,完全展示后停下。然后操作上方 hide 按钮时标签收起

public class VIPLabelSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

    private final Paint mPaint;
    private final Rect mRect;
    private final Element element;
    private boolean mIsDrawing;
    private int offsetX = 0;
    private boolean reflect; //是否开始反方向
    private boolean isPause; //暂停

    public VIPLabelSurfaceView(Context context) {
        this(context, null);
    }

    public VIPLabelSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VIPLabelSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        getHolder().addCallback(this);
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mRect = new Rect(-200, 0, 0, 100);

        element = new Element();
        SelfCircle circle = new SelfCircle();
        SelfRect rect = new SelfRect();
        element.addChildrenView(rect);
        element.addChildrenView(circle);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        mIsDrawing = true;
        new Thread(this).start();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        mIsDrawing = false;
    }


    @Override
    public void run() {
        while (mIsDrawing) {
            Log.e("zxm", "isPause : " + isPause);
            if (isPause) {
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            long startTime = SystemClock.uptimeMillis();
            synchronized (VIPLabelSurfaceView.class) {

                Canvas canvas = null;
                try {
                    canvas = getHolder().lockCanvas();
                    canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                    doDraw(canvas);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (canvas != null)
                        getHolder().unlockCanvasAndPost(canvas);
                }
                final int frameRate = 50;
                final int frameTime = 1000 / frameRate;
                int passTime = (int) (SystemClock.uptimeMillis() - startTime);
                int sleepTime = frameTime - passTime;
                if (sleepTime <= 0) {
                    Log.d("zxm", "sleepTime <= 0, passTime:" + passTime);
                    continue;
                }
                try {
                    Thread.sleep(sleepTime);  //画一帧需要的时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void doDraw(Canvas canvas) {
        try {
            if (reflect) { //标签已经完全移出屏幕范围
                canvas.translate(offsetX -= 5, (getHeight() >> 1) - 50);
//                canvas.drawRect(mRect, mPaint); //单个元素
                element.draw(canvas);  //覆盖叠加元素

                if (offsetX <= 0) {//退出屏幕
                    isPause = true;
                    reflect = false;
                }
            } else {
                canvas.translate(offsetX += 5, (getHeight() >> 1) - 50);
//                canvas.drawRect(mRect, mPaint); //单个元素
                element.draw(canvas);  //覆盖叠加元素

                if (offsetX >= 200) {//完全展示出
                    isPause = true;
                    reflect = true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void show() {
        this.isPause = true;
    }

    public void hide() {
        isPause = false;
    }

    /**
     * 切歌切换资源样式
     */
    public void switchStyle() {

    }

    /**
     * 资源释放
     */
    public void release() {

    }

}

盖压元素方块和圆形的代码:

元素基类

public class Element {

    public List<Element> children;

    public Element() {
        children = new ArrayList<>();
    }

    public void draw(Canvas canvas) {
        childrenView(canvas);
        Log.e("draw", "size : " + children.size());
        for (Element child : children) {
            Log.e("draw", children.toString());
            child.draw(canvas);
        }

    }

    public void childrenView(Canvas canvas) {

    }

    public void addChildrenView(Element child) {
        children.add(child);
    }

    public void removeChildrenView(Element child) {
        children.remove(child);
    }

}

方块与圆形

public class SelfCircle extends Element {
    private final Paint mPaint;

    public SelfCircle() {
        mPaint = new Paint();
        mPaint.setColor(Color.YELLOW);
    }

    @Override
    public void childrenView(Canvas canvas) {
        canvas.drawCircle(-150, 50, 40, mPaint);
    }
}
public class SelfRect extends Element {
    private final Paint mPaint;

    public SelfRect() {
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
    }

    @Override
    public void childrenView(Canvas canvas) {
        canvas.drawRect(-200, 0, 0, 100, mPaint);
    }
}

完整的元素覆盖和平移动画就出来了,其中元素移动的核心关键开启了一个子线程内部无限循环调用 ,内部维护了一个 offsetX 的变量去移动

canvas.translate

去移动画布,切记这次每次得把上次的画布留影给清除,不然就会出现留影移动堆积的现象。

关于标签完全移出或者收起的暂停控制则是用了一个 isPause 的标志位。如果 isPause 为 true 绘制则不会继续通过 thread sleep 的方法休眠 30 ms 去跳出当前 while 循环。

各位看官,到此处应该知道本文是怎么实现元素的移动。留下一个疑问,这样的无限循环然后通过 sleep 去控制的方式是不是性能开销很大?有没有什么更好的实现方案?欢迎拍砖讨论