[关闭]
@TryLoveCatch 2021-08-26T14:20:47.000000Z 字数 20438 阅读 4640

Flutter之与原生View交互

flutter


Flutter之在Flutter布局中嵌入原生组件Android篇

Android侧编写并注册原生组件

PlatformView

  1. MyPlatformView implements PlatformView {
  2. @Override
  3. public View getView() {
  4. return new TextView();
  5. }
  6. @Override
  7. public void dispose() {
  8. }
  9. }

这里我们返回一个TextView

PlatformViewFactory

  1. MyPlatformViewFactory extends PlatformViewFactory {
  2. @Override
  3. public PlatformView create(Context context, int id, Object args){
  4. returen new MyPlatformView(context);
  5. }

PlatformViewFactory的主要任务是,在create()方法中创建一个View并把它传给Flutter(这个说法并不准确,但是我们姑且可以这么理解,后续会进行解释)

返回就是第一步的PlatformView

注册插件

  1. public class MyPlugin {
  2. public static void registerWith(Registrar registrar) {
  3. if (registrar.activity() == null) {
  4. // When a background flutter view tries to register the plugin, the registrar has no activity.
  5. // We stop the registration process as this plugin is foreground only.
  6. return;
  7. }
  8. registrar
  9. .platformViewRegistry()
  10. .registerViewFactory(
  11. "plugins.test/my_view", new MyPlatformViewFactory(registrar));
  12. }
  13. }

"plugins.test/my_view",这是组件的注册名称,在Flutter调用时需要用到,你可以使用任意格式的字符串,但是两端必须统一。
注册了自己写的那个MyPlatformViewFactory

GeneratedPluginRegistrant

系统会自动生产两个类,MainActivity和GeneratedPluginRegistrant:

  1. public class MainActivity extends FlutterActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. GeneratedPluginRegistrant.registerWith(this);
  6. }
  7. }

MainActivity里面会调用GeneratedPluginRegistrant里面的静态方法:

  1. public final class GeneratedPluginRegistrant {
  2. public static void registerWith(PluginRegistry registry) {
  3. if (alreadyRegisteredWith(registry)) {
  4. return;
  5. }
  6. }
  7. private static boolean alreadyRegisteredWith(PluginRegistry registry) {
  8. final String key = GeneratedPluginRegistrant.class.getCanonicalName();
  9. if (registry.hasPlugin(key)) {
  10. return true;
  11. }
  12. registry.registrarFor(key);
  13. return false;
  14. }
  15. }

可以看到registerWith()什么也没做,我们需要注册咱们写的插件:

  1. public static void registerWith(PluginRegistry registry) {
  2. if (alreadyRegisteredWith(registry)) {
  3. return;
  4. }
  5. // GoogleMapsPlugin.registerWith(registry.registrarFor("io.flutter.plugins.googlemaps.GoogleMapsPlugin"));
  6. MyPlugin.registerWith(registry);
  7. }

Flutter侧调用原生View

  1. void main() => runApp(MyApp());
  2. class MyApp extends StatelessWidget {
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. title: 'Flutter Demo',
  7. theme: ThemeData(
  8. primarySwatch: Colors.blue,
  9. ),
  10. home: MyHomePage(title: 'Flutter Demo'),
  11. );
  12. }
  13. }
  14. class MyHomePage extends StatefulWidget {
  15. MyHomePage({Key key, this.title}) : super(key: key);
  16. final String title;
  17. @override
  18. _MyHomePageState createState() => _MyHomePageState();
  19. }
  20. class _MyHomePageState extends State<MyHomePage> {
  21. @override
  22. Widget build(BuildContext context) {
  23. return Scaffold(
  24. appBar: AppBar(
  25. title: Text(widget.title),
  26. ),
  27. body: Center(
  28. // 主要是这里!!!!!!!
  29. child: AndroidView(viewType: 'plugins.test/my_view'),
  30. ),
  31. );
  32. }
  33. }

