[Flutter] 和PreviewView结合起来看PlatformView的三种渲染模式

406 阅读11分钟

Preview

CameraX组件库,为我们提供了对相机设备更为简单、方便易用的使用组件,使得开发的门槛进一步降低,使用和官方文档可以戳这:

developer.android.google.cn/media/camer…

在XML布局文件中,我们只需要使用标签即可加入PreviewView视图到我们的View Hierarchy中,而代码中,我们只要使用简单地声明一个CameraController,即可使用它,PreviewView本身聚合了包括双指缩放在内的一系列的功能,我们只需要声明一个NativeView,和NativeViewFactory在其中创建PreviewView即可。

我们知道,Flutter自身有两种渲染模式:Surface与Texture,这两种模式有各自的优缺点在上一篇文章[Flutter]从Flutter来看TextureView和SurfaceView从Flutter来看TextureV - 掘金 (juejin.cn)中我们已经介绍过了,这里就不再进行赘述。 而PreviewView也有两种渲染模式,PERFORMANCE和COMPATIBLE,即性能模式和兼容模式,其实它们对应的分别也是使用SurfaceView、TextureView来渲染。

如果我们希望把PreviewView嵌入FlutterUI(我们把FlutterActivity对应的那块SurfaceView或者TextureView绘制的内容称为FlutterUI),那么就要使用到PlatfromView,从原理上,我们要如何把原生的内容给它展示到Flutter去呢?这正是我们今天的核心。

一、三种渲染方式简介:

PlatfromView在Android侧的三种渲染模式分别按照如下的方式进行渲染:

  1. VirtualDisplay:使用 VirtualDisplay 渲染原生控件到内存,然后利用 id 在 Flutter 界面上占用一个相应大小的位置,最后直接在内存里进行渲染,Flutter在绘制的时候直接从内存中根据TextureId再取出NativeView的纹理进行合成。
  2. HybridComposition:把原生View直接叠在Flutter视图View上进行渲染,如果Flutter侧有东西覆盖在PlatformView上(比如Stack视图的某个child),那么覆盖在PlatformView上的这一部分视图图层数据会被抽离出来,渲染成一个单独的原生View(使用显示布局边界可以很明显地看到悬浮在PlatformView上的FlutteWidget会有自己的原生边界)。
  3. TextureLayerHybridComposition:实现了一个PlatformViewWrapper,这玩意是一个FrameLayout,它会重写自己的draw方法,将所承接的nativeView的视图绘制到一块独立的Surface上,这一块Surface在Flutter实际进行绘制的时候(在native层)会把上面的图像数据取出再进行统一合成,如果NativeView是一个SurfaceView,对于TLHC模式的PlatformView来说不能说是有问题,只能说是完全不能用。
  1. TextureLayerHybridComposition诞生之后,原有的HybridComposition的初始化方法被替换成了:initExpensiveAndroidView,言下之意就是初始化一个「昂贵的」AndroidView,HybridComposition在Android9更早期、低端的设备上会有额外的性能花费。
  2. TextureLayer HybridComposition仍然是直接在ViewHierachy中渲染NativeView,不同之处在于通过「拦截」PlatformViewWrapper来拦截PlatformView的绘制逻辑,将图像内容「劫持」到指定的SurfaceTexture中,Engine在绘制的时候可以通过textureId找到这一块SurfaceTexture,然后提取出纹理数据直接绘制。这样看来硬伤就很明显了,诸如SurfaceView这一类的,不使用draw方法进行绘图的视图对于TextureLayer HybridComposition模式来说完全是抓瞎,不过我们不需要特地去处理这种情况,因为在PlatformViewsController中的createForTextureLayer方法,在PlatformViewsController选择具体使用哪种方式来进行渲染的时候就已经屏蔽了这种情况:

从渲染方式来说:

  1. VD和TLHC是两种基于原生视图纹理提取的渲染方式,只是提取方式不一样;而HC则是采用直接渲染的方式来渲染NativeView;

从NativeView的归属来说:

  1. VD渲染的NativeView并不直接属于ViewHierachy:

你可以看看蓝色框线其实是VirtualDisplay的实际渲染位置,但是它最终的纹理被提取到了FlutterSurfaceView中一起展示。

  1. 而HC和TLHC都是基于ViewHierachy绘制(View真正属于ViewHierachy当中)。只不过HC模式下,会直接将NativeView渲染出来:

  1. 而TLHC会在基于ViewHierachy进行测量、布局之后进一步将纹理提取出来,在Engine层面进行Flutter与原生的图层合成:

Content Commit ID:3c7aba2d7024a73f3d0619ce510f55213e9a40b7

二、实践:「3x2x2」种模式渲染

3:VirtualDisplay、HybridComposition、TextureLayerHybridComposition

2:渲染FlutterUI的方式:SurfaceView、TextView

2:渲染PreviewView的方式:PERFORMANCE(SurfaceView),COMPATIBLE(TextureView)

