解决SurfaceView渲染的各类疑难杂症

======================== 2020.3.24 修改部分过期API =============================html

RecyclerView加载多个surfaceview覆盖,旋转,黑屏 ??java

SurfaceView黑色背景??透明背景??多层嵌套被遮挡??android

苦苦找了好多天,各个论坛问遍了,都是互相抄,痛苦的我,尝试了好多种方式都解决不了。git

翻了几天surfaceview的源码和API,现把解决方法总结,分享一下。github


首先说:不能在list视图中使用VideoView,由于VideoView继承SurfaceView,web

而SurfaceView不支持UI同步缓冲(UI synchronization buffer),这致使当滑动list时视频会丢进度。编程

TextureView支持同步缓冲,但没有基于TextureView的VideoView。这个问题至今我没法解决。canvas

使用WebRTC 58 想实现一个视频窗口列表,几十个视频窗口,划出屏幕就暂停,显示时就开始加载。仍没法没法实现WebRTC内使用的是SurfaceView。app

赠送源码:https://github.com/yugu88/MagicWXide

《最完整的Android逆向知识体系》

这个问题有 愿意讨论 或者 作过相似效果,或者知道如何处理的,请告知我一声,十万分的感谢。。。


看一下google的API都有哪些:

多层嵌套被遮挡:

setZOrderOnTop(boolean onTop) // 在最顶层,会遮挡一切view

setZOrderMediaOverlay(boolean isMediaOverlay)// 如已绘制SurfaceView则在surfaceView上一层绘制。

网上不少人都会告诉你第一个,几乎都是互相抄袭,应用在游戏里还能够,多窗口视频是不能够的。

若是在surfaceView上绘制surfaceView应该用第二个,而且必须在addview以后调用。

  • layout.addView(surfaceView);
  • surfaceView.setZOrderMediaOverlay(true);   // 必须layout.addView以后使用,必须动态调用。

SurfaceView 怎么进行旋转,透明操做的?

  • 普通View旋转后,View的内容也跟着同步作了旋转.

  • SurfaceView在旋转以后,其显示内容并无跟着一块儿旋转.

比喻:这就比如在墙上开了一个窗(Surface),经过窗口能够看外面的花花世界,但窗口不管怎样变化,窗外面的世界是不会跟着窗口一同变化。

通常视频播放器能够横竖屏切换,是如何实现的?
在 Activity 中覆写 onConfigurationChanged 方法就能够。根据横竖屏切换,修改 SurfaceView 的 Parameter 的宽高参数便可。

android:configChanges="orientation|keyboardHidden|screenSize"
@Override  
public void onConfigurationChanged(Configuration newConfig) {  

    super.onConfigurationChanged(newConfig);  

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {  
            //变成横屏了    
    }   else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {  
            //变成竖屏了 
    }  
}

不绘制任何东西,SurfaceView显示的是黑色?

每次更新视图时都会先将背景绘制成黑色。因此在移动或者缩放过程,会更新不及时时就会看黑边。