在使用Android平台的view只需要创建AndroidView组件并告诉它组件的注册注册名称即可,这个名字,就是咱们在Android侧注册的名字。

如果你是双平台的实现,则可以通过引入package:flutter/foundation.dart包,并判断defaultTargetPlatform是TargetPlatform.android还是TargetPlatform.iOS来引入不同平台的实现。

给原生view增加参数

flutter

  1. class _MyHomePageState extends State<MyHomePage> {
  2. @override
  3. Widget build(BuildContext context) {
  4. print(defaultTargetPlatform);
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text(widget.title),
  8. ),
  9. body: Center(
  10. child: AndroidView(
  11. viewType: 'plugins.test/my_view',
  12. // 这里!!!!!!!
  13. creationParams: {
  14. "myContent": "通过参数传入的文本内容",
  15. },
  16. creationParamsCodec: const StandardMessageCodec(),
  17. ),
  18. ),
  19. );
  20. }
  21. }

android

  1. public class MyPlatformView implements PlatformView {
  2. private final TextView myNativeView;
  3. MyPlatformView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
  4. TextView myNativeView = new TextView(context);
  5. myNativeView.setText("我是来自Android的原生TextView");
  6. this.myNativeView = myNativeView;
  7. if (params.containsKey("myContent")) {
  8. String myContent = (String) params.get("myContent");
  9. myNativeView.setText(myContent);
  10. }
  11. }
  12. ...
  13. }

原生组件初始化的参数并不会随着setState重复赋值

通过MethodChannel与原生组件通讯

android

  1. public class MyPlatformView implements PlatformView, MethodChannel.MethodCallHandler {
  2. private final TextView myNativeView;
  3. MyPlatformView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
  4. ...
  5. MethodChannel methodChannel = new MethodChannel(messenger, "plugins.test/my_channel_" + id);
  6. methodChannel.setMethodCallHandler(this);
  7. }
  8. @Override
  9. public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
  10. // 在接口的回调方法中可以接收到来自Flutter的调用
  11. }
  12. ...
  13. }

MethodChannel也需要一个唯一的标示,我们这里是用"plugins.test/my_channel_" + id来标示,那么这个id是什么呢,继续看

flutter

  1. class _MyHomePageState extends State<MyHomePage> {
  2. @override
  3. Widget build(BuildContext context) {
  4. print(defaultTargetPlatform);
  5. return Scaffold(
  6. appBar: AppBar(
  7. title: Text(widget.title),
  8. ),
  9. body: Center(
  10. child: AndroidView(
  11. viewType: 'plugins.test/my_view',
  12. creationParams: {
  13. "myContent": "通过参数传入的文本内容",
  14. },
  15. creationParamsCodec: const StandardMessageCodec(),
  16. onPlatformViewCreated: onMyViewCreated,
  17. ),
  18. ),
  19. );
  20. }
  21. MethodChannel _channel;
  22. void onMyViewCreated(int id) {
  23. _channel = new MethodChannel('plugins.test/my_channel_$id');
  24. setMyViewText();
  25. }
  26. Future<void> setMyViewText(String text) async {
  27. assert(text != null);
  28. return _channel.invokeMethod('setText', text);
  29. }
  30. }
  • onPlatformViewCreated, 监听原始组件成功创建,并能够在回调方法的参数中拿到当前组件的id,这个id是系统随机分配的
  • invokeMethod(),来调用native的方法
  1. @Override
  2. public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
  3. if ("setText".equals(methodCall.method)) {
  4. String text = (String) methodCall.arguments;
  5. myNativeView.setText(text);
  6. result.success(null);
  7. }
  8. }

总结

在一个界面中实例化多个原生组件的情况对性能的影响非常的大,也不建议在实际开发中大量引入原生组件,因为除去地图/WebView等特殊情况,基本上原生能实现的UI效果Flutter的UI引擎都能实现。

注意:

原理

在Flutter中嵌入Native组件的正确姿势
下面我们来说说原理性的东西,问几个问题:

