混合开发中,flutter使用三方库依赖iOS的SDK和原生使用的SDK重叠
Symbols count in article: 974 Reading time ≈ 1 mins.
混合开发中,flutter使用三方库依赖iOS的SDK和原生使用的SDK重叠
影响范围
主要影响和接入方式有关
一、源码接入
判断flutter使用三方库依赖iOS的SDK和原生使用的SDK是否相同:
- 相同: Pod会只安装一次。
- 不相同: 在执行
pod install
过程中会报错,提示依赖冲突
二、通过xcode拖入framework产物接入
未测试,应该和源码接入类似
三、通过CocoaPods拖入framework产物接入
在flutter module下通过以下指令生成flutter相关产物:flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
这个时候flutter使用的三方库中,含有原生SDK的同样会生成一份framework。
默认集成中,flutter和原生共用的三方SDK会有两份包含在包里,会导致api冲突,包体积增加等问题。
解决方案
**注释掉
s.dependency xxx
**: flutter私有化冲突的插件,在插件的iOS目录下的xxx.podspec
文件中,注释掉# s.dependency xxx
新建头文件:新建
xxx_plugin-Swift.h
,里面只写SDK的引入修改引入方式:
1
2
3
4
5
6
7
8#if __has_include(<xxx_plugin/xxx_plugin-Swift.h>)
#import <xxx_plugin/xxx_plugin-Swift.h>
#else
// Support project import fallback if the generated compatibility header
// is not copied when this plugin is created as a library.
// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
#import "xxx_plugin-Swift.h"
#endif再次运行打包指令
flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
自定义ProxyWidget组件
Symbols count in article: 1.4k Reading time ≈ 1 mins.
自定义ProxyWidget组件
业务场景
滑动选择,根据手势判断选中的元素
需求分析
- 监听手势滑动
- 根据手势位置判断元素是否被选中
- 通知子元素做选中UI展示
监听手势滑动
通过GestureDetector
包裹识别区域,默认使用:
- onHorizontalDragStart
- onHorizontalDragUpdate
- onHorizontalDragEnd
三个方法,判断此次滑动区域起始点和结束点,以及滑动过程中选中部分。
根据手势位置判断元素是否被选中
自定义ProxyWidget
- 标记当前widget标识,当前用下标做标识,方便状态同步。
- 实现
createElement
方法,创建一个ProxyElement
子类; - ProxyElement根据
unmount
和mount
生命周期,上报父容器元素自身挂载状态。 - ProxyElement对象提供判断是否在父容器手势范围内,其中用到了renderObject,renderObject是Element的属性(Element>ComponentElement>ProxyElement)
1
2
3
4
5bool containsOffset(RenderObject? ancestor, Offset offset) {
final box = renderObject as RenderBox;
final rect = box.localToGlobal(Offset.zero, ancestor: ancestor) & box.size;
return rect.contains(offset);
}滑动过程中,获取手势当前位置信息
在监听手势过程中,手势的回调都会返回position信息。
_elements即子元素上报统计的集合
1 | int _findIndexOfSelectable(Offset offset) { |
熟悉一下ProxyWidget常用子类
- ParentDataWidget
- InheritedWidget
- NotificationListener
ProxyWidget和ProxyElement的主要功能
ProxyWidget
:
- 实现
createElement
方法,创建一个ProxyElement
子类; - 定义widget对外属性
- 提供子类接口
- 例如
InheritedWidget
的updateShouldNotify
,InheritedWidget
子类需要实现updateShouldNotify
,从而决定是否通知重绘等; updateShouldNotify
提升到widget层,而不是在Element的notifyClients
中判断,是为了让使用者不关注Element层。
- 例如
ProxyElement
- 重写
notifyClients
,用于对旧的数据进行对比,判断是否通知重绘等操作 - 根据Element的生命周期,实现自定义业务需求。
- 也可以在
ProxyElement
添加自定义方法,提供给业务使用。
FlutterBoost集成后WillPopScope失效问题
Symbols count in article: 2.3k Reading time ≈ 2 mins.
FlutterBoost集成后WillPopScope失效问题
问题出现场景一: 在集成FlutterBoost后,打开flutter页面WillPopScope包裹页面,其对返回手势拦截的功能没有生效,手势被原生容器捕捉,由原生路由处理
问题出现场景二: 在集成FlutterBoost后,打开flutter页面WillPopScope包裹页面,部分页面生效,但onWillPop返回true和flase效果一致。
定位问题
flutter源码flutter_boost/lib/src/boost_lifecycle_binding.dart
中BoostLifecycleBinding
类的containerDidShow
方法会根据当前容器flutter页面数量,选择是否禁用原生页面手势:
1 | void containerDidShow(BoostContainer container) { |
上面的代码中enablePopGesture
是像原生容器发送消息开启原生容器左滑手势;disablePopGesture
是像原生容器发送消息禁止原生容器左滑手势;
这个event我们在业务代码中也可以调用,会覆盖这部分代码,使用时候需慎重,因为同一个原生容器中,也可能包含多个flutter页面。强制开启手势,左滑返回会将多个flutter页面一同返回。enablePopGesture
事件,也是通过uniqueId
作为key,容器和业务代码建成的通讯channel,该channel目前只实现了对enablePopGesture
的处理。
1 | /// enable iOS native pop gesture for container matching [containerId] |
问题分析
- 在容器的原生手势开启的时候,WillPopScope是拿不到手势相应的,侧滑会由原生路由处理,这个时候添不添加WillPopScope都没有效果
- 在容器的原生手势禁止的时候,页面根据是否包含WillPopScope来判断是否处理flutter侧滑手势,如果有侧滑手势,侧滑路由修的是flutter路由部分,原生容器不受影响。这里注意了,原生容器侧滑手势被禁止,即当当前容器中flutter为路由栈退出后仅剩一个,侧滑手势无人响应,需要手动将原生侧滑手势打开。
在containerDidShow
判断page数量是根据容器计算的,也就是我们在跳转flutter页面的时候,带上withContainer = true
后,每个flutter页面均会独享一个原生容器。
BoostNavigator.instance.push
1 | Future<T> push<T extends Object?>( |
遗留问题
- 在容器的原生手势禁止的时候,页面不会根据WillPopScope的onWillPop返回值来判断是否响应侧滑手势,而是均不可侧滑。
flutter混合开发踩坑记录
Symbols count in article: 558 Reading time ≈ 1 mins.
通过源码接入三方库正常使用,framework产物接入部分功能失效
踩坑场景
flutter在使用permission_handler库时,flutter通过源码接入宿主工程时,iOS申请权限系统弹窗正常弹出。通过打包framework
方式接入宿主工程时,iOS申请系统弹窗不弹,直接返回被永久拒绝。
问题定位路线
- 下载permission_handler库源码,提交到私有仓库
- 增加日志,定位问题原因为permission_handler使用了宿主工程
GCC_PREPROCESSOR_DEFINITIONS
配置的一个宏定义。 GCC_PREPROCESSOR_DEFINITIONS
是用于设置预处理器宏的。这些宏在编译时会被替换。然而,这些宏只能在编译时期的源代码中使用,不能在运行时期的代码中使用。因此,如果你在framework中尝试读取这些宏,是无法读取到的。- permission_handler的iOS部分源码,通过宏是判断业务代码是否开启了某一权限白名单,获取到宏且宏定义为1,即调用申请权限流程。否则直接返回权限被永久拒绝。
解决方案
通过设置permission_handler的iOS源码中默认宏配置,开启对应白名单。打包提交私有库,页面代码修改 permission_handler路径指向为修改后的私有库地址。
Flutter 各种列表曝光方案
Symbols count in article: 1.6k Reading time ≈ 1 mins.
Flutter 各种列表曝光方案
目前的曝光有两大场景:播放器可视播放和元素曝光
其中元素曝光还需要细分,有主轴方向滑动曝光,还有混合纵轴方向滑动曝光
场景一:滑动中可视元素内选取元素播放或者停止播放
场景特点
需要在滑动中获取当前可视元素和移除可视范围的元素
- 先判断移除移除可视的元素是否存在播放,如果有,停止播放。
- 在上一步执行完毕之后,判断新的可视元素中是否含符合播放标准
- 开启下一轮播放
- 播放结束前没有被划走,继续监测其他符合播放的元素播放
曝光特点
- 实时监听滑动中视口内元素变化
- 在监听回调中需要同时拿到当前所有可视元素,进行规则匹配
技术方案
API
1 | /// The callback of getting observed result map. |
sliverListContexts
如果不为null,列表会通过element.visitChildren
方法获取的sliverlist列表。
onObserveAll
:是返回所有sliverlist回调
注意:
onObserveAll
返回所有sliverlist
,并不能一一绑定。需要通过函数返回的Map实例的key进行查找。
onObserve
:是返回第一个sliverlist回调
场景二:可视曝光
元素进入屏幕视口内回调,一般用于曝光打点
场景特点
- 用于列表元素曝光打点
曝光特点
- 每次曝光处理,元素独自处理,和列表其他元素曝光没有耦合。
技术方案
ScrollDetailProvider
嵌套滑动列表,监听滑动通知并根据需求转化通知到子组件Exposure
嵌套需要统计的单个元素,Exposure
接受ScrollDetailProvider
转化的通知,根据自身的偏移量判断是否在视口内。ExposureController
实例可以绑定多个Exposure
,通过调用reCheckExposeState
方法手动获取一遍当前视口曝光情况
场景三:可视曝光——特殊组合
我们在监测主轴曝光的同时,对特殊元素需要监测纵轴方向曝光。
场景特点
- 曝光列表中特殊元素曝光
- 适用于任何
renderObject
曝光特点
- 每次曝光处理,元素独自处理,和列表其他元素曝光没有耦合。
技术方案
visibility_detector
个人认为效率不高,而且回调次数较多。
需要监听的组件通过该三方组件直接嵌套
监听
onVisibilityChanged
回调,通过可视比例判断是否满足曝光在元素paint的方法中调用元素(PaintingContext)的
addCompositionCallback
方法,在上述方法中,调用计算任务去判断可视比例
计算任务,通过获取父级列表(到顶端),通过
rect
和transform
计算当前元素的bounds
,组合成VisibilityInfo
1
2
3
4
5final List<ContainerLayer> ancestors = <ContainerLayer>[ContainerLayer()];
while (ancestor != null && ancestor.parent != null) {
ancestors.add(ancestor);
ancestor = ancestor.parent;
}VisibilityInfo
中有visibleBounds
——实际可视的区域,visibleFraction
——显示比例
dart 实现卡尔曼滤波
Symbols count in article: 1.9k Reading time ≈ 2 mins.
Kalman Filter
Kalman滤波被称为线性二次型估计。其也可以认为是一个最优化自回归数据处理算法(optimal recursive data processing algorithm)。前提是这个数据变化是线性变化的。
对于其原理我们可以参考Kalman Filter。文章写的很专业,我就不废话。
应用场景
当我们要观察一个仪器测量值,比如力度、重量等测量,在采集过程中,我们会不断拿到新数据,这些数据是采集器从目标中收集出来,是属于线性变化的。但采集器的精准度和目标物体的都含有不确定行。我们需要对这些数据进行过滤,生成平滑的数据。目标如下图:
蓝线是原始数值,绿线是我们想要的结果。
Kalman Filter dart(Flutter)代码
1 | class KalmanFilter { |
过程噪声协方差
它用来描述状态量在一个时刻到下一个时刻之间的变化幅度。一般来说,过程噪声方差是由系统的动态特性和采样间隔决定的。
如果系统的动态特性比较快,即状态量在一个时刻到下一个时刻之间的变化幅度比较大,那么过程噪声方差就应该设置得比较大,以便更好地反映状态量的变化。反之,如果系统的动态特性比较慢,即状态量在一个时刻到下一个时刻之间的变化幅度比较小,那么过程噪声方差就应该设置得比较小,以便更好地适应状态量的变化。
另外,采样间隔也会影响过程噪声方差的定义。如果采样间隔比较大,即状态量的变化相对较慢,那么过程噪声方差应该设置得比较小,以充分利用状态量的变化信息。反之,如果采样间隔比较小,即状态量的变化相对较快,那么过程噪声方差应该设置得比较大,以避免过度拟合状态量的变化。
因此,过程噪声方差的定义需要考虑系统的动态特性和采样间隔,需要进行实验和分析来确定最佳值。一般来说,过程噪声方差可以通过试验和经验得到,也可以使用自适应卡尔曼滤波器来自动调整。
观测噪声协方差
观测噪声协方差用来描述测量噪声对状态估计的影响。通常情况下,观测噪声协方差是由测量设备的精度和信噪比等因素决定的。
如果测量设备的精度越高,即测量误差越小,那么观测噪声协方差就应该设置得比较小,以更准确地反映状态量的真实值。反之,如果测量设备的精度较低,即测量误差较大,那么观测噪声协方差就应该设置得比较大,以充分考虑测量误差的影响。
另外,信噪比也会影响观测噪声协方差的定义。如果信噪比越高,即信号强度比噪声强度高,那么观测噪声协方差就应该设置得比较小,以更好地反映状态量的真实值。反之,如果信噪比较低,即信号强度比噪声强度低,那么观测噪声协方差就应该设置得比较大,以充分考虑噪声的影响。
因此,观测噪声协方差的定义需要考虑测量设备的精度和信噪比等因素,并且需要根据具体的应用场景进行实验和分析来确定最佳值。一般来说,可以通过实际测量数据的分析和处理来估计观测噪声协方差的值。
基础使用
1 | // 初始化 |
上面这个代码片段只是简单过滤一组数据。在真实使用中当前状态协方差p和卡尔曼增益k会在每次检测流程中根据历史调整,而不是每次update的时候新建。所以是最好是通过一个单例的管理类去管理每次检测流程。
Flutter事件机制
Symbols count in article: 3.3k Reading time ≈ 3 mins.
事件分发机制
- 命中测试
- 事件分发
- 事件清理
命中测试
1 | /// A RenderProxyBox subclass that allows you to customize the |
- 手指按下触发
PointerDownEvent
事件,按照深度优先开始进行命中测试 - 如果子组件命中返回为ture,则先被加入命中结果数组,其父组件同样也会返回true,并加入命中结果数组。这一点在接口
RenderProxyBoxWithHitTestBehavior
的hitTest
方法中体现——hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position)
)。 - 这里要注意如果是多个子组件,是如何去判断命中的。这里我们看一下RenderBoxContainerDefaultsMixins的实现:
1 | bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) { |
多个子组件,遍历子组件(从后往前遍历),有命中的就终止遍历,返回结果。
为什么从后往前(previousSibling),而不是从前往后(nextSibling)?
自问自答:我个人理解是命中区域会有重叠,如Stack。当Stack有重叠的时候,最上层显示children中靠后的,当然事件也优先让他们去命中测试。
命中测试当前作用手机了命中结果数组HitTestResult。
事件分发
GestureBinding类为单例类,集成自BindingBase,并从BindingBase中获取platformDispatcher,并对platformDispatcher的触摸事件处理进行方法进行赋值。
事件分发调用是由platformDispatcher单例去管理的。
1
ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance
GestureBinding管理Queue_handlePointerEventImmediately
并在合适时机遍历分发event到目标上,包括hitTest
命中测试方法,
dispatchEvent
方法先判断事件类型,如果HitTestResult有结果,遍历调用HitTestTarget的handleEvent
方法,组件只需要重新handleEvent
方法去处理事件就好。对于我们日常用的Listener、GestureDetector(Listener的封装)均在Listener的createRenderObject
方法返回的RenderPointerListener中实现:
1 | @override |
handleEvent
根据具体的事件类型,调用传入的回调方法。
至此事件完整的分发出去了。
事件清理
事件清理也是在_handlePointerEventImmediately
方法中进行,当收到up、cancel等事件后,进行相应的分发,并从hitTestResult中移除
记录个人学习过程,不对的地方欢迎大佬指正
flutter 自定义flex组件
Symbols count in article: 5.1k Reading time ≈ 5 mins.
自定义flex组件,获取子组件布局信息
我们都在用
Column
和Row
,他们都是Flex子类,将children中组件纵向或者横向进行摆放。现在有一个需求是摆放完成后,需要知道children中各个widget所占的宽或者高,从而知道他们具体的位置锚点,然后可以根据这些锚点进行对children添加精准动画
我们先来了解一下需要用到的基础知识点Flex
Flex
根据主轴方向显示一个一维widget数组。
其中Column
和Row
就是Flex
子类,他们直接把主轴方向写死简单的进行一次封装。
Flex
集成自MultiChildRenderObjectWidget
,实现两个主要方法createRenderObject
和createRenderObject
,这两个方法分别被Element在挂载(mount)和更新(update)的时候调用。
所以Flex只是MultiChildRenderObjectWidget的一个子类,具体布局还是父类去实现的,Flex通过重写createRenderObject
指定返回元素RenderFlex
,具体元素的加载是在RenderFlex
中执行的,我们想第一时间拿到布局信息,也是在RenderFlex
的performLayout
中获取
RenderObjectElement 源码
1 | abstract class RenderObjectElement extends Element { |
- mount: RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。
- update: 如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成。
自定义Flex组件
这里我们先自定义flex组件,想要拿到并更新我们的位置信息,我们就得重新两个重要方法:createRenderObject
和createRenderObject
。
定义一个LayoutCallback
回调类型,在拿到我们想要的布局后通过这个回调去处理,并在需要更新的时候去调用。
1 |
|
- createRenderObject Element在挂载(mount)到树上通过调用该方法,去创建widget对应的renderObject。
- updateRenderObject 在Widget配置数据更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用
去实现createRenderObject方法
Flex的createRenderObject方法返回的RenderFlex。接下来要做的就是去实现自己的RenderFlex,肯定是去继承RenderFlex,重写相关方法,在重写中拿到我们想要的数据并回调。
- 将回调方法传入我们定义RenderFlex子类
- 在子类layout完毕的时候拿到布局信息,整合并回调
- RenderFlex继承RenderBox,RenderBox混入ContainerRenderObjectMixin 、 RenderBoxContainerDefaultsMixin 和 ContainerBoxParentData,通过 ContainerBoxParentData ,我们可以将 RenderBox 需要的 BoxParentData 和上面的 ContainerParentDataMixin 组合起来,事实上我们得到的 children 双链表就是以 ParentData 的形式呈现出来的。
- 要拿到所有同级widget布局,就要去先去拿到父级的renderObject
- 要拿到父级renderObject,我们可以通过其中一个子级的parentData
- ContainerRenderObjectMixin中的firstChild、lastChild、childCount。RenderFlex中可以直接能达到第一个元素firstChild。
- firstChild拿到parentData,然后就准确拿到了同级的RenderBox
- …
1 |
|
使用
到此,我们已经封装完毕。
1 | TabLabelBar( |
Flutter 自定义图片下载
Symbols count in article: 0 Reading time ≈ 1 mins.