Flutter布局中嵌入Android原生组件 - 全景图组件封装

Flutter布局中嵌入Android原生组件 - 全景图组件封装

Flutter已经拥有大量的UI组件库,但是有一些特殊的视图它并没有,这时候就需要Native来实现这样的视图,然后在Flutter端调用。这里以封装一个全景图组件为例讲解在Flutter布局中怎样嵌入Android原生组件。

项目地址:flutter_panorama
全景图插件:GoogleVr (这里是老版本的实现方式)

Flutter布局中嵌入Android原生组件 - 全景图组件封装_第1张图片

在Android工程中编写并注册原生组件

添加原生组件的流程基本是这样的:

  1. 编写Native组件,它需要实现Flutter的PlatformView,用于提供原生的组件视图。
  2. 创建PlatformViewFactory用于生成PlatformView。
  3. 创建FlutterPlugin用于注册原生组件。注意:这里新老版本写法不太一样,我的Flutter版本是v1.12.13+hotfix.9。

创建Native组件

在build.gradle中引入GoogleVr的依赖。

dependencies {
    implementation 'com.google.vr:sdk-panowidget:1.180.0'
}

创建FlutterPanoramaView,实现PlatformView和MethodCallHandler。
在这里实现MethodCallHandler,而不是在FlutterPlugin中实现,是因为每一个视图独立对应一个通信方法。
注意:panoramaView = new VrPanoramaView(context); 这里的context必须是Activity的context

public class FlutterPanoramaView implements PlatformView, MethodChannel.MethodCallHandler {
	// Method通道
    private final MethodChannel methodChannel;
    // 原生全景图
    private final VrPanoramaView panoramaView;
	// 加载图片的异步任务
    private ImageLoaderTask imageLoaderTask;
    private VrPanoramaView.Options options = new VrPanoramaView.Options();;