前提准备

自定义一个View,我们就直接继承TextView:

  1. public class MyTextView extends TextView {
  2. public MyTextView(Context context) {
  3. super(context);
  4. }
  5. public MyTextView(Context context, @Nullable AttributeSet attrs) {
  6. super(context, attrs);
  7. }
  8. public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  9. super(context, attrs, defStyleAttr);
  10. }
  11. public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
  12. super(context, attrs, defStyleAttr, defStyleRes);
  13. }
  14. @Override
  15. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  16. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  17. Log.e("xxxxx", "MyTextView onMeasure > width: " + getMeasuredWidth() + ", height: " + getMeasuredHeight());
  18. }
  19. @Override
  20. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  21. super.onLayout(changed, left, top, right, bottom);
  22. Log.e("xxxxx", "MyTextView onLayout > left: " + left + ", top: " + top + ", right: " + right + ", bottom: " + bottom);
  23. }
  24. @Override
  25. protected void onDraw(Canvas canvas) {
  26. super.onDraw(canvas);
  27. Log.e("xxxxx", "MyTextView onDraw");
  28. }
  29. @Override
  30. public boolean dispatchTouchEvent(MotionEvent event) {
  31. Log.e("xxxxx", "MyTextView dispatchTouchEvent");
  32. return super.dispatchTouchEvent(event);
  33. }
  34. }

