Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题

4,714 阅读3分钟

欢迎关注微信公众号: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 内容所响应~

有人会说,直接添加到 navigationControllerview 上不就行了吗?

// 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
...

从此处可以拿到 Enginecommit 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 内容响应触摸事件,在 touchesEndedtouchesCancelled 中对 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)