@Override
public void draw(Canvas canvas) {
    if (mDrawFinished && !isAboveParent()) {
        // 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);
}

//这句话表示PorterDuff.Mode.CLEAR会将像素设置为0,也就是黑色
//Destination pixels covered by the source are cleared to 0.
public enum Mode {
    // these value must match their native equivalents. See SkXfermode.h
    /**
     * <p>
     *     <img src="{@docRoot}reference/android/images/graphics/composite_CLEAR.png" />
     *     <figcaption>Destination pixels covered by the source are cleared to 0.</figcaption>
     * </p>
     * <p>\(\alpha_{out} = 0\)</p>
     * <p>\(C_{out} = 0\)</p>
     */
    CLEAR       (0),   
}

SurfaceView背景问题:

不少人介绍了好多种方法改变他的黑色背景为透明。我初步加载时就直接看到了桌面,直接透过去了。并无遇到不少人说的默认黑色背景。

缘由是设置的主题问题,因为我自定义了dialog。有没法去除的背景白条,因此设置了主题,这个白条背景也是主题引发的。然而这个主题

也形成了透明的效果。

尝试好多天,翻遍各类API,才忽然意识到多是主题不一样引发的。

studio建立工程时的默认主题:

<!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

这个主题是默认SurfaceView为黑色背景,固然dialog也是有没法去除的背景,想设置半透明就要设置一个主题。

<style name="Theme.AppStartLoadTranslucent" parent="android:Theme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowNoTitle">true</item>
        
    </style>

注意看主题的值:parent="android:Theme"   ,item不重要。若是是:DarkActionBar 就是黑色背景。

我换成了这个主题,dialog的背景就成功去除了,固然surfaceView默认加载就是透到桌面的,没有背景,若是设置背景会遮盖住画面。必须换一个主题。

网上你会搜到不少更改背景为透明的方法,好比:

  • surfaceView.setZOrderOnTop(true);
  • surfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT);   // 设置画布  背景透明

这是在默认的主题下设置的。屡次尝试后“Theme.Design.NoActionBar” 这个主题很通用。能够尝试一下。有问题能够留言,咱们再讨论。

当你发现一些可以API对你的View不起做用,尤为是透明度,背景,圆角等,你感受主题能够设置的一切属性,你要先检查一下本身设置的主题样式是否是对他有限制。

能够尝试换个主题试试呢,没准会有意外收获。。

赠送源码:https://github.com/yugu88/MagicWX

《最完整的Android逆向知识体系》


SurfaceView的生命周期管理有三个方法:

  • SurfaceCreated

  • SurfaceChanged

  • SurfaceDestoryed

如何使用SurfaceView呢?

一、获取SurfaceHolder对象,其是SurfaceView的内部类。

  • 监听Surface生命周期。

  • 只有当native层的Surface建立完毕以后,才能够调用lockCanvas(),不然失败。
    holder.Callback。

二、调用holder.lockCanvas()。
三、绘制
四、调用SurfaceHolder.unlockCanvasAndPost,将绘制内容post到Surface中

SurfaceView的特色有那些

  • 具备独立的绘图表面Surface。

  • 须要在宿主窗口上挖一个洞来显示本身,z轴比普通的window要小。

  • 它的UI绘制能够在独立的线程中进行,这样就能够进行复杂的UI绘制,而且不会影响应用程序的主线程响应用户输入。

SurfaceView的优缺点

优势

  • 在一个子线程中对本身进行绘制,避免形成UI线程阻塞。

  • 高效复杂的UI效果。

  • 独立Surface,独立的Window。

  • 使用双缓冲机制,播放视频时画面更流畅。

缺点

  • 每次绘制都会优先绘制黑色背景,更新不及时会出现黑边现象。

  • Surface不在View hierachy中,它的显示也不受View的属性控制,平移,缩放等变换。

  • 不支持UI同步缓冲

SurfaceVeiw双缓冲区

双缓冲:在运用时能够理解为: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。

至关与多个线程,交替解析和渲染每一帧视频数据。

surfaceholder.lockCanvas--surfaceholder.unlockCanvasAndPost

SurfaceView 和普通的View的区别?

  • surfaceView是在一个新起的单独线程中能够从新绘制画面。

  • View必须在UI的主线程中更新画面。

  • 那么在UI的主线程中更新画面可能会引起问题,好比你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将没法响应按键,触屏等消息。

  • 当使用surfaceView 因为是在新的线程中更新画面因此不会阻塞你的UI主线程。

SurfaceView 生命周期

使用:双缓冲
致使:须要更多的内存开销
为了节约系统内存开销:

SurfaceView 可见时 -> 建立 SurfaceHolder
SurfaecView 不可见时 -> 摧毁 SurfaceHolder

