5分钟彻底搞懂Flutter中PlatFormView与Texture

2020-02-17 12:37:55 浏览数 (1)

想要在flutter想显示原生的东东,大家知道,一般有两种方式,一种是PlatformView,另外一种是Texture(俗称外接纹理)。其中PlatformView区分Android和iOS,在Android平上上叫做 AndroidView,而在iOS平台,叫UIKitView。而今天,我要说的是,

PlatformView和Texture看似两种不同的方式,其实是一种方式。

PlatformView https://api.flutter.dev/flutter/widgets/AndroidView-class.html

主要适用于flutter中不太容易实现的widget(Native中已经很成熟,并且很有优势的View),如官方的WebView。感兴趣的化可以了解一下。

Texture Texture class - widgets library - Dart API

既然有PlatformView可以在flutter中显示原生的view,我们为什么还需要Texture,简单的来说,显示一个view,过于繁重了点,我们有可能只需要显示那个数据而已,我们知道,原生向flutter传递数据,我们可以使用消息通道,大家一定知道MethodChannel.Result也一定玩过

代码语言:txt复制
result.success(data);

但是,举个栗子,假如我们要发送拍照的图片和录像的视频数据到flutter那边,是否可以走这个方式呢,理论上是没啥问题的,但是,如果我们采用消息通道将录像时摄像头采集的每一帧图片都要从原生传递到Flutter中,这样做代价将会非常大,因为<u>将图像或视频数据通过消息通道实时传输必然会引起内存和CPU的巨大消耗</u>!为此,Flutter提供了一种基于Texture的图片数据共享机制。

然而,今天我要说的是,<u>PlatformView其实也是使用外接纹理的方式实现的</u>,如果你不信,那好把,我们一起拨开这层神秘面纱吧。

首先,我们看下flutter创建一个PlatformView做了些什么

为了减少叙述,这里直接拿AndroidView来分析

代码语言:txt复制
const AndroidView({
    Key key,
    @required this.viewType,
    this.onPlatformViewCreated,
    this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
    this.layoutDirection,
    this.gestureRecognizers,
    this.creationParams,
    this.creationParamsCodec,
  }) : assert(viewType != null),
       assert(hitTestBehavior != null),
       assert(creationParams == null || creationParamsCodec != null),
       super(key: key);

我们看到AndroidView的构造函数中要求传一个viewType,然后常见的是,还有一个onPlatformViewCreated的回调。传viewType是告知插件为我们创建原生那个NativeView,可以是ImageView,AudioVIew等等,这些都需要在插件初始化时注册。

代码语言:txt复制
public interface PlatformViewRegistry {

    boolean registerViewFactory(String viewTypeId, PlatformViewFactory factory);

接下来,我们关心onPlatformViewCreated,其回调参数是一个id,我们继续跟踪AndroidView的构造函数,往下面走,最后,我们会走到<u>platform_views.dart</u>中的_create方法

代码语言:txt复制
 Future<void> _create(Size size) async {
    final Map<String, dynamic> args = <String, dynamic>{
      'id': id,
      'viewType': _viewType,
      'width': size.width,
      'height': size.height,
      'direction': _getAndroidDirection(_layoutDirection),
    };
    if (_creationParams != null) {
      final ByteData paramsByteData = _creationParamsCodec.encodeMessage(_creationParams);
      args['params'] = Uint8List.view(
        paramsByteData.buffer,
        0,
        paramsByteData.lengthInBytes,
      );
    }
    _textureId = await SystemChannels.platform_views.invokeMethod('create', args);
    _state = _AndroidViewState.created;
    for (PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) {
      callback(id);
    }
  }

我们看到_textureId = await SystemChannels.platform_views.invokeMethod('create', args);这里是通过platform_views调用原生方法,返回了一个_textureId,那么,这就能证明是创建了一个纹理吗?不服气,继续往下追,看看platform_views。在system_channels.dart中

代码语言:txt复制
static const MethodChannel platform_views = MethodChannel(
    'flutter/platform_views',
    StandardMethodCodec(),
  );

注册名是flutter/platform_views,我们取原生成去找,看看这个MethodChannel的create方法究竟干了啥,在PlatformViewsChannel.java 我们看到 。(<u>注意,同目录包下有很多Channel,感兴趣的可以了解下</u>)

代码语言:txt复制
public PlatformViewsChannel(@NonNull DartExecutor dartExecutor) {
    channel = new MethodChannel(dartExecutor, "flutter/platform_views", StandardMethodCodec.INSTANCE);
    channel.setMethodCallHandler(parsingHandler);
  }

发现确实是他注册了这个消息通道,那么,create方法肯定是他来处理,看他怎么处理,最终,我们跟踪到这个方法

代码语言:txt复制
public long createPlatformView(@NonNull PlatformViewsChannel.PlatformViewCreationRequest request) {
            ensureValidAndroidVersion();

            if (!validateDirection(request.direction)) {
                throw new IllegalStateException("Trying to create a view with unknown direction value: "
                      request.direction   "(view id: "   request.viewId   ")");
            }

            if (vdControllers.containsKey(request.viewId)) {
                throw new IllegalStateException("Trying to create an already created platform view, view id: "
                      request.viewId);
            }

            PlatformViewFactory viewFactory = registry.getFactory(request.viewType);
            if (viewFactory == null) {
                throw new IllegalStateException("Trying to create a platform view of unregistered type: "
                      request.viewType);
            }

            Object createParams = null;
            if (request.params != null) {
                createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params);
            }