这里一共会有12种结果,我们统计两个内容:

  1. PreviewView是否能够正常展示;
  2. 我们对FlutterUI的Scaffold层级进行Widget截图操作,采集它的图像数据,查看Flutter层级是否含有PreviewView视图的图像数据;

具体如下:

以SurfaceView模式渲染FlutterUI

PreviewView模式PlatformView模式是否正常展示Flutter中对Widget截图是否包含预览内容
PERFORMANCEVD
PERFORMANCEHC
PERFORMANCETLHC❌(相机预览Surface会覆盖在主视图上方)
COMPATIBLEVD
COMPATIBLEHC
COMPATIBLETLHC

以TextureView模式渲染FlutterUI

PreviewView模式PlatformView模式是否正常展示Flutter中对Widget截图是否包含预览内容
PERFORMANCEVD
PERFORMANCEHC
PERFORMANCETLHC❌(预览页一闪而过然后就消失了)
COMPATIBLEVD
COMPATIBLEHC
COMPATIBLETLHC

好在无论是以SurfaceView还是TextureView模式来渲染FlutterActivity的UI,对于预览是否正常展示和截图是否包含预览信息的结果大致上是一致的,这样好歹我们能少一个影响因素,也就是只有六种场景了。

注意,比较新的FlutterSDK已经不会直接启用VirtualDisplay模式了,如果你真的需要启用VD模式去渲染,你需要满足两种条件:

  1. NativeView中含有SurfaceView,你可以添加一块空的SurfaceView进去然后再移除;
  2. 使用 PlatformViewsService.initAndroidView(...)在Flutter侧声明NativeView

2.1 表现模式分析

VD和TLHC

其中的VirtualDisplay模式和TLHC模式,都会给予一种纹理提取的方式来进行渲染NativeView,只不过提取的方式不同,VirtualDisplay模式渲染NativeView的时候会在一块内存中的、不依赖于ViewHierachy的区域去渲染指定的视图,然后在渲染时提取其纹理,与Flutter画面进行合成,达到「原生」嵌入「Flutter」的目的;

而TLHC则是在ViewHierachy上渲染,然后PlatformViewWrapper通过「挟持」draw方法,将NativeView的绘制内容渲染到一块指定的Surface上,然后在渲染时提取其纹理,与Flutter画面进行合成。

VirtualDisplay的提取纹理不经过draw方法,因此它可以正常提取以PERFORMANCE方法渲染的PreviewView中的纹理视图;而TLHC模式下的提取方式完全依赖PlatformViewWrapper的draw的绘制方法,对于一般的SurfaceView并不会有这个步骤,所以VD模式可以正常展示PERFORMANCE预览;而后者不行。

HC

对于HC模式,它采用堆叠的方式并不会直接去提取NativeView的纹理,而是直接渲染NativeView,仅对覆盖在NativeView上的Flutter部分图层做特殊处理,因此通常具有更高的兼容性,它能够正常渲染出以PERFORMANCE模式渲染的PreviewView。

如果采用COMPATIBLE模式去渲染PreviewView,那结果就比较统一了,三种方式都能兼容,HC就不用说了,直接渲染出来的意料之中也没有什么其他的问题;而VD和TLHC的提取纹理都能够成功提取TextureView上的图像数据,也都可以成功。

这里还有一个额外的指标:Flutter中对Widget截图是否包含PreviewView的图像数据。我们采用如下的方法对Flutter中的RepaintBoundary组件进行截图操作:

