Flutter布局中嵌入Android原生组件 - 全景图组件封装
Flutter布局中嵌入Android原生组件 - 全景图组件封装
Flutter已经拥有大量的UI组件库,但是有一些特殊的视图它并没有,这时候就需要Native来实现这样的视图,然后在Flutter端调用。这里以封装一个全景图组件为例讲解在Flutter布局中怎样嵌入Android原生组件。
项目地址:flutter_panorama
全景图插件:GoogleVr (这里是老版本的实现方式)
在Android工程中编写并注册原生组件
添加原生组件的流程基本是这样的:
- 编写Native组件,它需要实现Flutter的PlatformView,用于提供原生的组件视图。
- 创建PlatformViewFactory用于生成PlatformView。
- 创建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与原生组件通讯
- 让原始组件必须要MethodCallHandler接口,
- Flutter中创建MethodChannelPanoramaPlatform ,
- 在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 ? '图片加载完成' : '图片加载失败'}"); }, ), ) ), );
更多相关文章
- Android小项目之--电话与短信服务(附源码)
- Android中Intent的显示和隐式使用
- Android组件及UI框架大全
- 一、Android体系及系统架构
- cocos2d for android 项目的部署
- android 上下文菜单Context Menu--折腾一天的东东
- Android快捷方式解密
- android的ImageView中XML属性src和background的区别
- android中轮播图的实现