使用PlatformView将 Android 控件view制作成Flutter插件

目录
  • 引言
    • 1. FlutterPlugin 创建
    • 2. 创建 Android 控件
    • 3. 注册 Android 控件
    • 4. 封装 Android 层通信交互 ‘CustomViewController’
      • 代码说明
    • 5. 在 flutter 中如何使用已注册的 Android 控件(view)
      • 代码说明
      • 如何使用这个View
    • 6. 附上 example 完整代码

引言

小编最近在项目中实现相机识别人脸的功能,将 Android 封装的控件 view 进行中转,制作成 FlutterPlugin 提供给 flutter 项目使用。为了方便后期的知识整理,下面,用简单的 demo 记录 Android 控件如何封装成 flutter 插件以及如何实现交互的过程。

1. FlutterPlugin 创建

第一步,创建一个 FlutterPlugin 项目。

2. 创建 Android 控件

抛砖引玉,创建一个简单的自定义控件,控件内包含三个元素

layout_custom_view.xml (布局文件)

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/androidViewButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:padding="20dp"
        android:text="发送数据给 flutter" />
    <!--用于展示从flutter层接收的数据-->
    <TextView
        android:id="@+id/androidViewText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/androidViewButton"
        android:layout_centerHorizontal="true"
        android:padding="20dp"
        android:text="" />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:paddingBottom="10dp"
        android:text="Android-View"
        android:textSize="20dp"
        android:textStyle="bold" />
</RelativeLayout>

CustomView.kt

/**
 *  android 渲染的自定义view 提供 flutter 使用
 */
class CustomView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
    private var textView: TextView? = null
    private var onKeyEventCallback: OnKeyEventCallback? = null
    init {
        val rootView = LayoutInflater.from(context).inflate(R.layout.layout_custom_view, this, true)
        initView(rootView)
    }
    private fun initView(rootView: View) {
        textView = rootView.findViewById(R.id.androidViewText)
        rootView.findViewById<Button>(R.id.androidViewButton).setOnClickListener {
            //模拟生成一个随机数传递到 flutter
            val randomNum = (0..10).random()
            onKeyEventCallback?.onKeyEventCallback(randomNum.toString())
        }
    }
    fun setOnKeyEventCallback(callback: OnKeyEventCallback?) {
        onKeyEventCallback = callback
    }
    @SuppressLint("SetTextI18n")
    fun getMessageFromFlutter(message: String) {
        textView?.text = "自来flutter的数据:$message"
    }
}
interface OnKeyEventCallback {
    fun onKeyEventCallback(message: String)
}

自定义控件进行UI绘制,显示文本 Android-View。为了模拟双向交互流程,控件内放置了一个按钮用于生成随机数模拟 android 层向 flutter 层的数据传输;放置了一块文本区域用于展示从 flutter 层接收到的数据。

3. 注册 Android 控件

在 plugin 的 onAttachToEngine 方法中对自定义控件进行注册

class CustomAndroidViewPlugin: FlutterPlugin, ActivityAware {
  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    /// 将 Android 控件进行注册,提供 flutter 层使用
    flutterPluginBinding.platformViewRegistry
      .registerViewFactory(
        VIEW_TYPE_ID,
        CustomViewFactory(flutterPluginBinding.binaryMessenger)
      )
  }
 ...省略部分非关键代码
  companion object {
    // 通过唯一值id进行控件注册
    private const val VIEW_TYPE_ID = "com.rex.custom.android/customView"
  }
}

实际注册的对象 CustomViewFactory 代码如下:

class CustomViewFactory(
    private val messenger: BinaryMessenger
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(
        context: Context?,
        viewId: Int,
        args: Any?
    ): PlatformView {
        @Suppress("UNCHECKED_CAST")
        val params = args as HashMap<String, Any>
        return CustomViewController(
            context = requireNotNull(context),
            id = viewId,
            messenger = messenger,
            params = params
        )
    }
}

4. 封装 Android 层通信交互 ‘CustomViewController’

/**
 * 提供 AndroidView 与 flutter 间的交互能力
 */