一、程序打开
Activity 调用顺序:onCreate()->onStart()->onResume()
SurfaceView 调用顺序: surfaceCreated()->surfaceChanged()

二、程序关闭(按 BACK 键)
Activity 调用顺序:onPause()->onStop()->onDestory()
SurfaceView 调用顺序: surfaceDestroyed()

三、程序切到后台(按 HOME 键)
Activity 调用顺序:onPause()->onStop()
SurfaceView 调用顺序: surfaceDestroyed()

四、程序切到前台
Activity 调用顺序: onRestart()->onStart()->onResume()
SurfaceView 调用顺序: surfaceChanged()->surfaceCreated()

五、屏幕锁定(挂断键或锁定屏幕)
Activity 调用顺序: onPause()
SurfaceView 什么方法都不调用

六、屏幕解锁 
Activity 调用顺序: onResume()
SurfaceView 什么方法都不调用

横屏录制横屏播放,竖屏录制竖屏播放
经过如下方法能够获取到视频的宽高,根据视频的宽高就能够知道该视频是横屏仍是竖屏录制的。

public void onVideoSizeChanged(MediaPlayer mp, int width, int height)

横屏判断:width>height
旋转屏幕:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

竖屏录制:height>width
旋转屏幕:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        Log.e(TAG, "onVideoSizeChanged:WIDTH>>" + width);
        Log.e(TAG, "onVideoSizeChanged:HEIGHT>>" + height);

        if (width > height) {
            //横屏录制
            if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            }
        } else {
            //竖屏录制
            if (getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            }
        }
    }
});

View的绘制要知道的知识
View的绘制实际上是在UI线程(实现onCanvas方法进行绘制)。若是进行绘制高效复杂的UI,最好不用自定义View。要用SurfaceView进行绘制。

SurfaceView 能绘制什么东西?
从下面代码能够看到,SurfaceView 的绘制也是使用 Canvas 进行绘制的,绘制应该跟普通的 View 绘制差很少

/**
 * 绘制
 */
private void draw() {
    if (radius > getWidth()) {
        return;
    }
    Canvas canvas = mHolder.lockCanvas();
    if (canvas != null) {
        canvas.drawCircle(300, 300, radius += 10, mPaint);
        mHolder.unlockCanvasAndPost(canvas);
    }
}

View的绘画三要素

  • Canvas (画布,绘制BitMap操做)

  • Paint (绘制的画笔Paint,颜色、样式)

  • Path (路径)

1、Canvas

  • 若是直接extends View 能够重写onDraw(Canvas canvas)方法,直接用里面的canvas进行绘制。

  • 能够直接利用Activity的绘制机制,用lockCanvas()方法来得到当前的Activity的Canvas。

  • 在SurfaceView中,同2能够利用SurfaceHolder的对象的lockCanvas()方法来Canvas。

2、Paint

直接经过new关键字来实例化,而后经过Paint对象来对画笔进行相应的设置:
如:

  • 1.1 去锯齿setAntiAlia(true)

  • 1.2 去抖动setDither(true)

  • 1.3 设置图层混合模式setXfermode(Xfermode,xfermode)

3、 Path

  • 一、Path路径 直接用new来实例化

  • 二、经过path对象设置想要画图的轨迹或路线,如:矩形 、三角形 、圆、曲线等


综上所述:

为何视频技术入门要先了解图片绘制,那么图片绘制的API也有多种,为何选择用SurfaceView这个API,

由于:

其一,绘制是在子线程中进行绘制的,

其二,可能绘制出高效复杂的UI效果,

其三,使用双缓冲机制,播放视频时画面更流畅。


找到的一些小项目源码:https://github.com/yugu88/webRTC_Library

很值得学习的一个视频直播项目 https://github.com/littleMeng/video-live

赠送源码:https://github.com/yugu88/MagicWX

《最完整的Android逆向知识体系》

沉溺于编程不可自拔时,也要记得多多锻炼。。