修改MyPlatformViewFactory:

  1. MyPlatformViewFactory extends PlatformViewFactory {
  2. public MyPlatformViewFactory() {
  3. Log.e("xxxxx", "PlatformViewFactory init");
  4. }
  5. @Override
  6. public PlatformView create(Context context, int id, Object args){
  7. Log.e("xxxxx", "PlatformViewFactory create > id: " + id);
  8. returen new MyPlatformView(context);
  9. }

修改MyPlatformView:

  1. MyPlatformView implements PlatformView {
  2. private MyTextView mTxtView;
  3. public MyPlatformView() {
  4. Log.e("xxxxx", "PlatformView init");
  5. mTxtView = new MyTextView(registrar.context());
  6. mTxtView.setOnClickListener(new View.OnClickListener() {
  7. @Override
  8. public void onClick(View view) {
  9. Log.e("xxxxx", "PlatformView onClick");
  10. }
  11. });
  12. mTxtView.setOnLongClickListener(new View.OnLongClickListener() {
  13. @Override
  14. public boolean onLongClick(View view) {
  15. Log.e("xxxxx", "PlatformView onLongClick");
  16. return false;
  17. }
  18. });
  19. mTxtView.setText("123456789");
  20. }
  21. @Override
  22. public View getView() {
  23. Log.e("xxxxx", "PlatformView getView");
  24. return mTxtView;
  25. }
  26. @Override
  27. public void dispose() {
  28. Log.e("xxxxx", "PlatformView dispose");
  29. }
  30. }

运行程序

  1. 2019-09-04 11:53:28.411 25300-25300/? E/xxxxx: PlatformViewFactory init
  2. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformViewFactory create > id: 0
  3. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformView init
  4. 2019-09-04 11:53:29.940 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  5. 2019-09-04 11:53:29.949 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  6. 2019-09-04 11:53:29.963 25300-25300/fluttermapexample E/xxxxx: MyTextView onMeasure > width: 1080, height: 1680
  7. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView onMeasure > width: 1080, height: 1680
  8. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView onLayout > left: 0, top: 0, right: 1080, bottom: 1680
  9. 2019-09-04 11:53:29.995 25300-25300/fluttermapexample E/xxxxx: MyTextView onDraw
  • PlatformView getView调用了两次
  • MyTextView的onMeasure、onLayout、onDraw都执行了,并且大小为设备的大小

点击TextView

  1. 2019-09-04 11:57:25.922 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  2. 2019-09-04 11:57:25.922 25300-25300/fluttermapexample E/xxxxx: MyTextView dispatchTouchEvent
  3. 2019-09-04 11:57:25.927 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  4. 2019-09-04 11:57:25.927 25300-25300/fluttermapexample E/xxxxx: MyTextView dispatchTouchEvent
  5. 2019-09-04 11:57:25.929 25300-25300/fluttermapexample E/xxxxx: PlatformView onClick
  • 每次事件传递,都会执行一次PlatformView getView
  • PlatformViewFactory的构造函数和create(),都只执行一次
  • PlatformView的构造函数也只执行一次

更换Native View

  1. return AndroidView(
  2. viewType: viewType,
  3. onPlatformViewCreated: _onViewCreate,
  4. creationParams: {
  5. "type": 6
  6. },
  7. creationParamsCodec: const StandardMessageCodec(),
  8. );

既然AndroidView是一个Widget,那么我们是不是可以通过setState(),来更新AndroidView的viewType呢?当然是可以的了

  1. setState(() {
  2. _viewType = "plugins.test/my_view_2";
  3. });

我们替换"plugins.test/my_view""plugins.test/my_view_2",不过仅仅这样是不够的,因为Android端需要跟Flutter一致,我们看一下注册插件的地方:

  1. public class MyPlugin {
  2. public static void registerWith(Registrar registrar) {
  3. if (registrar.activity() == null) {
  4. return;
  5. }
  6. registrar
  7. .platformViewRegistry()
  8. .registerViewFactory(
  9. "plugins.test/my_view", new MyPlatformViewFactory(registrar));
  10. }
  11. }

registrar.platformViewRegistry()得到的是PlatformViewRegistry接口,唯一实现为PlatformViewRegistryImpl,我们可以看一下:

  1. class PlatformViewRegistryImpl implements PlatformViewRegistry {
  2. private final Map<String, PlatformViewFactory> viewFactories = new HashMap();
  3. PlatformViewRegistryImpl() {
  4. }
  5. public boolean registerViewFactory(String viewTypeId, PlatformViewFactory factory) {
  6. if (this.viewFactories.containsKey(viewTypeId)) {
  7. return false;
  8. } else {
  9. this.viewFactories.put(viewTypeId, factory);
  10. return true;
  11. }
  12. }
  13. PlatformViewFactory getFactory(String viewTypeId) {
  14. return (PlatformViewFactory)this.viewFactories.get(viewTypeId);
  15. }
  16. }

里面是一个HashMap存的viewType和PlatformViewFactory的对应关系,所以说,我们可以registerViewFactory多次:

  1. public class MyPlugin {
  2. public static void registerWith(Registrar registrar) {
  3. if (registrar.activity() == null) {
  4. return;
  5. }
  6. registrar
  7. .platformViewRegistry()
  8. .registerViewFactory(
  9. "plugins.test/my_view", new MyPlatformViewFactory(registrar));
  10. PlatformViewRegistry tPlatformViewRegistry = registrar.platformViewRegistry();
  11. MyPlatformViewFactory tMyPlatformViewFactory = new MyPlatformViewFactory(registrar);
  12. tPlatformViewRegistry.registerViewFactory(
  13. "plugins.test/my_view", tMyPlatformViewFactory);
  14. MyPlatformViewFactory2 tMyPlatformViewFactory2 = new MyPlatformViewFactory2(registrar);
  15. tPlatformViewRegistry.registerViewFactory(
  16. "plugins.test/my_view_2", tMyPlatformViewFactory2);
  17. }
  18. }

好了,这样就可以了,我们运行程序的时候,还是跟上面一样的:

  1. 2019-09-04 11:53:28.411 25300-25300/? E/xxxxx: PlatformViewFactory init
  2. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformViewFactory create > id: 0
  3. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformView init
  4. 2019-09-04 11:53:29.940 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  5. 2019-09-04 11:53:29.949 25300-25300/fluttermapexample E/xxxxx: PlatformView getView
  6. 2019-09-04 11:53:29.963 25300-25300/fluttermapexample E/xxxxx: MyTextView onMeasure > width: 1080, height: 1680
  7. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView onMeasure > width: 1080, height: 1680
  8. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView onLayout > left: 0, top: 0, right: 1080, bottom: 1680
  9. 2019-09-04 11:53:29.995 25300-25300/fluttermapexample E/xxxxx: MyTextView onDraw

然后,我们执行setState()来更改viewType:

  1. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformView dispose
  2. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformViewFactory create > id: 1
  3. 2019-09-04 11:53:29.899 25300-25300/fluttermapexample E/xxxxx: PlatformView2 init
  4. 2019-09-04 11:53:29.940 25300-25300/fluttermapexample E/xxxxx: PlatformView2 getView
  5. 2019-09-04 11:53:29.949 25300-25300/fluttermapexample E/xxxxx: PlatformView2 getView
  6. 2019-09-04 11:53:29.963 25300-25300/fluttermapexample E/xxxxx: MyTextView2 onMeasure > width: 1080, height: 1680
  7. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView2 onMeasure > width: 1080, height: 1680
  8. 2019-09-04 11:53:29.989 25300-25300/fluttermapexample E/xxxxx: MyTextView2 onLayout > left: 0, top: 0, right: 1080, bottom: 1680
  9. 2019-09-04 11:53:29.995 25300-25300/fluttermapexample E/xxxxx: MyTextView2 onDraw
  • 先执行了上一个PlatformView#dispose()
  • PlatformViewFactory#create()再次执行,这一次id变成了1
  • PlatformView2重新了初始化的流程
  • 两个View切换的间隔,会有黑屏现象

其实,仔细想想,跟上面连接里面的结论也是不冲突的,因为我们改变了Native View,Flutter也会立马得知,然后进行改变。所以就感觉是屏幕显示的是我们自己的native view,其实并不是,都是Flutter来渲染的。

后来发现的问题

注意,后来发现这里是有问题的:

我们上面是通过改变AndroidView的viewType,来切换Native View,但是Flutter会存在一个bug

  1. java.lang.NullPointerException: Attempt to invoke virtual method 'int android.view.Display.getDisplayId()' on a null object reference
  2. at android.view.SurfaceView.updateWindow(SurfaceView.java:539)
  3. at android.view.SurfaceView$3.onPreDraw(SurfaceView.java:162)
  4. at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:944)
  5. at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2245)
  6. at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1290)
  7. at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6399)
  8. at android.view.Choreographer$CallbackRecord.run(Choreographer.java:873)
  9. at android.view.Choreographer.doCallbacks(Choreographer.java:685)
  10. at android.view.Choreographer.doFrame(Choreographer.java:621)
  11. at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:859)
  12. at android.os.Handler.handleCallback(Handler.java:754)
  13. at android.os.Handler.dispatchMessage(Handler.java:95)
  14. at android.os.Looper.loop(Looper.java:165)
  15. at android.app.ActivityThread.main(ActivityThread.java:6375)
  16. at java.lang.reflect.Method.invoke(Native Method)
  17. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:912)
  18. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:802)