class CustomViewController(
    private val context: Context,
    messenger: BinaryMessenger,
    val id: Int,
    val params: HashMap<String, Any>
) : PlatformView {
    private var customView: CustomView? = null
    private val channel: MethodChannel = MethodChannel(
        messenger, "com.rex.custom.android/customView$id"
    )
    init {
        // 如果需要在自定义view交互中申请监听权限可以加上下面这句话
        // CustomShared.binding?.addRequestPermissionsResultListener(this)
        channel.setMethodCallHandler(this)
        params.entries.forEach {
            Log.i("rex", "CustomView初始化接收入参:${it.key} - ${it.value}")
        }
    }
    override fun getView(): View = initCustomView()
    private fun initCustomView(): View {
        if (customView == null) {
            customView = CustomView(context, null)
            customView!!.setOnKeyEventCallback(object : OnKeyEventCallback {
                override fun onKeyEventCallback(message: String) {
                    // 将 Android 层的数据传递到 flutter 层
                    channel.invokeMethod(
                        "getMessageFromAndroidView",
                        "native - $message"
                    )
                }
            })
        }
        return customView!!
    }
    override fun dispose() {
        // flutterView dispose 生命周期 在此响应
        Log.i("rex", "flutterView on Dispose")
    }
    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "getMessageFromFlutterView" -> {
                customView?.getMessageFromFlutter(call.arguments.toString())
                result.success(true)
            }
            else -> result.notImplemented()
        }
    }
}

代码说明

  • CustomViewController 需实现 PlatformView 实现 getView 方法返回 自定义UI控件
  • 通过 MethodChannel 实现 Android - Flutter 间的交互通信能力。
  • Android代码中,自定义控件如何接收 flutter 端的方法调用? 在 onMethodCall 方法中接收来自 flutter 端的方法调用,通过方法名区分,调用指定功能。如:示例中的getMessageFromFlutterView 接收 flutter 端传递的数据 call.arguments ,然后在自定义 Android-UI 控件中展示出来 customView.getMessageFromFlutter
  • Android代码中,自定义控件如何调用 flutter 端方法? 使用方法 channel.invokeMethod(param1, param2) ,param1 为约定的方法名称,如示例中的 getMessageFromAndroidView, 生成一个随机数传递给 flutter 端;param2 为 想要传递给 flutter 端的数据,数据类型可以是任意类型,示例中使用的是字符串类型。

5. 在 flutter 中如何使用已注册的 Android 控件(view)

创建 custom_android_view.dart 用于包裹 Android 控件

关键点:通过原生层中注册的 id 路径获取 AndroidView 要求:AndroidView 中 viewType 参数就是原生层中注册的自定义控件的映射路径,如示例中 CustomAndroidViewPlugin 内的 viewTypeId

AndroidView(
      viewType: 'com.rex.custom.android/customView', //要与注册的路径保持一致
      onPlatformViewCreated: _onPlatformViewCreated,
      creationParams: const <String, dynamic>{'initParams': 'hello world'},
      creationParamsCodec: const StandardMessageCodec(),
    )

将 AndroidView 进行封装,控件名称为 CustomAndroidView ,完整代码如下:

typedef OnViewCreated = Function(CustomViewController);
///自定义AndroidView
class CustomAndroidView extends StatefulWidget {
  final OnViewCreated onViewCreated;
  const CustomAndroidView(this.onViewCreated, {Key? key}) : super(key: key);
  @override
  State<CustomAndroidView> createState() => _CustomAndroidViewState();
}
class _CustomAndroidViewState extends State<CustomAndroidView> {
  late MethodChannel _channel;
  @override
  Widget build(BuildContext context) {
    return _getPlatformFaceView();
  }
  Widget _getPlatformFaceView() {
    return AndroidView(
      viewType: 'com.rex.custom.android/customView',
      onPlatformViewCreated: _onPlatformViewCreated,
      creationParams: const <String, dynamic>{'initParams': 'hello world'},
      creationParamsCodec: const StandardMessageCodec(),
    );
  }
  void _onPlatformViewCreated(int id) {
    _channel = MethodChannel('com.rex.custom.android/customView$id');
    final controller = CustomViewController._(
      _channel,
    );
    widget.onViewCreated(controller);
  }
}
class CustomViewController {
  final MethodChannel _channel;
  final StreamController<String> _controller = StreamController<String>();
  CustomViewController._(
    this._channel,
  ) {
    _channel.setMethodCallHandler(
      (call) async {
        switch (call.method) {
          case 'getMessageFromAndroidView':
            // 从native端获取数据
            final result = call.arguments as String;
            _controller.sink.add(result);
            break;
        }
      },
    );
  }
  Stream<String> get customDataStream => _controller.stream;
  // 发送数据给native
  Future<void> sendMessageToAndroidView(String message) async {
    await _channel.invokeMethod(
      'getMessageFromFlutterView',
      message,
    );
  }
}