            int physicalWidth = toPhysicalPixels(request.logicalWidth);
            int physicalHeight = toPhysicalPixels(request.logicalHeight);
            validateVirtualDisplayDimensions(physicalWidth, physicalHeight);

            TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
            VirtualDisplayController vdController = VirtualDisplayController.create(
                    context,
                    accessibilityEventsDelegate,
                    viewFactory,
                    textureEntry,
                    physicalWidth,
                    physicalHeight,
                    request.viewId,
                    createParams,
                    (view, hasFocus) -> {
                        if (hasFocus) {
                            platformViewsChannel.invokeViewFocused(request.viewId);
                        }
                    }
            );

            if (vdController == null) {
                throw new IllegalStateException("Failed creating virtual display for a "
                      request.viewType   " with id: "   request.viewId);
            }

            // If our FlutterEngine is already attached to a Flutter UI, provide that Android
            // View to this new platform view.
            if (flutterView != null) {
                vdController.onFlutterViewAttached(flutterView);
            }

            vdControllers.put(request.viewId, vdController);
            View platformView = vdController.getView();
            platformView.setLayoutDirection(request.direction);
            contextToPlatformView.put(platformView.getContext(), platformView);

            // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree.

            return textureEntry.id();
        }

我们看到关键部分

代码语言:txt复制
TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture();
。。。。。。
return textureEntry.id();

最后确实是返回的是纹理 textureId,至此,我们也证实了PlatformView其实背后也是用到了外接纹理。

在看看NativeView是怎么呈现到Flutter这边的

我们看官方实现的视频播放器的源码,(嗯,视频播放器是使用外接纹理方式)plugins/VideoPlayer.java at master · flutter/plugins · GitHub 第166行,关键代码

代码语言:txt复制
surface = new Surface(textureEntry.surfaceTexture());
exoPlayer.setVideoSurface(surface);

这里,通过TextureRegistry.SurfaceTextureEntry 这个entry拿到的这个surfaceTexture,是塞给了一个Surface,然后exoPlayer视频播放器将一帧帧的数据画到Surface上,这样,就能够实现数据共享了,也就是说,flutter端通过entry的那个textureId,就能用Texture展示数据啦。

代码语言:txt复制
 const Texture({
    Key key,
    @required this.textureId,
  }) : assert(textureId != null),
       super(key: key);

那么,PlatformView呢,怎么做的呢?是否也用Surface包了一下,然后把View给Draw到这个包装Surface上呢?这是我们的猜测,具体是不是,还是要看看源码。

代码语言:txt复制
public static VirtualDisplayController create(
            Context context,
            AccessibilityEventsDelegate accessibilityEventsDelegate,
            PlatformViewFactory viewFactory,
            TextureRegistry.SurfaceTextureEntry textureEntry,
            int width,
            int height,
            int viewId,
            Object createParams,
            OnFocusChangeListener focusChangeListener
    ) {
        textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
        Surface surface = new Surface(textureEntry.surfaceTexture());
        DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);

        int densityDpi = context.getResources().getDisplayMetrics().densityDpi;
        VirtualDisplay virtualDisplay = displayManager.createVirtualDisplay(
                "flutter-vd",
                width,
                height,
                densityDpi,
                surface,
                0
        );

同样的画面出现在了源码当中

代码语言:txt复制
textureEntry.surfaceTexture().setDefaultBufferSize(width, height);
Surface surface = new Surface(textureEntry.surfaceTexture());

而Surface也交给了VirtualDisplay管理,猜的没错的化,display的时候,就是把NativeView给Draw到这个Surface上,于是,我们在Flutter那边就看到这个NativeView。

PlatformView上的点击事件是如何从FLutter传递到原生的

确实你肯定也会好奇,那点击事件通过FLutter这边传递到原生的呢,其实,背后的实现是通过消息通道,将点击事件发送过去。

代码语言:txt复制
Future<void> sendMotionEvent(AndroidMotionEvent event) async {
    await SystemChannels.platform_views.invokeMethod<dynamic>(
        'touch',
        event._asList(id),
    );
  }

然后,在原生的中,我们发现,他处理了touch事件。

代码语言:txt复制
switch (call.method) {
        case "create":
          create(call, result);
          break;
        case "dispose":
          dispose(call, result);
          break;
        case "resize":
          resize(call, result);
          break;
        case "touch":
          touch(call, result);
          break;
        case "setDirection":
          setDirection(call, result);
          break;
        case "clearFocus":
          clearFocus(call, result);
          break;
        default:
          result.notImplemented();
      }

最终会交给PlatformViewFactory创建的那个与你目前联系的view来处理点击事件。

0 人点赞