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

然后就可以使用了

@overrideWidgetbuild(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 ? '图片加载完成' : '图片加载失败'}");          },        ),      )    ),  );

更多相关文章

  1. Android组件及UI框架大全
  2. Android四大组件之Service---本地服务、远程服务和IntentService
  3. Android架构组件—ViewModel
  4. Android app widget 支持的Layout和widget组件
  5. android 开发使用 kotlin 进行点击事件监听和界面跳转,直接传也方
  6. Android Jetpack组件学习 Room

随机推荐

  1. Android API 中文(77)——AdapterView.OnIt
  2. 让你的Android彻底裸奔吧!! -- 瘫痪Andro
  3. Mac OSX Android 开发环境 模拟器报错
  4. Android应用程序常见编译问题解决
  5. Android 单元测试 Robolectric
  6. Android瀑布流 & 仿Win8的metro的UI界面
  7. Android官方技术文档翻译—— Eclilpse项
  8. 新建android工程 没有R.java文件
  9. 安卓选择器 selector 的笔记
  10. 【转】 Android上调用google map api v2