// 组件声明
return RepaintBoundary(
  key: _key,
  child: Scaffold(
      body: PlatformViewLink...
      
// 截图
RenderRepaintBoundary? boundary =
    _key?.findRenderObject() as RenderRepaintBoundary?;
var image = await boundary?.toImage(pixelRatio: compressionRatio);

重点观察image中,是否包含PlatformViewLink的图像数据,测试的结果是只有COMPATIBLE模式下的VD和TLHC模式下的两种方式能够在Flutter层感知到PreviewView的图像数据,HC模式并不能正确PreviewView获得图像数据,这也是符合直觉的,HC模式并不是依靠纹理提取去获取图像数据的;而VD和TLHC模式下,都会有PreviewView视图数据和Flutter视图混合的步骤,而通过这种方式去截图,又是直接从原生一侧去截取已经渲染完成的Flutter视图帧,必然是已经混合了PreviewView纹理数据的画面。

三、TLHC自动降级到HC的一个注意点

如果按照上面的说法,使用了PERFORMANCE模式来渲染PreviewViewTLHCHC两种模式的结果其实完全一样的,因为Flutter框架会为我们自动将TLHC降级成HC

但是我们实际渲染的结果却不一样,PreviewView本身是一个FrameLayout,它并不是一个SurfaceView,在PlatformView创建的时候PreviewView只有它自己,作为预览的SurfaceView仍然没有创建,所以这个时候PlatformViewsController去检查,并不能检查到SurfaceView的存在,这甚至是已知的问题,TLHC自动降级到HC的策略并不支持在PlatformView已经创建后,后续动态添加的SurfaceView:

github.com/flutter/flu…

也就是说PreviewView以性能模式(PERMORMANCE)渲染,PlatformView仍然会以TLHC模式去渲染,导致了BUG,对于这种情况,我们一般就手动在Flutter侧使用PlatformViewsService.initExpensiveAndroidView 直接声明成兼容模式:

controller = PlatformViewsService.initExpensiveAndroidView(
    id: params.id,
    viewType: "y_viewType",
    layoutDirection: TextDirection.ltr);  

或者在原生一侧先添加一块SurfaceView,然后加载完成之后再移除SurfaceView也能够正常使得TLHC降级到HC模式(但没啥必要,如果要降级用上面的方法即可,这里只是为了让更直观地感受到降级失败的原因):

val surfacePlaceHolderView = SurfaceView(context)
mPreviewView.addView(surfacePlaceHolderView)
android.os.Handler(Looper.getMainLooper()).postDelayed( {
mPreviewView.removeView(surfacePlaceHolderView)
} , 5000)

四、HybridComposition与FlutterImageView

在我们采用HC模式渲染如下的一个视图之后:

我们可以看到视图树中的FlutterView结点下,除了原本的FlutterSurfaceView来渲染整个Flutter主视图之外,还多出了三个View,分别是:

FlutterImageView、FlutterMutatorView和PlatformOverlayView。

其中FlutterMutatorView是HC模式下,用来渲染NativeView的主要容器,它的子View就是我们的PreviewView提供的预览视图区域。

FlutterImageView和PlatformOverlayView这二者就让人有些迷惑了。通过查看源码可以发现,PlatformOverlayView也是一种FlutterImageView的子类,只不过增加了一些关于Accessibility的支持实现,所以核心还是要看FlutterImageView。

FlutterImageView继承自一个View,实现了RenderSurface接口,它的作用很简单,按照它注释上所说的就一句话:将ImageReader提供的Flutter视图绘制到指定的Canvas上,具体是这么做的那么我们只需要全局搜索一下Canvas相关的内容,即可定位onDraw方法上:

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  if (currentImage != null) {
    updateCurrentBitmap();
  }
  if (currentBitmap != null) {
  canvas. drawBitmap (currentBitmap, 0 , 0 , null ); 
  }
}

这里所做的主要的事情就是将currentBitmap这个Bitmap绘制到Canvas上。我们能不能直观地看到这两张Bitmap是啥呢?

当然可以,我们将这俩Bitmap通过反射提取,然后赋值给两个不同的ImageView,就可以得到这么个画面,

左侧是FlutterImageView,右侧是PlatformOverlayView,左侧这东西只有Scaffold的大部分视图,而右下角的FloatingActionButton被PlatformView所覆盖的位置被截掉了;右侧这东西其实是我们Flutter视图中的红色数字和悬浮的FloatinActionButton的图层。

这个意思已经很明显了,从Z轴上来说,以PlatformView为分界线,覆盖在PlatformView上的图层构成了一个共同的区域(PlatformOverlayView),包括Flutter视图中央的数字0和整个FloatingActionButton;其余的、未被PlatformView覆盖的图层构成了另一个区域(FlutterImageView)。

如果你不启用FloatingActionButton、不在PlatformView上覆盖一个文本,那么就不会有PlatformOverlayView这个图层:

如果我们把左侧的FlutterImageView删除会发生什么?

结果和你想象的完全一样:

同理,再把我们把右侧的PlatformOverlayView删除:

这种情况下就只剩预览视图了,这就意味着FlutterTextureView居然啥都没展示。

其实只和我一开始预想的还是不一样,我只以为FlutterTextureView会渲染左侧的底层视图;PlatformOverlayView来承载悬浮在PlatformView上的视图。

那么问题又来了,我们把FlutterImageView和PlatformOverlayView都移除之后,界面上已经看不到FloatingActionButton按钮了,这个时候事件是否还可以正常响应?

答案是肯定的。

这就说明了一件事情,FlutterImageView的这两个实例都只展示图像,不负责触摸事件处理,比如你在FlutterImageView找不到任何Touch相关的内容:

简单来看看FlutterImageView的创建,找到参数最多的构造函数打上断点,我们可以看到如下的调用栈,第一次调用:

第二次调用:

显然,第一次是在onDisplayPlatformView调用中,创建的FlutterImageView;而第二次则是在createOverlaySurface调用中创建的PlatformOverlayView,也符合我们预期。

比较有意思的点其实在于第一次调用的堆栈中,initializeRootImageViewIfNeeded调用中有这么一个方法调用:

它所做的事情是将flutterView,转换成ImageView(模式),flutterView是FlutterImageView、FlutterTextureView和PlatformOverlayView的共同父View,它本身是一个FrameLayout。

convertToImageView中进行了创建FlutterImageView、然后addView的操作。

在完成这步操作之后,flutterViewConvertedToIamgeView变量被置为了true,后续会根据这个变量来决定这个PlatformView的实际渲染行为。