代码说明

  • AndroidView 在加载完成时会回调我们的 _onPlatformViewCreated 方法,小编在 _onPlatformViewCreated 方法内将 methodChannel 初始化,用于监听 Android 端的方法调用,以及后续用其调用 Android控件内封装的方法。
  • 小编给 CustomAndroidView 封装了一个 controller 控制类,在 CustomAndroidView 的构造方法中回传给调用者,调用者可通过 controller 进行监听 Android 端传送过来的数据,以及通过 controller 调用控件提供的能力方法。

如何使用这个View

展示 CustomAndroidView :

Widget _buildAndroidView() {
    return CustomAndroidView(_onCustomAndroidViewCreated)
}

接收来自 Android 层的传输数据

void _onCustomAndroidViewCreated(CustomViewController controller) {
    _controller = controller;
    _controller?.customDataStream.listen((data) {
      //接收到来自Android端的数据
      setState(() {
        receivedData = '来自Android的数据:$data';
      });
    });
  }

通过控件发送数据给 Android 层

final randomNum = Random().nextInt(10);
_controller?.sendMessageToAndroidView('flutter - $randomNum ');
// _controller 在CustomAndroidView 的构造方法回调中获取,如标签2

6. 附上 example 完整代码

example/main.dart

void main() {
  runApp(const MaterialApp(home: MyHome()));
}
class MyHome extends StatelessWidget {
  const MyHome({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: CustomExample(),
    );
  }
}
class CustomExample extends StatefulWidget {
  const CustomExample({Key? key}) : super(key: key);
  @override
  State<CustomExample> createState() => _CustomExampleState();
}
class _CustomExampleState extends State<CustomExample> {
  String receivedData = '';
  CustomViewController? _controller;
  void _onCustomAndroidViewCreated(CustomViewController controller) {
    _controller = controller;
    _controller?.customDataStream.listen((data) {
      //接收到来自Android端的数据
      setState(() {
        receivedData = '来自Android的数据:$data';
      });
    });
  }
  Widget _buildAndroidView() {
    return Expanded(
      child: Container(
        color: Colors.blueAccent.withAlpha(60),
        child: CustomAndroidView(_onCustomAndroidViewCreated),
      ),
      flex: 1,
    );
  }
  Widget _buildFlutterView() {
    return Expanded(
      child: Stack(
        alignment: AlignmentDirectional.bottomCenter,
        children: [
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.max,
            children: [
              TextButton(
                onPressed: () {
                  final randomNum = Random().nextInt(10);
                  _controller
                      ?.sendMessageToAndroidView('flutter - $randomNum ');
                },
                child: const Text('发送数据给Android'),
              ),
              const SizedBox(height: 10),
              Text(receivedData),
            ],
          ),
          const Padding(
            padding: EdgeInsets.only(bottom: 15),
            child: Text(
              'Flutter - View',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
      flex: 1,
    );
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildAndroidView(),
        _buildFlutterView(),
      ],
    );
  }
}

如上,demo 将一个页面均分为上下两块,上半部分使用 Android 控件,下半部分使用 Flutter 控件,两组控件间进行通信交互。

demo 已上传:github.com/liyufengrex…

以上就是使用PlatformView将 Android 控件view制成Flutter插件的详细内容,更多关于Android view制成Flutter的资料请关注我们其它相关文章!

时间: 2022-11-13

Flutter开发Widgets 之 PageView使用示例

目录 构造方法以及参数: 基本用法 无限滚动 实现指示器 切换动画 总结: 构造方法以及参数: PageView可用于Widget的整屏滑动切换,如当代常用的短视频APP中的上下滑动切换的功能,也可用于横向页面的切换,如APP第一次安装时的引导页面,也可用于开发轮播图功能. PageView({ Key? key, this.scrollDirection = Axis.horizontal, // 设置滚动方向 垂直 / 水平 this.reverse = false, // 反向滚动 Pag

Android开发之Flutter与webview通信桥梁实现

前言 最近业务有需求需要在flutter内使用webview进行内嵌H5进行展示,此时需要涉及到H5与flutter之间额通信问题.比如发送消息或者H5调用Flutter的相机等等 webview选型 这里我们使用官方维护的插件webview_flutter 如何通信? webview在初始化的时候需要向容器内注册一个全局方法供H5进行调用 WebView( initialUrl: 'https://flutter.dev', javascriptMode: JavascriptMode.unr

Flutter之 ListView组件使用示例详解

目录 ListView的默认构造函数定义 默认构造函数 ListView.builder ListView.separated 固定高度列表 ListView 原理 实例:无限加载列表 添加固定列表头 总结 ListView的默认构造函数定义 ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建).我们看看ListView的默认构造函数定义: ListView({ ... //可滚动widget公共参数 Axis scrollD

Flutter之TabBarView组件项目实战示例

目录 TabBarView TabBar TabBarView+项目实战 1 构建导航头部搜索框 2 构建导航头部TabBar 3 构建导航底部TabBarView容器 4 构建导航底部结构填充 5 构建导航底部结构轮播图 6 构建导航底部结构信息流 总结 TabBarView TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用. TabBarView 封装了 PageView,它的构造方法: TabBarView({ Key? key,

Flutter之PageView页面缓存与KeepAlive

目录 正文 构造函数 页面缓存 KeepAlive KeepAliveWrapper 总结 正文 如果要实现页面切换和 Tab 布局,我们可以使用 PageView 组件.需要注意,PageView 是一个非常重要的组件,因为在移动端开发中很常用,比如大多数 App 都包含 Tab 换页效果.图片轮动以及抖音上下滑页切换视频功能等等,这些都可以通过 PageView 轻松实现. 构造函数 PageView({ Key? key, this.scrollDirection = Axis.horiz

vue缓存的keepalive页面刷新数据的方法

用到这个的业务场景是这样的: a页面点击新建列表按钮进入到新建的页面b,填写b页面并点击b页面确认添加按钮,把这些数据带到a页面,填充到列表(数组),可以添加多条, 点击这条的时候进入到编辑页面,确认修改之后,回退到a页面,a页面需要更新这条数据 实现这个功能的时候,由于是路由页面之间的跳转,首先想到的方案有几个:1. 用sessionStorage本地存储:2. 用路由参数带过去:3. 用兄弟组件传值 由于是添加完之后如果按回退是需要退出整个页面,如果用路由跳转,会出现回退到编辑页面了,所以这

vuex + keep-alive实现tab标签页面缓存功能

在开发很多管理系统过程之中,常遇到这种需求,需要对打开路由页面进行缓存,然后在系统页眉提供方便查阅的tab标签进行切换以及对已经缓存页面进行数据刷新和清除数据操作.具体演示如下图所示: 在上面演示中实现了类似 window tab 标签页效果,会对当前数据进行缓存.在浏览器中实现对路由页面的缓存可以减少接口请求,也方便了用户来回切换想搜索的数据列表. 原理 Vue 提供的 keep-alive API实现对路由组件的缓存. include 属性可以绑定一个数组,里面是需要路由组件的 name 值

详解基于vue的移动web app页面缓存解决方案

现在移动web app越来越热门了,许多公司开始尝试使用angular.react.vue等MVVM框架来开发单页架构的web app.但在开发web app时,如果希望页面的导航体验也接近原生应用,那一般都会遇到这两个问题: 识别前进后退行为 后退时恢复之前的页面 笔者开发了一个基于vue与vue-router的导航库vue-navigation,来帮助开发者来解决这些问题,下面是问题的解决思路. 识别前进后退 先说第一个问题.和原生app不一样,浏览器中主要有这几个限制: 没有提供前进后退的

详解Nginx 反向代理、负载均衡、页面缓存、URL重写及读写分离详解

注,操作系统为 CentOS 6.4 x86_64 , Nginx 是版本是最新版的1.4.2,所以实验用到的软件请点击这里下载: CentOS 6.4下载地址:http://www.jb51.net/softs/78243.html Nginx下载地址:http://www.jb51.net/softs/35633.html 一.前言 在前面的几篇博文中我们主要讲解了Nginx作为Web服务器知识点,主要的知识点有nginx的理论详解.nginx作为web服务器的操作讲解.nginx作为LNM

详解vue服务端渲染浏览器端缓存(keep-alive)

在使用服务器端渲染时,除了服务端的接口缓存.页面缓存.组建缓存等,浏览器端也避免不了要使用缓存,减少页面的重绘. 这时候我们就会想到vue的keep-alive,接下来我们说一下keep-alive的使用 假如现在我们有两个页面,home.vue 和 about.vue home.vue <template> <div> home </div> </template> <script> export default { name: Home, c

Vue项目全局配置页面缓存之按需读取缓存的实现详解

写在前面 一个web app的实际使用场景中,有一些情景的交互要求,是记录用户的浏览状态的.最常见的就是在列表页进入详情页之后,再返回到列表页,用户希望返回到进入详情页之前的状态继续操作.但是有些使用场景,用户又是希望能够获取最新的数据,例如同级列表页之间切换的时候. 如此,针对上述两种使用场景,需要实现按需读取页面缓存.由于SPA应用的路由逻辑也是在前端实现的,因此可以在前端对路由的逻辑进行设置以实现所需效果. 使用技术 Vue.js作为主要框架 Vue-router作为前端路由管理器 Vue

Vue2.0 实现页面缓存和不缓存的方式

1.在app中设置需要缓存的div <keep-alive>//缓存的页面 <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view>//不缓存的页面 2.在路由router.js中设置.vue页面是

vue页面跳转实现页面缓存操作

业务需求 打野在A页面野区进行一波骚操作打了一只蓝爸爸,然后点击导航栏跑到B页面的野区秀操作打了一只红爸爸,然后他又回到A野区,希望A野区还是只有一只蓝爸爸被打的状态,其他野没被偷 第一步 在路由里面设置需要缓存的页面 第二步 使用keep-alive属性包裹需要缓存的页面使用v-if判断,为true的是需要缓存的,false是不需要缓存的 第三步 在需要缓存的页面设置导航钩子,在A野区离开时将值设置为false保证离开当前野区不被偷 第四步 在其他野区浪一波离开时设置为true,保证回到A野区

详解vue之页面缓存问题(基于2.0)

比如有一个列表页面,然后列表每项都有一个详情,之前用vue1.x的时候,页面缓存基本没有什么问题. 在vue2.0中出现了列表页面是每次都重新加载数据,但是详情页面却只在第一次加载的时候调用数据,如果返回到列表再进入详情那么页面是不会重新渲染页面,可能是新手吧,这个问题困扰了我很久,一直没有办法解决-- 根据vue-router的官方文档所说, watch: { // 如果路由有变化,会再次执行该方法 '$route': 'fetchData' } 按照这样写了,但是页面还是没有渲染 . . .

JSP页面缓存cache技术--浏览器缓存介绍及实现方法

一.概述 缓存的思想可以应用在软件分层的各个层面.它是一种内部机制,对外界而言,是不可感知的. 数据库本身有缓存,持久层也可以缓存.(比如:hibernate,还分1级和2级缓存) 业务层也可以有缓存(但一般来说,这是一个过程域,不会设缓存). 表现层/数据服务层(传统web的表现层)也可以设置缓存(jsp cache 就是这一层,实现在app server上的缓存机制) 另外Browser也有缓存(如IE)这个大家也都知道(实现在 web server 上的缓存机制).越上层的缓存效果越好,越