项目作者: TonyReet

项目描述 :
Gesture conflict resolution for iOS and flutter.iOS和flutter手势冲突问题解决.
高级语言: Objective-C
项目地址: git://github.com/TonyReet/iOS-flutter-gesture-conflict.git
创建时间: 2019-07-13T03:59:49Z
项目社区:https://github.com/TonyReet/iOS-flutter-gesture-conflict

开源协议:MIT License

下载


项目介绍

iOS和flutter的手势冲突" class="reference-link">iOS和flutter的手势冲突

单独查看冲突处理

文件目录

flutter_iOS_gesture_demo:iOS项目目录
flutter_ios_gesture_demo_module:flutter项目目录

在iOS项目执行pod install 即可运行混合工程,查看手势问题

具体问题

iOS和flutter都有手势处理,但是在某些情况下可能产生手势冲突,比如iOS有一个抽屉手势,而flutter有一个水平的滑动手势,这个时候就会产生冲突的问题,具体问题看下面情况。

  • iOS抽屉手势
    原生抽屉

1、需求场景

绿色部分为放置flutter的控制器(ContentViewController),当在屏幕左侧滑动的时候,会划出iOS的抽屉控制器(LeftTableViewController),并且此抽屉手势也是iOS控制。
iOS抽屉手势的代码网上很多,此处是从项目里面抽出来的,就不再赘述,代码地址

2、flutter页面

假设我们flutter页面是有横向滑动的view,需要集成到iOS里面去,如下图:
flutter页面

flutter页面主要代码:

  1. Column(children: <Widget>[
  2. Container(
  3. child: ListView.separated(
  4. itemCount: 10,
  5. shrinkWrap: true,
  6. scrollDirection: Axis.horizontal,
  7. separatorBuilder: (BuildContext context, int index) {
  8. return Container(
  9. width: 5,
  10. );
  11. },
  12. itemBuilder: (BuildContext context, int index) {
  13. return GestureDetector(
  14. child: Container(
  15. margin: EdgeInsets.all(5),
  16. child: new Column(
  17. mainAxisAlignment: MainAxisAlignment.center,
  18. children: <Widget>[
  19. Text('测试标题:${index}',
  20. style: TextStyle(fontSize: 19, color: Colors.white),
  21. maxLines: 1,
  22. overflow: TextOverflow.ellipsis),
  23. Container(
  24. width: 22,
  25. height: 2,
  26. color: Colors.white,
  27. ),
  28. Text(
  29. '测试内容:${index}',
  30. textAlign: TextAlign.center,
  31. style: TextStyle(fontSize: 13, color: Colors.white),
  32. )
  33. ],
  34. ),
  35. width: 180,
  36. decoration: BoxDecoration(
  37. borderRadius: BorderRadius.all(Radius.circular(10)),
  38. color: RandomColor().rColor),
  39. ));
  40. },
  41. ),
  42. height: 180),
  43. Expanded(
  44. child:Container()
  45. )
  46. ]
  • 最上方是一个横向滑动的listView,我们先明确一个需求,当我们在listView上面滑动的时候,只触发listView的左右滑动效果,当我们不在listView上面滑动的时候,才触发iOS的抽屉手势。

-那么将flutter集成到iOS以后,当我横向滑动listView的时候,是触发listView滑动,还是会触发iOS的抽屉手势呢?

3、集成效果

我们将flutter页面集成到iOS项目中,集成方法网上有很多,这里使用google官方方案
)

在ContentViewController添加代码:

  1. AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
  2. self.flutterViewController = [[FlutterViewController alloc] initWithEngine:appDelegate.flutterEngine nibName:nil bundle:nil];
  3. self.flutterViewController.view.frame = self.view.bounds;
  4. [self.view addSubview:self.flutterViewController.view];

看一下效果:
首次集成flutter.gif

我们可以看到,在listView上面从左向右滑动时,大概率会触发iOS的抽屉手势,这和我们的需求不符。
相信大家都知道原因了,这就是iOS的手势优先级高于flutter手势的优先级,所以会触发iOS的抽屉手势。

4、解决手势问题

知道原因,解决起来也方便了。这里提供一个方案,当我们的手势在flutter的页面上操作时,由flutter自行判断是否需要触发抽屉的动作,那么在flutter端处理的思路就清晰了。当我们在listView上滑动时候,不需要iOS参与,当我们在flutter其他区域存在手势时,调用iOS原生的触发方法。

流程:
流程图.png