    FlutterPanoramaView(final Context context,
                        BinaryMessenger messenger,
                        int id,
                        Map<String, Object> params) {
        // 创建视图,这里的context必须是Activity的context
        panoramaView = new VrPanoramaView(context);
        // 配置参数
        if (params.get("enableInfoButton") == null || !(boolean) params.get("enableInfoButton")) {
            panoramaView.setInfoButtonEnabled(false);
        }
        if (params.get("enableFullButton") == null || !(boolean) params.get("enableFullButton")) {
            panoramaView.setFullscreenButtonEnabled(false);
        }
        if (params.get("enableStereoModeButton") == null || !(boolean) params.get("enableStereoModeButton")) {
            panoramaView.setStereoModeButtonEnabled(false);
        }
        if (params.get("imageType") != null) {
            options.inputType = (int) params.get("imageType") ;
        }
        // 加载图像
        imageLoaderTask = new ImageLoaderTask(context);
        imageLoaderTask.execute((String)params.get("uri"), (String)params.get("asset"), (String)params.get("packageName"));

        // 为每一个组件实例注册MethodChannel,通过ID区分
        methodChannel = new MethodChannel(messenger, "plugins.vincent/panorama_" + id);
        methodChannel.setMethodCallHandler(this);
    }

    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        // TODO 处理Flutter端传过来的方法
    }

    @Override
    public View getView() {
    	// 在这里返回原生视图
        return panoramaView;
    }

    @Override
    public void dispose() {
        imageLoaderTask = null;
    }

    private boolean isHTTP(Uri uri) {
        if (uri == null || uri.getScheme() == null) {
            return false;
        }
        String scheme = uri.getScheme();
        return scheme.equals("http") || scheme.equals("https");
    }

    private class ImageLoaderTask extends AsyncTask<String, String, Bitmap> {
        final Context context;

        public ImageLoaderTask(Context context) {
            this.context = context;
        }

        @Override
        protected Bitmap doInBackground(String... strings) {
            if (strings == null || strings.length < 1) {
                return null;
            }
            String path = strings[0];
            String asset = strings[1];
            String packageName = strings[2];
            Bitmap image = null;
            if (!TextUtils.isEmpty(asset)) {
            	// Flutter的Asset资源
                String assetKey;
                if (!TextUtils.isEmpty(packageName)) {
                    assetKey = FlutterMain.getLookupKeyForAsset(asset, packageName);
                } else {
                    assetKey = FlutterMain.getLookupKeyForAsset(asset);
                }
                try {
                    AssetManager assetManager = context.getAssets();
                    AssetFileDescriptor fileDescriptor = assetManager.openFd(assetKey);
                    image = BitmapFactory.decodeStream(fileDescriptor.createInputStream());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                Uri uri = Uri.parse(path);
                if (isHTTP(uri)) {
                    // 网络资源
                    try {
                        URL fileUrl = new URL(path);
                        InputStream is = fileUrl.openConnection().getInputStream();
                        image = BitmapFactory.decodeStream(is);
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    // 存储卡资源
                    try {
                        File file = new File(uri.getPath());
                        if (!file.exists()) {
                            throw new FileNotFoundException();
                        }
                        image = BitmapFactory.decodeFile(uri.getPath());
                        panoramaView.loadImageFromBitmap(image, null);

                    } catch (IOException | InvalidParameterException e) {
                        e.printStackTrace();
                    }
                }
            }
            return image;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            methodChannel.invokeMethod("onImageLoaded", bitmap == null ? 0 : 1);
            // 处理回调
            if (bitmap == null) {
                Toast.makeText(context, "全景图片加载失败",Toast.LENGTH_LONG).show();
                return;
            }
            panoramaView.loadImageFromBitmap(bitmap, options);
        }
    }
}

创建PlatformViewFactory

接下来创建FlutterPanoramaFactory,它继承自PlatformViewFactory:

public class FlutterPanoramaFactory extends PlatformViewFactory {
    private final BinaryMessenger messenger;
    private final Context context;

    FlutterPanoramaFactory(Context context, BinaryMessenger messenger) {
        super(StandardMessageCodec.INSTANCE);
        this.context = context;
        this.messenger = messenger;
    }

    @Override
    public PlatformView create(Context context1, int viewId, Object args) {
        Map<String, Object> params = (Map<String, Object>) args;
        // args是由Flutter传过来的自定义参数
        return new FlutterPanoramaView(this.context, messenger, viewId, params);
    }
}

在create方法中能够获取到三个参数。context是Android上下文(这里并不是一个Activity的context),viewId是生成组件的ID,args是Flutter端传过来的自定义参数。

注册全景图插件

编写FlutterPanoramaPlugin,它实现了FlutterPlugin。为了获取到Flutter中的Activity的context,同时也实现了ActivityAware。

public class FlutterPanoramaPlugin implements FlutterPlugin, ActivityAware {
  private FlutterPluginBinding flutterPluginBinding;

  public static void registerWith(Registrar registrar) {
    registrar
            .platformViewRegistry()
            .registerViewFactory(
                    "plugins.vincent/panorama",
                    new FlutterPanoramaFactory(registrar.activeContext(), registrar.messenger()));
  }

  @Override
  public void onAttachedToEngine(FlutterPluginBinding binding) {
    this.flutterPluginBinding = binding;
  }

  @Override
  public void onDetachedFromEngine(FlutterPluginBinding binding) {
    this.flutterPluginBinding = null;
  }

  @Override
  public void onAttachedToActivity(ActivityPluginBinding binding) {
    BinaryMessenger messenger = this.flutterPluginBinding.getBinaryMessenger();
    this.flutterPluginBinding
            .getPlatformViewRegistry()
            .registerViewFactory(
                    "plugins.vincent/panorama", new FlutterPanoramaFactory(binding.getActivity(), messenger));
  }

  @Override
  public void onDetachedFromActivityForConfigChanges() {
//    onDetachedFromActivity();
  }

  @Override
  public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
//    onAttachedToActivity(binding);
  }

  @Override
  public void onDetachedFromActivity() {
  }
}

上面代码中使用了plugins.vincent/panorama这样一个字符串,这是组件的注册名称,在Flutter调用时需要用到,你可以使用任意格式的字符串,但是两端必须一致。

在Flutter工程中调用原生View

原生View的调用非常简单,在使用Android平台的view只需要创建AndroidView组件并告诉它组件的注册注册名称即可,可通过creationParams传递参数

return AndroidView(
   viewType: "plugins.vincent/panorama",
   creationParams: {
      "myContent": "通过参数传入的文本内容",
    },
   creationParamsCodec: const StandardMessageCodec(),
   onPlatformViewCreated: (int id) {
   	 // 注册MethodChannel
     MethodChannelPanoramaPlatform(id, callbacksHandler);
   },
 );

creationParams传入了一个map参数,并由原生组件接收,creationParamsCodec传入的是一个编码对象这是固定写法。

通过MethodChannel与原生组件通讯

  1. 让原始组件必须要MethodCallHandler接口,
  2. Flutter中创建MethodChannelPanoramaPlatform ,
  3. 在AndroidView创建时的onPlatformViewCreated方法中,去创建MethodChannelPanoramaPlatform 。

通过callbacksHandler回调函数,触发方法。

class MethodChannelPanoramaPlatform {
  final MethodChannel _channel;
  final PanoramaPlatformCallbacksHandler _callbacksHandler;

  MethodChannelPanoramaPlatform(int id, this._callbacksHandler) : assert(_callbacksHandler != null), _channel = MethodChannel('plugins.vincent/panorama_$id') {
    _channel.setMethodCallHandler(_onMethodCall);
  }

