欢迎重视微信公众号:FSA全栈举动
一、问题
我先跳转至 Flutter
页面,1秒后在 Flutter
页面上增加一个原生的弹窗视图,代码如下:
let flutterVc = FlutterViewController(engine: fetchFlutterEngine(), nibName: nil, bundle: nil)
self.navigationController?.pushViewController(flutterVc, animated: true)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) { [self] in
let popView = LXFPopView(frame: CGRect(x: 0, y: 0, width: screenW, height: screenH))
flutterVc.view.addSubview(popView)
popView.checkInfoBlock = { [weak self] in
guard let self = self else { return }
self.navigationController?.pushViewController(InfoViewController(), animated: true)
}
}
能够看到,我在原生弹窗视图上滑动和点击,会被底下的 Flutter
内容所呼应~
有人会说,直接增加到 navigationController
的 view
上不就行了吗?
// flutterVc.view.addSubview(popView)
self.navigationController?.view.addSubview(popView)
不可,由于跳转到其它页面后会遮挡其它页面内容,看效果图便一望而知
接下来咱们一起来看看 FlutterViewController
源码,便可知道原因了
二、源码
1、定位匹配的源码
首先找到与当前 Flutter
环境相匹配的源码内容
➜ flutter doctor -v
[✓] Flutter (Channel stable, 2.10.4, on macOS 12.2.1 21D62 darwin-x64, locale
zh-Hans-CN)
• Flutter version 2.10.4 at /Users/lxf/developer/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision c860cba910 (9 days ago), 2022-03-25 00:23:12 -0500
• Engine revision 57d3bac3dd
• Dart version 2.16.2
• DevTools version 2.9.2
• Pub download mirror https://pub.flutter-io.cn
• Flutter download mirror https://storage.flutter-io.cn
...
从此处能够拿到 Engine
的 commit id
Engine revision 57d3bac3dd
将下方链接中的 【commit id】
进行替换即可得到相匹配的源码链接了
https://github.com/flutter/engine/blob/【commit id】/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
链接: engine/FlutterViewController.mm at 57d3bac3dd
2、定位形成问题的源码
经过源码的检查,能够很快定位到如下部分的内容:
...
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}
- (void)forceTouchesCancelled:(NSSet*)touches {
flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
[self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
}
...
// Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
// from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
// in the status bar area are available to framework code. The change type (optional) of the faked
// touch is specified in the second argument.
- (void)dispatchTouches:(NSSet*)touches
pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
event:(UIEvent*)event {
...
}
能够看到,在 FlutterViewController
内部,重写了 touchesXXX
系列的方法,然后统一调用 - dispatchTouches:pointerDataChangeOverride:event:
方法,将 UITouches
分发至 Flutter
引擎,从而与 Flutter
内容进行交互
这便是咱们在原生弹窗上的点击、拖拽操作会被 Flutter
内容所呼应的原因。
三、解决问题
既然咱们已经知道原因所在,现在就好想办法去解决这个问题了
这儿我直接给出最终实现代码:
// LXFFlutterViewController.swift
import Foundation
import Flutter
protocol LXFFlutterForbidResponseProtocol { }
class LXFFlutterViewController: FlutterViewController {
var isForbidResponseForFlutter = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.isForbidResponse() {
self.isForbidResponseForFlutter = true
return
}
print("touches began")
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if isForbidResponseForFlutter { return }
print("touches move")
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if isForbidResponseForFlutter {
self.isForbidResponseForFlutter = false
return
}
print("touches ended")
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
if self.isForbidResponse() {
self.isForbidResponseForFlutter = false
return
}
print("touches cacelled")
super.touchesCancelled(touches, with: event)
}
}
extension LXFFlutterViewController {
func isForbidResponse() -> Bool {
var subViews = self.view?.subviews ?? []
subViews = subViews.reversed()
for i in 0..<subViews.count {
let subView = subViews[i]
if self.isHadForbidResponseView(view: subView) {
return true
}
}
return false
}
fileprivate func isHadForbidResponseView(view: UIView) -> Bool {
if view is LXFFlutterForbidResponseProtocol {
return true
}
let subViews = view.subviews
for i in 0..<subViews.count {
let subView = subViews[i]
if self.isHadForbidResponseView(view: subView) {
return true
}
}
return false
}
}
上图能够看到,在 FlutterView
中,LXFPopView
这一类的弹窗视图一般都会在运用时才会刺进到视图中,所以在 isForbidResponse
方法里进行反转遍历子视图,以减少遍历次数。
touchesMoved
调用次数较多,所以为了避免在 touchesMoved
中去高频率的遍历 subViews
,这儿运用了 isForbidResponseForFlutter
变量,在 touchesBegan
时判别并记录是否需要禁用 Flutter
内容呼应触摸事情,在 touchesEnded
和 touchesCancelled
中对 isForbidResponseForFlutter
重置为 false
四、运用过程
过程一:
运用 LXFFlutterViewController
let flutterVc = LXFFlutterViewController(engine: fetchFlutterEngine(), nibName: nil, bundle: nil)
过程二:
令弹窗视图所在类遵守协议:LXFFlutterForbidResponseProtocol
extension LXFPopView: LXFFlutterForbidResponseProtocol { }
看看效果如何:
完美
最后附上 Demo
链接:LinXunFeng/flutter_hybrid_touch_response_demo (github.com)