我们判断手势是否在flutter页面上:

  1. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
  2. {
  3. // 若为FlutterView(即点击了flutter),则不截获Touch事件
  4. if ([NSStringFromClass([touch.view class]) isEqualToString:@"FlutterView"]) {
  5. // 当手势在flutter上,由flutter处理
  6. NSLog(@"flutterView");
  7. return NO;
  8. }
  9. NSLog(@"native View");
  10. return YES;
  11. }

iOS和flutter交互使用channel,代码如下:

iOS

  1. FlutterMethodChannel *scrollMethodChannel = [FlutterMethodChannel methodChannelWithName:@"scrollMethodChannel" binaryMessenger:self.flutterViewController];
  2. self.scrollMethodChannel = scrollMethodChannel;
  3. __weak typeof(self) weakSelf = self;
  4. [self.scrollMethodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
  5. [weakSelf flutterInvokeNativeMethod:call result:result];
  6. }];

flutter

  1. static const MethodChannel _scrollMethodChannel =
  2. MethodChannel('scrollMethodChannel');
  3. static String _scrollBeganKey = 'scrollBeganKey';
  4. static String _scrollUpdateKey = 'scrollUpdateKey';
  5. static String _scrollEndKey = 'scrollEndKey';

flutter端,只需要把非listView的手势通知iOS就行,listView手势不需要处理。那么,只需要处理Expanded里面的 Container即可。

  1. Column(children: <Widget>[
  2. Container(
  3. child: ListView.separated(...),// 将listView代码省略,主要提现container
  4. height: 180),
  5. Expanded(
  6. child: GestureDetector(
  7. onHorizontalDragStart: (detail) {
  8. Map<String, dynamic> resInfo = {
  9. "offsetX": detail.globalPosition.dx,
  10. "velocityX": detail.globalPosition.dx
  11. };
  12. _scrollMethodChannel.invokeMethod(_scrollBeganKey, resInfo);
  13. },
  14. onHorizontalDragEnd: (detail) {
  15. Map<String, dynamic> resInfo = {
  16. "offsetX": 0,
  17. "velocityX": detail.primaryVelocity
  18. };
  19. _scrollMethodChannel.invokeMethod(_scrollEndKey, resInfo);
  20. },
  21. onHorizontalDragUpdate: (detail) {
  22. Map<String, dynamic> resInfo = {
  23. "offsetX": detail.globalPosition.dx,
  24. "velocityX": detail.primaryDelta
  25. };
  26. _scrollMethodChannel.invokeMethod(_scrollUpdateKey, resInfo);
  27. },
  28. child: Container(color: Colors.yellow),
  29. ))
  30. ]

看代码其实比较简单,使用GestureDetector 的onHorizontalDragxxx方法监听开始滑动,滑动ing,和滑动结束的动作,并且将嘴硬的坐标和滑动的速度信息等传递给iOS,iOS拿到数据后,进行view的移动处理即可。

iOS拿到数据后的处理方式:

  1. // 定义的block:
  2. @property (nonatomic, copy) void(^scrollGestureBlock)(CGFloat offsetX,CGFloat velocityX,TYSideState state);
  3. - (void)flutterInvokeNativeMethod:(FlutterMethodCall * _Nonnull )call result:(FlutterResult _Nonnull )result{
  4. if (!call.arguments)return;
  5. NSLog(@"测试%@",call.arguments);
  6. CGFloat offsetX = [call.arguments[@"offsetX"] floatValue];
  7. CGFloat velocityX = [call.arguments[@"velocityX"] floatValue];
  8. /// 开始滑动
  9. if ([call.method isEqualToString:@"scrollBeganKey"]){
  10. if (self.scrollGestureBlock){
  11. dispatch_async(dispatch_get_main_queue(), ^{
  12. self.scrollGestureBlock(0, velocityX, TYSideStateBegan);
  13. });
  14. }
  15. }
  16. /// 滑动更新
  17. if ([call.method isEqualToString:@"scrollUpdateKey"]){
  18. if (self.scrollGestureBlock){
  19. dispatch_async(dispatch_get_main_queue(), ^{
  20. self.scrollGestureBlock(offsetX, velocityX, TYSideStateUpdate);
  21. });
  22. }
  23. }
  24. /// 结束滑动
  25. if ([call.method isEqualToString:@"scrollEndKey"]){
  26. if (self.scrollGestureBlock){
  27. dispatch_async(dispatch_get_main_queue(), ^{
  28. self.scrollGestureBlock(0, velocityX, TYSideStateEnded);
  29. });
  30. }
  31. }
  32. }

很开心的开始看效果,结果还是有问题,仔细看图,当触发抽屉滑动的时候,边缘有明显的抖动。
flutter抖动.gif

5、抖动处理

查看原因发现,当我们将滑动的消息发送到iOS以后,iOS会修改flutterView的x坐标,比如从0修改到10。但是flutter的手势此时一直没有中断,并且时从0开始计算偏移量,但是iOS修改x坐标以后,偏移量就会有10的误差,这个时候,就想到当iOS修改完x后,将x保存起来。在下次flutter消息到来的时候,加上此偏移量即可。

保存x偏移:

  1. if ([self.rootViewController isKindOfClass:[ContentViewController class]]){
  2. ContentViewController *vc = (ContentViewController *)self.rootViewController;
  3. vc.currentViewOffsetX = xoffset;
  4. }

使用x偏移:

  1. /// 滑动更新
  2. if ([call.method isEqualToString:@"scrollUpdateKey"]){
  3. if (self.scrollGestureBlock){
  4. dispatch_async(dispatch_get_main_queue(), ^{
  5. self.scrollGestureBlock(offsetX + self.currentViewOffsetX, velocityX, TYSideStateUpdate);
  6. });
  7. }
  8. }
  9. /// 结束滑动
  10. if ([call.method isEqualToString:@"scrollEndKey"]){
  11. if (self.scrollGestureBlock){
  12. dispatch_async(dispatch_get_main_queue(), ^{
  13. self.scrollGestureBlock(self.currentViewOffsetX, velocityX, TYSideStateEnded);
  14. });
  15. }
  16. }

效果:
最后效果.gif

可以看到,当在ListView上面滑动的时候,listView左右滑动正常,并且没有误触iOS时候,当在flutter下方的非listView区域滑动时,能够触发iOS的抽屉手势,并且没有抖动。

文章地址:https://www.jianshu.com/p/47729e23b3f3

flutter和native混合开发的项目,很多需要共用一套文件,以减小包大小,比如共用图片,字体资源等。图片资源的共用方案很多,但是flutter和native共用字体方案资料比较少。

iOS实现共用字体" class="reference-link">iOS实现共用字体

单独查看字体处理

共用资源的方案的话,主要从两方面解决。

1、资源在native,通过某种方式将资源从native传给flutter使用。

这种方案一直没有想到处理的办法,如果知道怎么处理的同学请告知下,谢谢。

2、资源在flutter,通过某种方式将资源从flutter传给native使用。

iOS加载字体有2种方式
a:在工程里面添加字体文件 xxx.otf,yyy.otf,然后在Info.plist添加字段”Fonts provided by application”,并且添加对应的字体,如图:
123.png

b:可以通过动态注册字体的方法加载字体

具体实现:

  1. + (void) loadCustomFont:(NSString*)fontFileName{
  2. NSString *fontPath = [[NSBundle mainBundle] pathForResource:fontFileName ofType:nil];
  3. if (!fontPath) {
  4. NSLog(@"Failed to load font: no fontPath %@", fontFileName);
  5. return;
  6. }
  7. NSData *inData = [NSData dataWithContentsOfFile:fontPath];
  8. CFErrorRef error;
  9. CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
  10. CGFontRef font = CGFontCreateWithDataProvider(provider);
  11. if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
  12. CFStringRef errorDescription = CFErrorCopyDescription(error);
  13. NSLog(@"Failed to load font: %@", errorDescription);
  14. CFRelease(errorDescription);
  15. }
  16. CFRelease(font);
  17. CFRelease(provider);
  18. }

如代码所示,只需要确定font文件的路径即可,问题是怎么获取font文件的路径呢?
经过查找,方法也简单,只需要使用lookupKeyForAsset即可获取到font的文件地址

  1. [FlutterDartProject lookupKeyForAsset:@"xxxx"];

lookupKeyForAsset的参数是pubspec.yaml配置的font文件地址,加入我们配置的地址是”fonts/iconfont.ttf”

  1. fonts:
  2. - family: iconfont
  3. fonts:
  4. - asset: fonts/iconfont.ttf

那么lookupKeyForAsset就是

  1. [FlutterDartProject lookupKeyForAsset:@"fonts/iconfont.ttf"];

Android实现共用字体

android比较简单

  1. val assetManager = FlutterMain.getLookupKeyForAsset()
  2. val fontKey = flutterView.getLookupKeyForAsset("fonts/iconfont.ttf")
  3. val myTypeface = Typeface.createFromAsset(assetManager, fontKey)

如果iOS加载字体的时候报错:

  1. Could not register the CGFont '<CGFont (0xxxxxxxxx): YYYYYYY>

可以使用FontCreator(Win)或者FontForge(Mac)修改字体信息[/md]