欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-05-29 20:02:29
...

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

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

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

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

在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<bool> _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<String, dynamic> _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<String, dynamic> 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