Flutter已经拥有大量的UI组件库,但是有一些特殊的视图它并没有,这时候就需要Native来实现这样的视图,然后在Flutter端调用。这里以封装一个全景图组件为例讲解在Flutter布局中怎样嵌入Android原生组件。
项目地址:flutter_panorama
全景图插件:GoogleVr (这里是老版本的实现方式)
添加原生组件的流程基本是这样的:
在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);
}
}
}
接下来创建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调用时需要用到,你可以使用任意格式的字符串,但是两端必须一致。
原生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传入的是一个编码对象这是固定写法。
通过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;
}
}
通过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 ? '图片加载完成' : '图片加载失败'}");
},
),
)
),
);