场景

横滑tab上嵌套flutter页面,iOS 的UIScrollview内放Flutter页面,scrollview可左右滑动,scrollview的手势优先级最高,滑动flutter页面的时候会触发iOS原生手势,flutter页面很难滑动

手势原理

iOS的所有手势优先级高于touch事件,滑动手势的优先级高于touch事件

flutter的滑动手势是由 iOS的touch事件传递到flutterview上,flutter对touch事件进行处理得到的flutter的手势动作

滑动冲突原因

UIScrollview 的滑动手势在此场景下优先级最高,这个场景的scrollview的滑动手势是在左右滑动下触发,正常上下滑动不会触发UIScrollview的手势,这个时候flutterview是可以滑动的,但是手指稍微有一点左右偏移,就会触发原生手势,触发原生手势之后,touch事件就不会触发,flutterview获得不了touch事件就无法识别手势,就无法滑动了

解决

iOS原生手势的优先级最高,flutter的手势是touch事件,所以原生手势比flutter天生高一个级别,要解决这个问题必须把flutter和原生提到一个级别,进行手势冲突竞争处理,我们在原生中在flutterview上加一个原生手势,这个手势是flutter在原生中竞争的代理手势,用这个代理手势和原生手势竞争,这个代理手势的优先级最高,用户触摸屏幕时先让这个手势响应,把touch事件传递到flutter响应手势,如果flutter页面可以响应手势,那这个原生的代理手势就竞争成功了,如果flutter不能响应手势就告诉原生代理手势响应失败了,让iOS的Scrollview的手势响应,

此方案的flutter端代码用在安卓上相同的原理也可以解决冲突问题,安卓上的不需要代理手势直接修改滑动手势是否响应就可以,

判断flutter是否有手势可以响应,可以利用flutter的手势竞技场,在flutter的widget最上层加一个PanGestureRecognizer手势优先级是最低的,如果有其他手势响应会触发rejectGesture,如果没有其他手势触发会响应这个手势触发acceptGesture,

实现

设置代理手势的优先级高于UIScrollview高于的滑动手势

[superScrollView.panGestureRecognizer requireGestureRecognizerToFail:self.flutterGesture];

将flutter代理手势的touch事件传递到flutter

iOS的手势有个属性cancelsTouchesInView 默认是YES,响应手势的时候不会触发touch事件,给flutter的代理手势设置为YES就可以在代理手势响应的时候把touch事件传递到flutterView

flutterGesture.cancelsTouchesInView = NO;

判断flutter是否可以响应手势

class _PointerTracker extends PanGestureRecognizer {
  MethodChannel channel = const MethodChannel('com.qqreader.flutter/scroll_channel');
  bool _flutterGestureIsWorking = false;
  @override
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    _flutterGestureIsWorking = true;
    _notify();
  }

  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    _flutterGestureIsWorking = false;
    _notify();
  }

  void _notify() {
    try{
      if (_flutterGestureIsWorking) {
        channel.invokeMethod("isWork", "");
      } else {
        channel.invokeMethod("noWork", "");
      }
    } catch(e){
      // ignored, really.
    }
  }
}

以上代码基本可以解决滑动冲突的问题了,但是会发现flutter页面滑动的时候反应会很慢,那是因为UIScrollview的delaysContentTouches 属性导致的,这个属性默认是YES,touch事件会延迟传递给内部的view flutter收到手势的时机会被延迟,会出现滑得很快但是页面走的很慢,delaysContentTouches设置为NO就可以解决了

superScrollView.delaysContentTouches = NO;

源码
iOS端 
@interface QRNativeScrollFixViewController ()

@property (nonatomic, strong) UIPanGestureRecognizer *flutterGesture;
@property (nonatomic, strong) FlutterMethodChannel *scrollChannel;
@end

@implementation QRNativeScrollFixViewController

- (void)viewDidLoad{
    [super viewDidLoad];
   
    UIPanGestureRecognizer *flutterGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(flutterGestureAction)];
    flutterGesture.delegate = self;
    
    flutterGesture.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:flutterGesture];
    self.flutterGesture = flutterGesture;
    
    self.scrollChannel = [FlutterMethodChannel
                          methodChannelWithName:@"com.qqreader.flutter/scroll_channel"
                          binaryMessenger:self.engine.binaryMessenger];
    [self handleMethod];
}

- (void)flutterGestureAction{
    //do nothing
}

- (void)viewWillLayoutSubviews{
    [super viewWillLayoutSubviews];
    UIScrollView *superScrollView = [self getScrollView];
    superScrollView.delaysContentTouches = NO;
    if(superScrollView){
        [superScrollView.panGestureRecognizer requireGestureRecognizerToFail:self.flutterGesture];
    }
}

- (UIScrollView *)getScrollView{
    UIView *superView = self.view.superview;
    while (superView) {
        if([superView isKindOfClass:UIScrollView.class]){
            return (UIScrollView *)superView;
        } else {
            superView = superView.superview;
        }
    }
    return nil;
}

- (void)flutterHandleTouch:(BOOL)isWorking{
    if(isWorking){
        self.flutterGesture.state = UIGestureRecognizerStateBegan;
    } else {
        self.flutterGesture.state = UIGestureRecognizerStateFailed;
    }
}

- (void)handleMethod {
    @weakify(self);
    [self.scrollChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        @strongify(self);

        if ([@"isWork" isEqualToString:call.method]) {
            [self flutterHandleTouch:YES];
        }
        if ([@"noWork" isEqualToString:call.method]) {
            [self flutterHandleTouch:NO];
        }
        result(nil);
    }];
}
@end
 
   
flutter源码

使用时直接用QRNativeScrollFixWidget 嵌套在最外层就可以

class QRNativeScrollFixWidget extends StatelessWidget {
  const QRNativeScrollFixWidget({super.key, required this.child});
  final Widget child;
  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      behavior: HitTestBehavior.translucent,
      gestures: {
        _PointerTracker: GestureRecognizerFactoryWithHandlers<_PointerTracker>(
          () => _PointerTracker(),
          (_PointerTracker instance) {
            instance
              ..onStart = (DragStartDetails details) {
                print('开始');
              }
              ..onEnd = (detail) {
                print('结束');
              };
          },
        )
      },
      child: child,
    );
  }
}

class _PointerTracker extends PanGestureRecognizer {
  MethodChannel channel = const MethodChannel('com.qqreader.flutter/scroll_channel');
  bool _flutterGestureIsWorking = false;
  @override
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    _flutterGestureIsWorking = true;
    _notify();
  }

  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    _flutterGestureIsWorking = false;
    _notify();
  }

  void _notify() {
    try{
      if (_flutterGestureIsWorking) {
        channel.invokeMethod("isWork", "");
      } else {
        channel.invokeMethod("noWork", "");
      }
    } catch(e){
      // ignored, really.
    }
  }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