参考;https://github.com/flutter/flutter/issues/26345

  1. java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.View io.flutter.plugin.platform.VirtualDisplayController.getView()' on a null object reference
  2. at io.flutter.plugin.platform.PlatformViewsController.onTouch(PlatformViewsController.java:275)
  3. at io.flutter.plugin.platform.PlatformViewsController.onMethodCall(PlatformViewsController.java:127)
  4. at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:200)
  5. at io.flutter.view.FlutterNativeView.handlePlatformMessage(FlutterNativeView.java:163)
  6. at android.os.MessageQueue.nativePollOnce(Native Method)
  7. at android.os.MessageQueue.next(MessageQueue.java:329)
  8. at android.os.Looper.loop(Looper.java:142)
  9. at android.app.ActivityThread.main(ActivityThread.java:6375)
  10. at java.lang.reflect.Method.invoke(Native Method)
  11. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:912)
  12. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:802)
  1. this.methodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
  2. @Override
  3. public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
  4. if ("switchView".equals(methodCall.method)) {
  5. ((FrameLayout)mMapView.getParent()).addView(mTxtView, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
  6. ((FrameLayout)mMapView.getParent()).removeView(mMapView);
  7. }
  8. }

然后在PlatformView#getView()返回这个TextView即可。

当然,最后一种方法也不是太好,因为直接修改了副屏的布局。
最好的方式是,不要通过viewType来切换Native View了,就使用一个Native View,然后通过channel发送消息,让这个Native View增加或者删除自己的子View。

