Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
# 一、问题
我先跳转至 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 (opens new window)
# 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) (opens new window)
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08