  Future _onMethodCall(MethodCall call) async {
    switch(call.method) {
      case "onImageLoaded":
        final int state = call.arguments;
        _callbacksHandler.onImageLoaded(state);
        return true;
    }
    return null;
  }
}

封装FlutterPanorama组件,实现跨平台

通过defaultTargetPlatform区分当前平台,然后调用不同的组件。

class FlutterPanorama extends StatelessWidget {
  final String dataSource;
  final DataSourceType dataSourceType;
  final String package;
  final ImageType imageType;
  final bool enableInfoButton;
  final bool enableFullButton;
  final bool enableStereoModeButton;
  final ImageLoadedCallback onImageLoaded;
  /// 自定义回调函数
  _PlatformCallbacksHandler _platformCallbacksHandler;
  /// 针对Flutter中Asset资源的构造器
  FlutterPanorama.assets(this.dataSource, {
    this.package,
    this.imageType: ImageType.MEDIA_MONOSCOPIC,
    this.enableInfoButton,
    this.enableFullButton,
    this.enableStereoModeButton,
    this.onImageLoaded,
  }) : dataSourceType = DataSourceType.asset, super();

  /// 针对网络资源的构造器
  FlutterPanorama.network(this.dataSource, {
    this.imageType: ImageType.MEDIA_MONOSCOPIC,
    this.enableInfoButton,
    this.enableFullButton,
    this.enableStereoModeButton,
    this.onImageLoaded,
  }) : dataSourceType = DataSourceType.network, package = null, super();
  
  /// 针对存储卡资源的构造器
  FlutterPanorama.file(this.dataSource, {
    this.imageType: ImageType.MEDIA_MONOSCOPIC,
    this.enableInfoButton,
    this.enableFullButton,
    this.enableStereoModeButton,
    this.onImageLoaded,
  }) :  dataSourceType = DataSourceType.file, package = null, super();

  static FlutterPanoramaPlatform _platform;

  static set platform(FlutterPanoramaPlatform platform) {
    _platform = platform;
  }
	
  /// 平台区分,返回不同平台的视图
  static FlutterPanoramaPlatform get platform {
    if (_platform == null) {
      switch (defaultTargetPlatform) {
        case TargetPlatform.android:
          _platform = AndroidPanoramaView();
          break;
        case TargetPlatform.iOS:
          _platform = IosPanoramaView();
          break;
        default:
          throw UnsupportedError(
              "Trying to use the default panorama implementation for $defaultTargetPlatform but there isn't a default one");
      }
    }
    return _platform;
  }

  @override
  Widget build(BuildContext context) {
    _platformCallbacksHandler = _PlatformCallbacksHandler(this);

    return FlutterPanorama.platform.build(
        context,
        _toCreationParams(),
        _platformCallbacksHandler
    );
  }

  /// 转换参数
  Map _toCreationParams() {
    DataSource dataSourceDescription;
    switch (dataSourceType) {
      case DataSourceType.asset:
        dataSourceDescription = DataSource(
          sourceType: DataSourceType.asset,
          asset: dataSource,
          package: package,
        );
        break;
      case DataSourceType.network:
        dataSourceDescription = DataSource(
            sourceType: DataSourceType.network,
            uri: dataSource
        );
        break;
      case DataSourceType.file:
        dataSourceDescription = DataSource(
          sourceType: DataSourceType.file,
          uri: dataSource,
        );
        break;
    }
    Map creationParams = dataSourceDescription.toJson();
    creationParams["imageType"] = this.imageType.index;
    creationParams["enableInfoButton"] = this.enableInfoButton;
    creationParams["enableFullButton"] = this.enableFullButton;
    creationParams["enableStereoModeButton"] = this.enableStereoModeButton;
    return creationParams;
  }
}

class _PlatformCallbacksHandler implements PanoramaPlatformCallbacksHandler {
  FlutterPanorama _widget;

  _PlatformCallbacksHandler(this._widget);

  @override
  void onImageLoaded(int state) {
    _widget.onImageLoaded(state);
  }
}

使用方法

先在pubspec.yaml中引用

  flutter_panorama:
    git:
      url: https://github.com/lytian/flutter_panorama.git

然后就可以使用了

@override
Widgetbuild(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Plugin example app'),
      ),
      body: Center(
//          child: FlutterPanorama.assets("images/xishui_pano.jpg"),
        child: FlutterPanorama.network('https://storage.googleapis.com/vrview/examples/coral.jpg',
          imageType: ImageType.MEDIA_STEREO_TOP_BOTTOM,
          onImageLoaded: (state) {
            print("------------------------------- ${state == 1 ? '图片加载完成' : '图片加载失败'}");
          },
        ),
      )
    ),
  );

你可能感兴趣的:(Flutter)