Native View

SingleViewPresentation源码

图一:

图二:

我们运行程序之后,打开“layout inspector”,惊奇的发现有两个MainActivity,分别对应上面的两张图

图一

增加了一个布局,这个布局的root是FrameLayout,看源码其实就是AccessibilityDelegatingFrameLayout;里面有两个子View,一个是FrameLayout,这个是专门用来方native view的;另一个是SingleViewPresentation#FakeWindowViewGroup,这是一个自定义ViewGroup。

当我们执行:

  1. ((FrameLayout)mTxtView.getParent()).setVisibility(View.GONE);

这个时候MyTextView就会从界面上消失了,也就是说我们在手机上看不到MyTextView了。但是,当点击屏幕的时候,PlatformView#getView()依然会调用。而且,我们通过Flutter加进来的FloatingActionButton依然还在界面上。

再次:

  1. mViewGroup = new FrameLayout(registrar.context());
  2. mViewGroup.setBackgroundColor(Color.RED);
  3. ((FrameLayout)mMapView.getParent()).addView(mViewGroup, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));

这个mViewGroup会覆盖之前写的TextView,当然,FloatingActionButton依然还在界面上。

图二

图二只有一个FlutterView,我们知道FlutterView是一个SurfaceView,而且这个才是显示在手机屏幕上的那个View,为什么?
因为我们的Activity是继承FlutterActivity的,而FlutterActivity的布局是FlutterView。

关系

Flutter 面试知识点集锦
那么图二和图一是什么关系呢?

PlatformView 可以嵌套原生 View 到 Flutter UI 中,这里面其实是使用了 Presentation + VirtualDisplay + Surface 等实现的,大致原理就是:

使用了类似副屏显示的技术,VirtualDisplay 类代表一个虚拟显示器,调用 DisplayManager 的 createVirtualDisplay() 方法,将虚拟显示器的内容渲染在一个 Surface 控件上,然后将 Surface 的 id 通知给 Dart,让 engine 绘制时,在内存中找到对应的 Surface 画面内存数据,然后绘制出来。em... 实时控件截图渲染显示技术。

我们可以通过一些adb 命令来侧面印证一下:

  1. adb dumpsys activity activities

结果:

  1. ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
  2. Display #0 (activities from top to bottom):
  3. Stack #1:
  4. mResumedActivity=ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  5. Running activities (most recent first):
  6. TaskRecord{4a3257f #463 A=fluttermapexample U=0 StackId=1 sz=1}
  7. Run #0: ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  8. mResumedActivity: ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  9. Display #43 (activities from top to bottom):
  10. Display #44 (activities from top to bottom):
  11. Display #92 (activities from top to bottom):
  12. mFocusedActivity: ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  13. mFocusedStack=ActivityStack{7ce9a9b stackId=1, 1 tasks} mLastFocusedStack=ActivityStack{7ce9a9b stackId=1, 1 tasks}
  14. mSleepTimeout=false
  15. mCurTaskIdForUser={0=463}
  16. mUserStackInFront={}
  17. mActivityContainers={0=ActivtyContainer{0}A, 1=ActivtyContainer{1}A}
  18. mLockTaskModeState=NONE mLockTaskPackages (userId:packages)=
  19. 0:[]
  20. mLockTaskModeTasks[]

截取了一部分,我们重点看这些:

  1. Display #0 (activities from top to bottom):
  2. mResumedActivity=ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  3. Running activities (most recent first):
  4. TaskRecord{4a3257f #463 A=fluttermapexample U=0 StackId=1 sz=1}
  5. Run #0: ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  6. Display #92 (activities from top to bottom):
  7. mFocusedActivity: ActivityRecord{1ab999c u0 fluttermapexample/.MainActivity t463}
  8. mFocusedStack=ActivityStack{7ce9a9b stackId=1, 1 tasks} mLastFocusedStack=ActivityStack{7ce9a9b stackId=1, 1 tasks}

我们继续,执行:

  1. adb shell dumpsys display | grep DisplayDeviceInfo

得到结果:

  1. DisplayDeviceInfo{"内置屏幕": uniqueId="local:0", 1080 x 1920, modeId 1, defaultModeId 1, supportedModes [{id=1, width=1080, height=1920, fps=60.000004}], colorTransformId 1, defaultColorTransformId 1, supportedColorTransforms [{id=1, colorTransform=0}], HdrCapabilities android.view.Display$HdrCapabilities@ceffe26, density 480, 397.565 x 399.737 dpi, appVsyncOff 0, presDeadline 17666666, touch INTERNAL, rotation 0, type BUILT_IN, state ON, FLAG_DEFAULT_DISPLAY, FLAG_ROTATES_WITH_CONTENT, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS}
  2. DisplayDeviceInfo{"flutter-vd": uniqueId="virtual:fluttermapexample,10971,flutter-vd,0", 600 x 600, modeId 93, defaultModeId 93, supportedModes [{id=93, width=600, height=600, fps=60.0}], colorTransformId 0, defaultColorTransformId 0, supportedColorTransforms [], HdrCapabilities null, density 480, 480.0 x 480.0 dpi, appVsyncOff 0, presDeadline 16666666, touch NONE, rotation 0, type VIRTUAL, state ON, owner fluttermapexample (uid 10971), FLAG_PRIVATE, FLAG_NEVER_BLANK, FLAG_OWN_CONTENT_ONLY}

可以看到有两个DisplayDeviceInfo,一个是"local:0",一个是"virtual:fluttermapexample"

总结

  • 程序启动之后,会有两个MainActivity的实例,一个包含FlutterView,一个包含PlatformView#getView()返回的那个TextView
  • 包含FlutterView的Activity是真正在屏幕显示的,这个称为主屏。
  • 另外一个是通过VirtualDisplay,代表一个虚拟显示器,可以称为副屏。
  • Native侧会把这个副屏的内容渲染到Surface上,这些数据对应一个textureId,Native侧会把这个textureId传递给Flutter,Flutter通过这个ID可以直接在GPU中找到相应的绘图数据并使用显示在主屏上
  • 原生层 Flutter 通过 Presentation 副屏显示的原理,利用 VirtualDisplay 的方式,让 Android 控件在内存中绘制到 Surface 层。 VirtualDisplay 绘制在 Surface 的 textureId ,之后会通知到 Dart 层,在 Dart 层利用 AndroidView 定义好的 Widget 并带上 textureId ,那么 Engine 在渲染时,就会在内存中将 textureId 对应的数据渲染到 AndroidView 上。

FlutterView

Flutter学习系列(5)— 页面初始化
Flutter学习系列(6)— FlutterView初始化

PlatformView其实也是使用的Texture

https://cloud.tencent.com/developer/article/1584477

拾遗

在Flutter中嵌入Native组件的正确姿势
其实Flutter中PlatFromView也是基于texture来实现的

壹 View最终的显示尺寸由谁决定

触摸事件如何传递

刚开始,我以为事件需要我们自己处理,写了demo,跑了之后发现,事件flutter已经帮我们处理好了。

Flutter按照自己的规则去处理事件,如果AndroidView赢得了事件,事件就会被封装成相应的Native端的事件并且通过方法通道传回Native,Native再根据自己的处理事件的规则去处理。

总之,当触摸native的view的时候,flutter会把触摸事件传递给native的view。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注