【跨平台开发Flutter】iOS和Flutter里的事件处理实例

目录
一、寻找第一响应者阶段的实例
二、原始指针事件的实例
三、手势的实例
四、一些手势冲突处理的实例

iOS和Flutter里的事件处理
iOS和Flutter里的事件处理实例


一、寻找第一响应者阶段的实例


1、点击子视图超出父视图的部分,让父视图也能响应事件

红色视图是父视图,绿色视图是子视图
iOS里的处理

正常情况下点击绿色视图超出红色视图的部分,红色视图是不会响应事件的,因为执行到红色视图的hitTest方法时,里面判断到触摸的点不在红色视图身上,直接返回了nil,这才导致红色视图没有机会成为响应者。

这种场景下,如果真得想让红色视图也能响应事件,我们只需要重写红色视图的hitTest方法,在里面强制把红色视图搞成响应者就可以了。

------RedView.h------

#import 

@interface RedView : UIView

@end


------RedView.m------

#import "RedView.h"

@implementation RedView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 获取到子视图——greenView
    UIView *greenView = self.subviews[0];
    // 把触摸点的坐标转换到子视图——greenView的坐标系统下
    CGPoint convertedPoint = [self convertPoint:point toView:greenView];
    
    if ([greenView pointInside:convertedPoint withEvent:event]) { // 如果触摸的点在子视图——greenView范围内,那就让父视图——redView作为响应者
        return self;
    } else { // 否则就采用系统默认的响应方式
        return [super hitTest:point withEvent:event];
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}

@end
Flutter里的处理

正常情况下点击绿色视图超出红色视图的部分,红色视图是不会响应事件的,因为执行到红色视图的hitTest方法时,里面判断到触摸的点不在红色视图身上,就没把红色视图的子视图及红色视图添加到响应者数组里,而是直接返回了false,告诉红色视图的父视图去hitTest红色视图的兄弟视图了,这才导致红色视图没有机会成为响应者。

这种场景下,如果真得想让红色视图也能响应事件,我们只需要重写红色视图的hitTest方法就可以了,在里面强制把红色视图的子视图(特指能响应事件的子视图)及红色视图添加到响应者数组里就可以了。

------main.dart------

import 'package:flutter/material.dart';
import 'package:flutter_app/red_color_widget.dart';
import 'package:flutter_app/green_color_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding: EdgeInsets.only(top: 20, left: 20),
      alignment: Alignment.topLeft,
      child: RedColorWidget(
        child: GreenColorWidget(),
      ),
    );
  }
}
------red_color_widget.dart------

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_app/green_color_widget.dart';

class RedColorWidget extends SingleChildRenderObjectWidget {
  RedColorWidget({
    Widget? child,
  }) : super(
    child: Listener(
      child: Container(
        color: Colors.red,
        width: 200,
        height: 200,
        padding: EdgeInsets.only(top: 100, left: 100),
        child: OverflowBox(
          alignment: Alignment.topLeft,
          minWidth: 0,
          minHeight: 0,
          maxWidth: 200,
          maxHeight: 200,
          child: child,
        ),
      ),
      onPointerDown: (_) {
        print("redView onPointerDown");
      },
    ),
  );

  /// 为我们自定义的RenderObjectWidget生成RenderObject
  @override
  RenderObject createRenderObject(BuildContext context) {
    return RedColorWidgetRenderObject();
  }

  /// 为我们自定义的RenderObjectWidget更新RenderObject
  @override
  void updateRenderObject(
      BuildContext context, covariant RenderObject renderObject) {
    // 暂时没啥可更新的...
  }
}

class RedColorWidgetRenderObject extends RenderProxyBox {
  RedColorWidgetRenderObject();

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    // 找绿色视图对应的RenderObject
    var green = this.child;
    while (green != null && !(green is GreenColorWidgetRenderObject)) {
      if (green is RenderProxyBox) {
        green = green.child;
      }

      if (green is RenderShiftedBox) {
        green = green.child;
      }
    }
    // 把触摸点的坐标转换绿色视图对应的RenderObject的坐标系统下
    Offset convertedPosition = green!.globalToLocal(position);

    if (green.size.contains(convertedPosition)) {
      // 如果触摸的点在绿色视图对应的RenderObject范围内,那就把红色视图对应的RenderObject添加到响应者数组里
      // 但这还不够,因为红色视图对应的RenderObject并不能响应事件,真正响应事件的是Listener对应的RenderObject,我们还需要把它也添加进去
      // 我们根据上面写的UI层级可以知道的child就是Listener,所以相应的红色视图对应的RenderObject的child就是Listener对应的RenderObject
      result.add(BoxHitTestEntry(this.child!, position));
      result.add(BoxHitTestEntry(this, position));

      // return true,告诉红色视图的父视图:红色视图这一层级的响应者已经找到了,不要再遍历红色视图的兄弟视图了
      return true;
    } else {
      // 否则就采用系统默认的响应方式
      return super.hitTest(result, position: position);
    }
  }
}
------green_color_widget.dart------

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class GreenColorWidget extends SingleChildRenderObjectWidget {
  GreenColorWidget()
      : super(
          child: Container(
            color: Colors.green.withOpacity(0.618),
            width: 200,
            height: 200,
          ),
        );

  @override
  RenderObject createRenderObject(BuildContext context) {
    return GreenColorWidgetRenderObject();
  }

  @override
  void updateRenderObject(
      BuildContext context, covariant RenderObject renderObject) {}
}

class GreenColorWidgetRenderObject extends RenderProxyBox {
  GreenColorWidgetRenderObject();
}

2、事件穿透

红色视图和绿色视图没有父子关系,只不过红色视图在下面,绿色视图像个蒙版一样盖在红色视图上面
iOS里的处理

正常情况下点击红色视图,红色视图是不会响应事件的,因为执行到绿色视图的hitTest方法时,里面判断到触摸的点在绿色视图身上,就把绿色视图return出去作为第一响应者了,这才导致红色视图没有机会成为响应者。

这种场景下,如果真得想让红色视图也能响应事件,我们只需要重写绿色视图的hitTest方法,在里面强制把红色视图搞成响应者就可以了。

------GreenView.h------

#import 

@interface GreenView : UIView

@end


------GreenView.m------

#import "GreenView.h"
#import "RedView.h"

@interface GreenView ()

@property (weak, nonatomic) IBOutlet RedView *redView;

@end

@implementation GreenView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 把触摸点的坐标转换到redView的坐标系统下
    CGPoint convertedPoint = [self convertPoint:point toView:self.redView];
    
    if ([self.redView pointInside:convertedPoint withEvent:event]) { // 如果触摸的点在redView范围内,那就让redView作为响应者
        return self.redView;
    } else { // 否则就采用系统默认的响应方式
        return [super hitTest:point withEvent:event];
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}

@end
Flutter里的处理

正常情况下点击红色视图,红色视图是不会响应事件的,因为执行到绿色视图的hitTest方法时,里面判断到触摸的点在绿色视图身上,就把绿色视图添加到响应者数组里了,并且直接返回了true,告诉绿色视图的父视图不用去hitTest绿色视图的兄弟视图——即红色视图了,这才导致红色视图没有机会成为响应者。

这种场景下,如果真得想让红色视图也能响应事件,我们只需要重写绿色视图的hitTest方法就可以了,在里面强制把红色视图的子视图(特指能响应事件的子视图)及红色视图添加到响应者数组里就可以了。

------main.dart------

import 'package:flutter/material.dart';
import 'package:flutter_app/red_color_widget.dart';
import 'package:flutter_app/green_color_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      child: Stack(
        alignment: Alignment.center,
        children: [
          RedColorWidget(), // 必须自定义红色视图
          GreenColorWidget(), // 也必须自定义绿色视图
        ],
      ),
    );
  }
}
------red_color_widget.dart------

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class RedColorWidget extends SingleChildRenderObjectWidget {
  RedColorWidget()
      : super(
          child: Listener(
            child: Container(
              color: Colors.red,
              width: 50,
              height: 50,
            ),
            onPointerDown: (_) {
              print("redView onPointerDown");
            },
          ),
        );

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RedColorWidgetRenderObject();
  }

  @override
  void updateRenderObject(
      BuildContext context, covariant RenderObject renderObject) {}
}

class RedColorWidgetRenderObject extends RenderProxyBox {
  RedColorWidgetRenderObject();
}
------green_color_widget.dart------

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class GreenColorWidget extends SingleChildRenderObjectWidget {
  GreenColorWidget()
      : super(
          child: Container(
            color: Colors.green.withOpacity(0.618),
            width: 414,
            height: 736,
          ),
        );

  /// 为我们自定义的RenderObjectWidget生成RenderObject
  @override
  RenderObject createRenderObject(BuildContext context) {
    return GreenColorWidgetRenderObject();
  }

  /// 为我们自定义的RenderObjectWidget更新RenderObject
  @override
  void updateRenderObject(
      BuildContext context, covariant RenderObject renderObject) {
    // 暂时没啥可更新的...
  }
}

class GreenColorWidgetRenderObject extends RenderProxyBox {
  GreenColorWidgetRenderObject();

  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
    if (size.contains(position)) {
      if (hitTestChildren(result, position: position) ||
          hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));

        // return false,告诉绿色视图的父视图去hitTest绿色视图的兄弟视图————红色视图
        return false;
      }
    }
    return false;
  }
}


二、原始指针事件的实例


画板

iOS里的实现

画板的实现比较简单,一共分两步:

  • 第一步:用UIRespondertouchesMove方法来画路径,并把画的路径数组给记录下来;
  • 第二步:在drawRect方法里,用贝塞尔曲线绘制记录的路径数组。
------ViewController.h------

#import 

@interface ViewController : UIViewController

@end


------ViewController.m------

#import "ViewController.h"
#import "DrawingBoardView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    DrawingBoardView *drawingBoardView = [[DrawingBoardView alloc] init];
    drawingBoardView.backgroundColor = [UIColor blackColor];
    drawingBoardView.frame = CGRectMake(0, 20, 414, 414);
    [self.view addSubview:drawingBoardView];
}

@end
------DrawingBoardView.h------

#import 

/// 画板View
///
/// 画板的实现比较简单,一共分两步:
/// 第一步:用UIResponder的touchesMove方法来画路径,并把画的路径数组给记录下来
/// 第二步:在drawRect方法里,用贝塞尔曲线绘制记录的路径数组
@interface DrawingBoardView : UIView

@end


------DrawingBoardView.m------

#import "DrawingBoardView.h"

@interface DrawingBoardView ()

/// 每一次画的路径
@property (nonatomic, strong) UIBezierPath *bezierPath;

/// 画的路径数组
@property (nonatomic, strong) NSMutableArray *bezierPathArray;

@end

@implementation DrawingBoardView

- (instancetype)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    if (self) {
        
        self.bezierPathArray = [NSMutableArray array];
    }
    return self;
}


#pragma mark - 第一步:用UIResponder的touchesMove方法来画路径,并把画的路径数组给记录下来

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    // 触摸的手指可能有多个,任选其一
    UITouch *touch = [touches anyObject];
    // 获取开始触摸的点在画板坐标系统下的位置
    CGPoint beganPoint = [touch locationInView:self];
    
    // 每一次画的路径
    self.bezierPath = [UIBezierPath bezierPath];
    
    // 把开始触摸的点追加到当前路径上
    [self.bezierPath moveToPoint:beganPoint];
    
    // 记录每一次画的路径
    [self.bezierPathArray addObject:self.bezierPath];
    
    // 刷新UI
    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    // 触摸的手指可能有多个,任选其一
    UITouch *touch = [touches anyObject];
    // 获取实时触摸的点在画板坐标系统下的位置
    CGPoint movedPoint = [touch locationInView:self];
    
    // 把实时触摸的点追加到当前路径上
    [self.bezierPath addLineToPoint:movedPoint];
    
    // 刷新UI
    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    
    // 触摸的手指可能有多个,任选其一
    UITouch *touch = [touches anyObject];
    // 获取结束触摸的点在画板坐标系统下的位置
    CGPoint endedPoint = [touch locationInView:self];
    
    // 把实时触摸的点追加到当前路径上
    [self.bezierPath addLineToPoint:endedPoint];
    
    // 刷新UI
    [self setNeedsDisplay];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    
    // 触摸的手指可能有多个,任选其一
    UITouch *touch = [touches anyObject];
    // 获取取消触摸的点在画板坐标系统下的位置
    CGPoint cancelledPoint = [touch locationInView:self];
    
    // 把取消触摸的点追加到当前路径上
    [self.bezierPath addLineToPoint:cancelledPoint];
    
    // 刷新UI
    [self setNeedsDisplay];
}


#pragma mark - 第二步:在drawRect方法里,用贝塞尔曲线绘制记录的路径数组

- (void)drawRect:(CGRect)rect {
    
    for (UIBezierPath *bezierPath in self.bezierPathArray) {
        
        // 画笔
        [[UIColor redColor] setStroke]; // 路径颜色
        [bezierPath strokeWithBlendMode:(kCGBlendModeNormal) alpha:1.0]; // 路径模式
        // 橡皮擦
//        [[UIColor clearColor] setStroke]; // 路径颜色
//        [bezierPath strokeWithBlendMode:(kCGBlendModeClear) alpha:1.0]; // 路径模式
        
        bezierPath.lineWidth = 3; // 路径宽度
        bezierPath.lineCapStyle = kCGLineCapRound; // 路径开始和结尾的样式
        bezierPath.lineJoinStyle = kCGLineJoinRound; // 路径转角处的样式
        [bezierPath stroke];
    }
}

@end
Flutter里的实现

画板的实现比较简单,一共分两步:

  • 第一步:用Listener的move方法来画路径,并把画的路径数组给记录下来;
  • 第二步:用CustomPainter绘制记录的路径数组。
------main.dart------

import 'package:flutter/material.dart';

import 'package:flutter_app/drawing_board_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      alignment: Alignment.topLeft,
      child: DrawingBoardWidget(),
    );
  }
}
------drawing_board_widget.dart------

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// 画板Widget
///
/// 画板的实现比较简单,一共分两步:
/// 第一步:用Listener的move方法来画路径,并把画的路径数组给记录下来
/// 第二步:用CustomPainter绘制记录的路径数组
class DrawingBoardWidget extends StatefulWidget {
  DrawingBoardWidget();

  @override
  DrawingBoardWidgetState createState() => DrawingBoardWidgetState();
}

class DrawingBoardWidgetState extends State {
  /// 每一次画的路径
  late CustomPath _myPath;

  /// 画的路径数组
  List _pathList = [];

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: Listener(
        onPointerDown: (PointerDownEvent pointerDownEvent) {
          setState(() {
            _myPath = CustomPath();

            _pathList.add(_myPath);
          });
        },
        onPointerMove: (PointerMoveEvent pointerMoveEvent) {
          setState(() {
            _myPath.pointList.add(pointerMoveEvent.localPosition);
          });
        },
        onPointerUp: (PointerUpEvent pointerUpEvent) {
          setState(() {
            _myPath.pointList.add(pointerUpEvent.localPosition);
          });
        },
        onPointerCancel: (PointerCancelEvent pointerCancelEvent) {
          setState(() {
            _myPath.pointList.add(pointerCancelEvent.localPosition);
          });
        },
        child: Container(
          color: Colors.black,
          width: 414,
          height: 414,
          child: CustomPaint(
            painter: _DrawingBoardPainter(
              pathList: _pathList,
            ),
          ),
        ),
      ),
    );
  }
}

/// 绘制记录的路径数组
class _DrawingBoardPainter extends CustomPainter {
  /// 记录的路径数组
  final List pathList;

  _DrawingBoardPainter({
    required this.pathList,
  });

  /// 重写掉系统的paint()方法,把平移的点绘制到画布上就可以了
  @override
  void paint(Canvas canvas, Size size) {
    // canvas.saveLayer()和canvas.restore()必须写,保证橡皮擦的涂层能和画笔的涂层合并
    canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
    pathList.forEach((customPath) {
      // 画笔
      Paint paint = Paint()
        ..color = Colors.red // 路径颜色
        ..blendMode = BlendMode.srcOver // 路径模式
        ..strokeWidth = 3 // 路径宽度
        ..strokeCap = StrokeCap.round // 路径开始和结尾的样式
        ..style = PaintingStyle.stroke; // 路径转角处的样式

      // 橡皮擦
      // Paint paint = Paint()
      //   ..color = Colors.transparent // 路径颜色
      //   ..blendMode = BlendMode.clear // 路径模式
      //   ..strokeWidth = 3 // 路径宽度
      //   ..strokeCap = StrokeCap.round // 路径开始和结尾的样式
      //   ..style = PaintingStyle.stroke; // 路径转角处的样式

      Path path = Path();
      for (int i = 0; i < customPath.pointList.length; i++) {
        // 点如果在画布内,再绘制出来,超出画布的点不绘制
        Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
        if (rect.contains(customPath.pointList[i])) {
          if (i == 0) {
            path.moveTo(customPath.pointList[i].dx, customPath.pointList[i].dy);
          } else {
            path.lineTo(customPath.pointList[i].dx, customPath.pointList[i].dy);
          }
        }
      }

      // 采用drawPath()方法代替drawPoint()方法,这个方法内部做了优化,是一条路径一条路径来刷新的,比drawPoint()的性能要高一些
      canvas.drawPath(path, paint);
    });
    canvas.restore();
  }

  /// 重写掉系统的shouldRepaint()方法,决定要不要重新绘制画布,即要不要重新执行上面的paint()方法
  @override
  bool shouldRepaint(_DrawingBoardPainter oldDelegate) {
    return true;
  }
}

/// 每一次画的路径
class CustomPath {
  /// 该路径上的点
  List pointList = [];

  CustomPath();
}


三、手势的实例


手势盒子

UIView和CALayer的仿射变换属性主要就是两个用途:一和动画配合,二和手势配合。

iOS里的实现

手势盒子View,功能:

  • 缩放:支持捏合缩放、支持双击缩放,缩放时以捏合中心或双击点为缩放中心
  • 平移:支持放大后平移查看局部,放大后平移时不能超出可是范围的边界判断,超出边界时自动回弹
  • 无侵入:作为一个功能型视图插入到视图树中,暴露一个subview属性给外界,用来接收需要添加手势的子视图,对该子视图无侵入
------ViewController.h------

#import 

@interface ViewController : UIViewController

@end


------ViewController.m------

#import "ViewController.h"
#import "GestureBoxView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.userInteractionEnabled = YES;
    imageView.image = [UIImage imageNamed:@"test.jpg"];
    
    GestureBoxView *gestureBoxView = [[GestureBoxView alloc] init];
    gestureBoxView.frame = CGRectMake(0, 20, 414, 414);
    gestureBoxView.subview = imageView;
    [self.view addSubview:gestureBoxView];
}

@end
------GestureBoxView.h------

#import 

/// 手势盒子View,功能:
///
/// 1、缩放:支持捏合缩放、支持双击缩放,缩放时以捏合中心或双击点为缩放中心
/// 2、平移:支持放大后平移查看局部,放大后平移时不能超出可是范围的边界判断,超出边界时自动回弹
/// 3、无侵入:作为一个功能型视图插入到视图树中,暴露一个subview属性给外界,用来接收需要添加手势的子视图,对该子视图无侵入
@interface GestureBoxView : UIView

/// subview
@property (nonatomic, strong) UIView *subview;

@end


------GestureBoxView.m------

#import "GestureBoxView.h"

#define kMinScale 0.8
#define kMaxScale 2

#define kVisibleAreaWidth CGRectGetWidth(self.frame)
#define kVisibleAreaHeight CGRectGetHeight(self.frame)

// 将来这两个数减出来肯定是个负数,因为subview原大小是等于可视区域宽度的,一放大宽高肯定超过可视区域了
#define kHorizontalRange (kVisibleAreaWidth - CGRectGetWidth(self.subview.frame))
#define kVerticalRange (kVisibleAreaHeight - CGRectGetWidth(self.subview.frame))

@interface GestureBoxView () 

/// subview的初始形变
@property (nonatomic) CGAffineTransform subViewOriginalTransform;

/// subview的实时缩放比例
@property (nonatomic, assign) CGFloat subviewTotalScale;

/// subview的缩放中心
@property (nonatomic, assign) CGPoint subviewPinchCenter;

@end

@implementation GestureBoxView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.frame = frame;
        self.backgroundColor = [UIColor grayColor];
        
        // 剪裁掉放大后超出的部分
        self.clipsToBounds = YES;
    }
    return self;
}


#pragma mark - UIGestureRecognizerDelegate

/// 是否支持多个手势同时响应触摸事件,返回YES,支持,返回NO,不支持
///
/// 默认返回NO
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    if (([gestureRecognizer isMemberOfClass:[UIPinchGestureRecognizer class]] && [otherGestureRecognizer isMemberOfClass:[UIPanGestureRecognizer class]]) || ([gestureRecognizer isMemberOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isMemberOfClass:[UIPinchGestureRecognizer class]])) { // 缩放时不要让它平移,否则边界会有问题,你又得去平移结束手势那里判断scale等等,很麻烦
        return NO;
    }
    return YES;
}


#pragma mark - UIGestureRecognizer

- (void)pinch:(UIPinchGestureRecognizer *)pinchGestureRecognizer {
    CGFloat scale = pinchGestureRecognizer.scale;
    
    //-----------缩小特殊处理-----------//
    if (scale < 1.0) { // 这代表是在缩小
        if (self.subviewTotalScale < kMinScale) { // 超出最小缩放比例时:就不能再缩小了,停留在这个最小比例
            if (pinchGestureRecognizer.state == UIGestureRecognizerStateEnded) { // 一旦手势结束了、这里肯定是缩小手势,就把subview复位
                [self resetSubviewScale:pinchGestureRecognizer];
            }
            return;
        }
    }
    
    // 未超出最小缩放比例时:一旦手势结束了、而且还仅做了缩小手势,就把subview复位
    if (pinchGestureRecognizer.state == UIGestureRecognizerStateEnded && self.subviewTotalScale < 1.0) {
        [self resetSubviewScale:pinchGestureRecognizer];
    }
    //-----------缩小特殊处理-----------//
    
    
    //-----------放大特殊处理-----------//
    if (scale > 1.0) { // 这代表是在放大
        if (self.subviewTotalScale > kMaxScale) { // 超出最大缩放比例时:就不能再放大了,停留在这个最大比例
            return;
        }
    }
    //-----------放大特殊处理-----------//
    
    
    //-----------正常的缩放-----------//
    if (pinchGestureRecognizer.state == UIGestureRecognizerStateBegan) { // 初始化一下中心位置
        self.subviewPinchCenter = [pinchGestureRecognizer locationInView:pinchGestureRecognizer.view];
    }
    
    // 未超出最大最小缩放比例时:正常的缩放代码
    if (pinchGestureRecognizer.state == UIGestureRecognizerStateBegan || pinchGestureRecognizer.state == UIGestureRecognizerStateChanged) {
        // 通过scale仿射变换进行缩放
        self.subview.transform = CGAffineTransformScale(self.subview.transform, scale, scale);
        
        // 实时记录subview当前的缩放比例
        // 每次都是在上次缩放的基础上又乘以了某个比例,因为你缩放一次,可能会松开手停留在这个大小看看,然后再进行一次缩放
        // 主要用来判断是不是超出了最大或最小缩放比例
        self.subviewTotalScale *= scale;
        
        // 立马把手势的缩放比例置为1,从头开始记录手势的缩放比例
        pinchGestureRecognizer.scale = 1;
        
        // 保证缩放时以捏合中心为缩放中心
        CGPoint nowPoint = [pinchGestureRecognizer locationInView:pinchGestureRecognizer.view];
        pinchGestureRecognizer.view.transform = CGAffineTransformTranslate(pinchGestureRecognizer.view.transform, nowPoint.x - self.subviewPinchCenter.x, nowPoint.y - self.subviewPinchCenter.y);
        self.subviewPinchCenter = [pinchGestureRecognizer locationInView:pinchGestureRecognizer.view];
    }
    
    // 未超出最大最小缩放比例时:缩放结束时,也要检测下边界的,因为subview放大后、平移、然后再缩小,即便subview缩小了,也只是大小缩小了,位置有可能因为平移被搞到边界以外了
    if (pinchGestureRecognizer.state == UIGestureRecognizerStateEnded && self.subviewTotalScale >= 1.0) {
        [self handleOutLeft:pinchGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutRight:pinchGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutTop:pinchGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutBottom:pinchGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
    }
    //-----------正常的缩放-----------//
}

- (void)pan:(UIPanGestureRecognizer *)panGestureRecognizer {
    if (panGestureRecognizer.numberOfTouches >= 2) {
        return;
    }
    
    // 返回给我们一个在subview坐标系中的点,这个点包含的信息就是:手指在坐标系中相对于开始点的偏移量
    CGPoint offset = [panGestureRecognizer translationInView:self.subview];
    
    // 视图如果已经在最左边了,这时你还要往左滑,禁止左滑
    if (CGRectGetMinX(self.subview.frame) <= kHorizontalRange && offset.x <= 0) {
        // 如果你很猛地平移视图到边界处,视图有可能冲出边界的,所以这里要做下异常处理
        [self handleOutLeft:panGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        
        // 同时视图如果已经在最上边了,这时你还要往上滑,禁止上滑
        if (CGRectGetMinY(self.subview.frame) <= kVerticalRange && offset.y <= 0) {
            return;
        }
        
        // 同时视图如果已经在最下边了,这时你还要往下滑,禁止下滑
        if (CGRectGetMinY(self.subview.frame) >= 0 && offset.y >= 0) {
            return;
        }
        
        // 此时你可以上下滑动
        self.subview.transform = CGAffineTransformTranslate(self.subview.transform, 0, offset.y);
        [panGestureRecognizer setTranslation:CGPointZero inView:self.subview];
        
        return;
    }
    
    // 视图如果已经在最右边了,这时你还要往右滑,禁止右滑
    if (CGRectGetMinX(self.subview.frame) >= 0 && offset.x >= 0) {
        // 如果你很猛地平移视图到边界处,视图有可能冲出屏幕的,所以这里要做下异常处理
        [self handleOutRight:panGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        
        // 同时视图如果已经在最上边了,这时你还要往上滑,禁止上滑
        if (CGRectGetMinY(self.subview.frame) <= kVerticalRange && offset.y <= 0) {
            return;
        }
        
        // 同时视图如果已经在最下边了,这时你还要往下滑,禁止下滑
        if (CGRectGetMinY(self.subview.frame) >= 0 && offset.y >= 0) {
            return;
        }
        
        // 此时你可以上下滑动
        self.subview.transform = CGAffineTransformTranslate(self.subview.transform, 0, offset.y);
        [panGestureRecognizer setTranslation:CGPointZero inView:self.subview];
        
        return;
    }

    // 视图如果已经在最上边了,这时你还要往上滑,禁止上滑
    if (CGRectGetMinY(self.subview.frame) <= kVerticalRange && offset.y <= 0) {

        // 如果你很猛地平移视图到边界处,视图有可能冲出屏幕的,所以这里要做下异常处理
        [self handleOutTop:panGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        
        // 同时视图如果已经在最左边了,这时你还要往左滑,禁止左滑
        if (CGRectGetMinX(self.subview.frame) <= kHorizontalRange && offset.x <= 0) {
            return;
        }
        
        // 同时视图如果已经在最右边了,这时你还要往右滑,禁止右滑
        if (CGRectGetMinX(self.subview.frame) >= 0 && offset.x >= 0) {
            return;
        }
        
        // 此时你可以左右滑动
        self.subview.transform = CGAffineTransformTranslate(self.subview.transform, offset.x, 0);
        [panGestureRecognizer setTranslation:CGPointZero inView:self.subview];

        return;
    }

    // 视图如果已经在最下边了,这时你还要往下滑,禁止下滑
    if (CGRectGetMinY(self.subview.frame) >= 0 && offset.y >= 0) {
        
        // 如果你很猛地平移视图到边界处,视图有可能冲出屏幕的,所以这里要做下异常处理
        [self handleOutBottom:panGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        
        // 同时视图如果已经在最左边了,这时你还要往左滑,禁止左滑
        if (CGRectGetMinX(self.subview.frame) <= kHorizontalRange && offset.x <= 0) {
            return;
        }
        
        // 同时视图如果已经在最右边了,这时你还要往右滑,禁止右滑
        if (CGRectGetMinX(self.subview.frame) >= 0 && offset.x >= 0) {
            return;
        }
        
        // 此时你可以左右滑动
        self.subview.transform = CGAffineTransformTranslate(self.subview.transform, offset.x, 0);
        [panGestureRecognizer setTranslation:CGPointZero inView:self.subview];

        return;
    }
    
    // 在边界范围内,上下左右可以自由滑动
    // 通过translate仿射变换进行平移
    self.subview.transform = CGAffineTransformTranslate(self.subview.transform, offset.x, offset.y);
    
    // 将增量置为零,从头开始记录手指在坐标系中相对于开始点的偏移量
    [panGestureRecognizer setTranslation:CGPointZero inView:self.subview];
}

- (void)tap:(UITapGestureRecognizer *)tapGestureRecognizer {
    if (self.subviewTotalScale > 1.0) {// subview处于放大状态,双击复位
        [self resetSubviewScale:tapGestureRecognizer];
    } else {// subview处于正常状态,双击放大至最大
        // 返回我们点击的点,一定要在self坐标系里,因为我们只看这一屏的内容,如果在subview坐标系里,看不见的地方也是算坐标的,这就不对了
        CGPoint point = [tapGestureRecognizer locationInView:self];
                
        [UIView animateWithDuration:0.25 animations:^{
            // 初始化一个什么都不做的变换
            CGAffineTransform transform = CGAffineTransformIdentity;
            transform = CGAffineTransformScale(transform, kMaxScale, kMaxScale);
            
            // 视图中心点的坐标
            CGFloat centerX = kVisibleAreaWidth / 2.0;
            CGFloat centerY = kVisibleAreaHeight / 2.0;
            
            // 点击点的坐标相对于中心点的偏移量
            CGFloat sourceOffsetX = point.x - centerX;
            CGFloat sourceOffsetY = point.y - centerY;
            
            // 放大后点击点的坐标相对于中心点的偏移量
            CGFloat scaledOffsetX = sourceOffsetX * kMaxScale;
            CGFloat scaledOffsetY = sourceOffsetY * kMaxScale;
            
            // 两个点坐标的偏移量
            CGFloat twoPointOffsetX = sourceOffsetX - scaledOffsetX;
            CGFloat twoPointOffsetY = sourceOffsetY - scaledOffsetY;
            
            transform = CGAffineTransformTranslate(transform, twoPointOffsetX, twoPointOffsetY);
            
            self.subview.transform = transform;
        }];
        
        self.subviewTotalScale = kMaxScale;
        
        [self handleOutLeft:tapGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutRight:tapGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutTop:tapGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
        [self handleOutBottom:tapGestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
    }
}


#pragma mark - private method

/// subview的缩放效果复位
- (void)resetSubviewScale:(UIGestureRecognizer *)gestureRecognizer {
    [UIView animateWithDuration:0.25 animations:^{
        // 用这个方式让subview的缩放效果复位
        self.subview.transform = self.subViewOriginalTransform;
        // 不要用这种方式让subview的缩放效果复位
        // 因为subview放大并平移后,用这样的复位方式后,再次缩放的中心点就不是屏幕中心了,而是某个奇怪的点,很不舒服
//        self.subview.transform = CGAffineTransformScale(self.subview.transform, 1.0 / self.totalScale, 1.0 / self.totalScale);
    }];
    self.subviewTotalScale = 1.0;
    
    // 复位后,也要检测下边界的,因为subview放大后、平移、然后再缩小,即便subview复位了,也只是大小复位了,位置有可能因为平移被搞到边界以外了
    [self handleOutLeft:gestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
    [self handleOutRight:gestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
    [self handleOutTop:gestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
    [self handleOutBottom:gestureRecognizer horizontalRange:kHorizontalRange verticalRange:kVerticalRange];
}

/// 左边界异常处理
- (void)handleOutLeft:(UIGestureRecognizer *)recognizer horizontalRange:(CGFloat)horizontalRange verticalRange:(CGFloat)verticalRange {
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self resetOutLeft:horizontalRange];
        [self resetOutTop:verticalRange];
        [self resetOutBottom:verticalRange];
    }
}

/// 右边界异常处理
- (void)handleOutRight:(UIGestureRecognizer *)recognizer horizontalRange:(CGFloat)horizontalRange verticalRange:(CGFloat)verticalRange {
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self resetOutRight:horizontalRange];
        [self resetOutTop:verticalRange];
        [self resetOutBottom:verticalRange];
    }
}

/// 上边界异常处理
- (void)handleOutTop:(UIGestureRecognizer *)recognizer horizontalRange:(CGFloat)horizontalRange verticalRange:(CGFloat)verticalRange {
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self resetOutTop:verticalRange];
        [self resetOutLeft:horizontalRange];
        [self resetOutRight:horizontalRange];
    }
}

/// 下边界异常处理
- (void)handleOutBottom:(UIGestureRecognizer *)recognizer horizontalRange:(CGFloat)horizontalRange verticalRange:(CGFloat)verticalRange {
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self resetOutBottom:verticalRange];
        [self resetOutLeft:horizontalRange];
        [self resetOutRight:horizontalRange];
    }
}

/// 冲出左边界了,置位
- (void)resetOutLeft:(CGFloat)horizontalRange {
    if (CGRectGetMinX(self.subview.frame) <= horizontalRange) {
        [UIView animateWithDuration:0.25 animations:^{
            self.subview.frame = CGRectMake(horizontalRange, CGRectGetMinY(self.subview.frame), CGRectGetWidth(self.subview.frame), CGRectGetHeight(self.subview.frame));
        }];
    }
}

/// 冲出右边界了,置位
- (void)resetOutRight:(CGFloat)horizontalRange {
    if (CGRectGetMinX(self.subview.frame) >= 0) {
        [UIView animateWithDuration:0.25 animations:^{
            self.subview.frame = CGRectMake(0, CGRectGetMinY(self.subview.frame), CGRectGetWidth(self.subview.frame), CGRectGetHeight(self.subview.frame));
        }];
    }
}

/// 冲出上边界了,置位
- (void)resetOutTop:(CGFloat)verticalRange {
    if (CGRectGetMinY(self.subview.frame) <= verticalRange) {
        [UIView animateWithDuration:0.25 animations:^{
            self.subview.frame = CGRectMake(CGRectGetMinX(self.subview.frame), verticalRange, CGRectGetWidth(self.subview.frame), CGRectGetHeight(self.subview.frame));
        }];
    }
}

/// 冲出下边界了,置位
- (void)resetOutBottom:(CGFloat)verticalRange {
    if (CGRectGetMinY(self.subview.frame) >= 0) {
        [UIView animateWithDuration:0.25 animations:^{
            self.subview.frame = CGRectMake(CGRectGetMinX(self.subview.frame), 0, CGRectGetWidth(self.subview.frame), CGRectGetHeight(self.subview.frame));
        }];
    }
}


#pragma mark - setter, getter

- (void)setSubview:(UIView *)subview {
    _subview = subview;
    _subview.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
    
    self.subViewOriginalTransform = _subview.transform;
    self.subviewTotalScale = 1.0;
    
    // 缩放手势
    UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
    pinchGestureRecognizer.delegate = self;
    [_subview addGestureRecognizer:pinchGestureRecognizer];
    
    // 平移手势
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    panGestureRecognizer.delegate = self;
    [_subview addGestureRecognizer:panGestureRecognizer];
    
    // 双击手势
    UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    tapGestureRecognizer.numberOfTapsRequired = 2;
    tapGestureRecognizer.delegate = self;
    [_subview addGestureRecognizer:tapGestureRecognizer];
    
    [self addSubview:_subview];
}

@end
Flutter里的实现

手势盒子View,功能:

  • 缩放:支持捏合缩放、支持双击缩放,缩放时以捏合中心或双击点为缩放中心
  • 平移:平移:支持放大后平移查看局部,放大后平移时不能超出可视范围的边界判断,超出边界时自动回弹
  • 旋转:支持旋转
  • 无侵入:作为一个功能型父Widget插入到Widget树中,暴露一个child属性给外界,用来接收需要添加手势的子Widget,对该子Widget无侵入
------main.dart------

import 'package:flutter/material.dart';

import 'package:flutter_app/gesture_box_widget.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      alignment: Alignment.topLeft,
      child: GestureBoxWidget(
        size: Size(414, 414),
        child: Image.network("https://picsum.photos/414"),
      ),
    );
  }
}
------gesture_box_widget.dart------

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';

/// 手势盒子Widget,功能:
///
/// 1、缩放:支持捏合缩放、支持双击缩放,缩放时以捏合中心或双击点为缩放中心
/// 2、平移:支持放大后平移查看局部,放大后平移时不能超出可视范围的边界判断,超出边界时自动回弹
/// 3、旋转:支持旋转
/// 4、无侵入:作为一个功能型父Widget插入到Widget树中,暴露一个child属性给外界,用来接收需要添加手势的子Widget,对该子Widget无侵入
///
/// ```dart
/// final GlobalKey _globalKey = GlobalKey();
///
/// GestureBoxWidget(
///   key: _globalKey,
///   size: Size(400, 300),
///   child: Image.asset(
///     "lib/asset/image/db.jpeg",
///   ),
/// ),
///
/// onTap: () {
///   _globalKey.currentState.rotate();
/// },
/// ```
class GestureBoxWidget extends StatefulWidget {
  /// 手势盒子Widget的大小
  ///
  /// child放大后平移时不会超出这个大小的矩形区域
  final Size size;

  /// 手势盒子Widget的背景色
  final Color bgColor;

  /// 最小缩放比例
  final double minScale;

  /// 最大缩放比例
  final double maxScale;

  /// 缩放比例改变的回调
  final Function(double scale)? scaleDidChange;

  /// 放大后平移时自动回弹的最大内边距
  final double maxDragOver;

  /// 动画时长
  final Duration duration;

  /// 是否允许捏合缩放
  final bool enablePinchScale;

  /// 是否允许双击缩放
  final bool enableDoubleTapScale;

  /// 是否允许旋转
  final bool enableRotate;

  /// child
  final Widget child;

  /// child的比例
  ///
  /// child旋转后可能需要调整比例
  final double childScale;

  /// child旋转过的角度
  final double childAngle;

  GestureBoxWidget({
    Key? key,
    required this.size,
    this.bgColor = Colors.grey,
    this.minScale = 0.8,
    this.maxScale = 2.0,
    this.scaleDidChange,
    this.maxDragOver = 50,
    this.duration = const Duration(milliseconds: 250),
    this.enablePinchScale = true,
    this.enableDoubleTapScale = true,
    this.enableRotate = true,
    required this.child,
    this.childScale = 1.0,
    this.childAngle = 0,
  })  : assert(size.width > 0 && size.height > 0),
        assert(minScale > 0 &&
            minScale < 1 &&
            maxScale >= 1.0 &&
            maxScale <= 10.0),
        assert(maxDragOver >= 0 && maxDragOver <= size.width),
        super(key: key);

  @override
  GestureBoxWidgetState createState() => GestureBoxWidgetState();
}

class GestureBoxWidgetState extends State
    with TickerProviderStateMixin {
  /// 是否正在缩放
  ///
  /// Pan and scale callbacks cannot be used simultaneous because scale is a superset of pan. Simply use the scale callbacks instead.
  bool _isScaling = false;

  /// 是否正在平移
  ///
  /// Pan and scale callbacks cannot be used simultaneous because scale is a superset of pan. Simply use the scale callbacks instead.
  bool _isPanning = false;

  /// 当前缩放比例
  double _curScale = 1.0;

  /// 上一次缩放数据
  ///
  /// 停止缩放后,Widget的scale变化了,可能会基于这次缩放继续缩放,所以搞这么个属性来记录上一次缩放的数据,以便求缩放增量
  ScaleUpdateDetails? _lastScaleUpdateDetails;

  /// 缩放前后某个点的偏移量
  ///
  /// 以便平移来保证缩放中心
  Offset _curOffset = Offset.zero;

  /// 双击的位置
  Offset _doubleTapPosition = Offset.zero;

  /// 缩放动画控制器
  AnimationController? _scaleAnimationController;

  /// 平移动画控制器
  AnimationController? _translateAnimationController;

  /// 是否顺时针
  bool _clockwise = true;

  /// 一次旋转的角度
  double _angle = 0;

  @override
  void dispose() {
    _scaleAnimationController?.dispose();
    _translateAnimationController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _angle = widget.childAngle;

    return ClipRect(
      // 切掉图片放大后超出的部分
      child: Container(
        color: widget.bgColor,
        width: widget.size.width,
        height: widget.size.height,
        child: Transform.scale(
          scale: widget.childScale,
          child: Transform.rotate(
            // 旋转
            angle: widget.enableRotate ? (_clockwise ? _angle : -_angle) : 0,
            child: Transform(
              // 缩放、平移
              alignment: Alignment.center,
              transformHitTests: true,
              transform: Matrix4.identity()
                ..translate(_curOffset.dx, _curOffset.dy)
                ..scale(_curScale, _curScale, _curScale),
              child: Listener(
                onPointerUp: _onPointerUp,
                child: GestureDetector(
                  behavior: HitTestBehavior.translucent,
                  onScaleStart: _onScaleStart,
                  onScaleUpdate: _onScaleUpdate,
                  onScaleEnd: _onScaleEnd,
                  onDoubleTap: _onDoubleTap,
                  child: Container(
                    color: Colors.black,
                    alignment: Alignment.center,
                    child: widget.child,
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  /// 开始缩放
  void _onScaleStart(ScaleStartDetails details) {
    if (!widget.enablePinchScale) {
      return;
    }

    _isScaling = false;
    _isPanning = false;

    _lastScaleUpdateDetails = null;
    _scaleAnimationController?.stop();
    _translateAnimationController?.stop();
  }

  /// 缩放中
  void _onScaleUpdate(ScaleUpdateDetails details) {
    if (!widget.enablePinchScale) {
      return;
    }

    setState(() {
      if (details.scale != 1.0) {
        // 缩放中
        _executeScaling(details);
      } else {
        // 平移中
        _executePanning(details);
      }
    });
  }

  /// 执行缩放
  void _executeScaling(ScaleUpdateDetails details) {
    if (details.pointerCount < 2 && _isPanning) {
      return;
    }
    _isScaling = true;
    if (_lastScaleUpdateDetails == null) {
      _lastScaleUpdateDetails = details;
      return;
    }

    // ------------ 计算缩放比例 ------------ //
    // scaleIncrement缩放增量:scaleIncrement > 0,代表在放大;scaleIncrement < 0,代表在缩小
    double scaleIncrement = details.scale - _lastScaleUpdateDetails!.scale;
    if (scaleIncrement < 0 && _curScale < widget.minScale) {
      // 在缩小,并且缩小到了比最小比例还小,就不要增量了,交给缩放结束后后搞个动画
      scaleIncrement = 0;
    }
    if (scaleIncrement > 0 && _curScale > widget.maxScale) {
      // 在放大,并且放大到了比最大比例还大,就不要增量了,交给缩放结束后后搞个动画
      scaleIncrement = 0;
    }
    // 正常的缩放,缩放比例就是:_curScale = _curScale + scaleIncrement
    _curScale = max(_curScale + scaleIncrement, 0.0);

    // 缩放过程中实时更新上一次缩放的数据
    _lastScaleUpdateDetails = details;
    // ------------ 计算缩放比例 ------------ //

    // ------------ 处理缩放中心 ------------ //
    // 计算缩放后偏移前(缩放前后的内容中心对齐)的左上角坐标变化
    double scaleOffsetX = context.size!.width * (_curScale - 1.0) / 2;
    double scaleOffsetY = context.size!.height * (_curScale - 1.0) / 2;
    // 将缩放前的触摸点映射到缩放后的内容上
    double scalePointDX =
        (details.localFocalPoint.dx + scaleOffsetX - _curOffset.dx) / _curScale;
    double scalePointDY =
        (details.localFocalPoint.dy + scaleOffsetY - _curOffset.dy) / _curScale;
    // 计算偏移量
    _curOffset += Offset(
      (context.size!.width / 2 - scalePointDX) * scaleIncrement,
      (context.size!.height / 2 - scalePointDY) * scaleIncrement,
    );
    // ------------ 处理缩放中心 ------------ //
  }

  /// 执行平移
  void _executePanning(ScaleUpdateDetails details) {
    if (details.pointerCount >= 2 && _isScaling) {
      return;
    }
    _isPanning = true;
    if (_lastScaleUpdateDetails == null) {
      _lastScaleUpdateDetails = details;
      return;
    }

    // ------------ 计算平移增量、处理边界 ------------ //
    // localFocalPoint为触摸屏幕的点;X轴、Y轴的平移增量
    double offsetXIncrement = (details.localFocalPoint.dx -
            _lastScaleUpdateDetails!.localFocalPoint.dx) *
        _curScale;
    double offsetYIncrement = (details.localFocalPoint.dy -
            _lastScaleUpdateDetails!.localFocalPoint.dy) *
        _curScale;

    // 处理X轴边界
    double scaleOffsetX = context.size!.width * (_curScale - 1.0) / 2;
    if (scaleOffsetX <= 0) {
      offsetXIncrement = 0;
    } else if (_curOffset.dx > scaleOffsetX) {
      offsetXIncrement *=
          (widget.maxDragOver - (_curOffset.dx - scaleOffsetX)) /
              widget.maxDragOver;
    } else if (_curOffset.dx < -scaleOffsetX) {
      offsetXIncrement *=
          (widget.maxDragOver - (-scaleOffsetX - _curOffset.dx)) /
              widget.maxDragOver;
    }
    // 处理Y轴边界
    double scaleOffsetY =
        (context.size!.height * _curScale - widget.size.height) / 2;
    if (scaleOffsetY <= 0) {
      offsetYIncrement = 0;
    } else if (_curOffset.dy > scaleOffsetY) {
      offsetYIncrement *=
          (widget.maxDragOver - (_curOffset.dy - scaleOffsetY)) /
              widget.maxDragOver;
    } else if (_curOffset.dy < -scaleOffsetY) {
      offsetYIncrement *=
          (widget.maxDragOver - (-scaleOffsetY - _curOffset.dy)) /
              widget.maxDragOver;
    }

    _curOffset += Offset(offsetXIncrement, offsetYIncrement);

    _lastScaleUpdateDetails = details;
    // ------------ 计算平移增量、处理边界 ------------ //
  }

  /// 缩放结束/平移结束
  void _onScaleEnd(ScaleEndDetails details) {
    if (!widget.enablePinchScale) {
      return;
    }

    if (_curScale < 1.0) {
      // 缩小操作的话,缩放结束后恢复原大小
      _animationScale(1.0);
    } else if (_curScale > widget.maxScale) {
      // 放大的话,放大到了比最大比例还大,则置为最大比例
      _animationScale(widget.maxScale);
    }

    if (_curScale <= 1.0) {
      // 缩小操作的话,缩放结束后修改偏移值,使内容居中
      _animationOffset(Offset.zero);
    } else if (_isPanning) {
      // 平移时超出可视范围,自动回弹到边界
      double realScale =
          _curScale > widget.maxScale ? widget.maxScale : _curScale;
      double targetOffsetX = _curOffset.dx, targetOffsetY = _curOffset.dy;
      // 处理X轴边界
      double scaleOffsetX = context.size!.width * (realScale - 1.0) / 2;
      if (scaleOffsetX <= 0) {
        targetOffsetX = 0;
      } else if (_curOffset.dx > scaleOffsetX) {
        targetOffsetX = scaleOffsetX;
      } else if (_curOffset.dx < -scaleOffsetX) {
        targetOffsetX = -scaleOffsetX;
      }
      // 处理Y轴边界
      double scaleOffsetY =
          (context.size!.height * realScale - widget.size.height) / 2;
      if (scaleOffsetY < 0) {
        targetOffsetY = 0;
      } else if (_curOffset.dy > scaleOffsetY) {
        targetOffsetY = scaleOffsetY;
      } else if (_curOffset.dy < -scaleOffsetY) {
        targetOffsetY = -scaleOffsetY;
      }
      if (_curOffset.dx != targetOffsetX || _curOffset.dy != targetOffsetY) {
        // 自动回弹
        _animationOffset(Offset(targetOffsetX, targetOffsetY));
      } else {
        // 处理X轴边界
        double duration =
            (widget.duration.inSeconds + widget.duration.inMilliseconds / 1000);
        Offset targetOffset =
            _curOffset + details.velocity.pixelsPerSecond * duration;
        targetOffsetX = targetOffset.dx;
        if (targetOffsetX > scaleOffsetX) {
          targetOffsetX = scaleOffsetX;
        } else if (targetOffsetX < -scaleOffsetX) {
          targetOffsetX = -scaleOffsetX;
        }
        // 处理X轴边界
        targetOffsetY = targetOffset.dy;
        if (targetOffsetY > scaleOffsetY) {
          targetOffsetY = scaleOffsetY;
        } else if (targetOffsetY < -scaleOffsetY) {
          targetOffsetY = -scaleOffsetY;
        }
        // 启动惯性滚动
        // _animationOffset(Offset(targetOffsetX, targetOffsetY));
      }
    }

    if (widget.scaleDidChange != null) {
      widget.scaleDidChange!(_curScale);
    }

    _isScaling = false;
    _isPanning = false;
    _lastScaleUpdateDetails = null;
  }

  /// 手指抬起事件
  ///
  /// 获取双击点的位置
  void _onPointerUp(PointerUpEvent event) {
    if (!widget.enableDoubleTapScale) {
      return;
    }

    _doubleTapPosition = event.localPosition;
  }

  /// 双击了
  void _onDoubleTap() {
    if (!widget.enableDoubleTapScale) {
      return;
    }

    double targetScale = _curScale == 1.0 ? widget.maxScale : 1.0;
    _animationScale(targetScale);
    if (targetScale == 1.0) {
      _animationOffset(Offset.zero);
    }
  }

  /// 搞个动画把Widget搞到指定的[targetScale]
  void _animationScale(double targetScale) {
    _scaleAnimationController?.dispose();
    _scaleAnimationController =
        AnimationController(vsync: this, duration: widget.duration);
    Animation animation = Tween(begin: _curScale, end: targetScale)
        .animate(_scaleAnimationController!);
    animation.addListener(() {
      setState(() {
        _executeScaling(ScaleUpdateDetails(
          focalPoint: _doubleTapPosition,
          localFocalPoint: _doubleTapPosition,
          scale: animation.value,
          horizontalScale: animation.value,
          verticalScale: animation.value,
        ));
      });
    });
    _scaleAnimationController?.forward();

    if (widget.scaleDidChange != null) {
      widget.scaleDidChange!(targetScale);
    }
  }

  /// 搞个动画把Widget偏移指定的[targetOffset]
  void _animationOffset(Offset targetOffset) {
    _translateAnimationController?.dispose();
    _translateAnimationController =
        AnimationController(vsync: this, duration: widget.duration);
    Animation animation = _translateAnimationController!
        .drive(Tween(begin: _curOffset, end: targetOffset));
    animation.addListener(() {
      setState(() {
        _curOffset = animation.value;
      });
    });
    _translateAnimationController?.forward();
  }

  /// 旋转
  ///
  /// [clockwise]: 是否顺时针
  /// [angle]: 一次旋转的角度
  void rotate({bool clockwise = true, double angle = pi / 2}) {
    if (!mounted) {
      return;
    }

    setState(() {
      _clockwise = clockwise;
      _angle += angle;
    });
  }

  /// 恢复原始大小
  void resetScale() {
    if (!mounted) {
      return;
    }

    if (_curScale != 1) {
      _animationScale(1);
      _animationOffset(Offset.zero);
    }
  }
}


四、一些手势冲突处理的实例


1、画板放进手势盒子里

iOS里的实现

当我们把画板放进手势盒子里,会发现画板的touchesMove绘制笔迹事件和手势盒子添加给画板放大后的平移手势存在冲突,冲突的现象是画板放大后我想绘制笔迹,但是画板却响应了平移事件,而没有不能响应原始指针事件绘制不出笔迹,冲突的原因就是平移手势比touchesMove事件的优先级高。

处理的办法有多种,此处的办法是让两者完全不要共存,即选中画笔的情况下,代表是要绘制笔迹了,那就禁掉平移手势,这样touchesMove就能第一顺位响应事件从而绘制笔迹了;而当未选中画笔的情况下,代表没有绘制笔迹的可能性,那就禁掉touchesMove,这样平移手势在响应事件时就不会带出那么一小点笔迹。

给画板View添加一个是否允许绘制的属性:

/// 是否允许绘制
@property (nonatomic, assign) BOOL enableDraw;

- (void)setEnableDraw:(BOOL)enableDraw {
    _enableDraw = enableDraw;
}

然后在画板Viewtouches...方法里都做一下是否允许绘制的判断即可:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (!self.enableDraw) {
        return;
    }
    
    // ...
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if (!self.enableDraw) {
        return;
    }
    
    // ...
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (!self.enableDraw) {
        return;
    }
    
    // ...
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (!self.enableDraw) {
        return;
    }
    
    // ...
}

给手势盒子View添加一个是否允许手势的属性,然后根据这个属性来决定是否启用手势即可:

/// 是否允许手势
@property (nonatomic, assign) BOOL enableGesture;

- (void)setEnableGesture:(BOOL)enableGesture {
    _enableGesture = enableGesture;
    
    self.pinchGestureRecognizer.enabled = _enableGesture;
    self.panGestureRecognizer.enabled = _enableGesture;
    self.tapGestureRecognizer.enabled = _enableGesture;
}

然后在选中画笔和取消画笔的方法里做如下操作:

/// 选中画笔
- (IBAction)penSelected:(id)sender {
    // 可以绘制
    self.drawingBoardView.enableDraw = YES;
    // 禁掉手势
    self.gestureBoxView.enableGesture = NO;
}

/// 取消画笔
- (IBAction)penCanceled:(id)sender {
    // 禁掉绘制
    self.drawingBoardView.enableDraw = NO;
    // 可以手势
    self.gestureBoxView.enableGesture = YES;
}
Flutter里的实现

当我们把画板放进手势盒子里,会发现画板的onPointerMove绘制笔迹事件和手势盒子添加给画板放大后的平移手势存在冲突,冲突的现象是画板放大后我想绘制笔迹,笔迹倒是绘制出来了,但画板同时也响应了平移事件,这和iOS里冲突的现象不一样,冲突的原因就是Listener和手势都会接收到事件并响应。

处理的办法有多种,此处的办法也是让两者完全不要共存。

2、画板放进UITableView

iOS里的实现

当我们把画板放进UITableView里,会发现画板的touchesMove绘制笔迹事件和UITableView的滚动事件存在冲突,冲突的现象是当我们想在画板上绘制笔迹时发现绘制不出笔迹,直接拖着UITableView跑了,冲突的原因还是手势比touchesMove事件的优先级高,只不过这个地方是父视图UITableView的手势抢到了优先响应权,而不是画板自己身上的手势抢到了优先响应权(我们知道UIScrollView自带着一个panGestureRecognizerUIScrollView的实现原理其实就是在平移手势的回调里实时更新偏移量,从而达到滚动的效果)。

处理的办法有多种,此处的办法还是让两者完全不要共存,即当我们的手指落在画板内时,禁掉UITableView的滚动——也就是禁掉UITableView的平移手势,这样touchesMove就能第一顺位响应事件从而绘制笔迹了,当我们的手指从画板上抬起时,再恢复UITableView的滚动;而当手指落在画板外时,什么都不做,继续让UITableView滚动即可。

给画板View添加一个手指落在画板内的回调属性和一个手指从画板上抬起的回调属性:

/// 手指落在画板内的回调
@property (nonatomic, copy) void (^onPointerDown)(void);

/// 手指从画板上抬起的回调
@property (nonatomic, copy) void (^onPointerUp)(void);

然后在画板Viewtouches...方法里调用一下这两个回调即可:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerDown != nil) {
        self.onPointerDown();
    }

    // ...
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerUp != nil) {
        self.onPointerUp();
    }
    
    // ...
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerUp != nil) {
        self.onPointerUp();
    }
    
    // ...
}

然后在创建画板View的地方做如下操作:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellId" forIndexPath:indexPath];
    
    self.drawingBoardView = [[DrawingBoardView alloc] init];
    self.drawingBoardView.backgroundColor = [UIColor blackColor];
    self.drawingBoardView.frame = CGRectMake(0, 0, 414, 414);
    __weak typeof(self) weakSelf = self;
    self.drawingBoardView.onPointerDown = ^{
        weakSelf.tableView.scrollEnabled = NO;
    };
    self.drawingBoardView.onPointerUp = ^{
        weakSelf.tableView.scrollEnabled = YES;
    };
    [cell.contentView addSubview:self.drawingBoardView];
    
    return cell;
}

这样做基本就可以了,但是当我们的手指在画板View上做一个类似轻扫的操作时,UITableView直接就滚动起来了,根本就没机会让画板View识别到touchesBegan,这种情况下就没法触发我们的回调了。于是我们还得把拦截往前推来加固一下,那就是画板ViewhitTest方法,无论我们在画板View上做什么操作,都会触发这个方法的:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) { // 如果触摸的点在自己范围内,那就让自己作为响应者
        self.tableView.scrollEnabled = NO;
        return self;
    } else {  // 否则就采用系统默认的响应方式
        self.tableView.scrollEnabled = YES;
        return [super hitTest:point withEvent:event];
    }
}

- (UITableView *)tableView {
    UIView *tableView = self.superview;
    while (![tableView isKindOfClass:[UITableView class]] && tableView) {
        tableView = tableView.superview;
    }
    return (UITableView *)tableView;
}
Flutter里的实现

冲突现象、冲突原因和解决办法同1。

3、手势盒子放进UITableView

iOS里的实现

当我们把手势盒子放进UITableView里,会发现手势盒子subview放大后的平移手势和UITableView的滚动事件存在冲突,冲突的现象是它们都响应了事件,冲突的原因是window会把触摸事件取出UIEvent.UITouch.gestureRecognizers数组里的各个手势识别器,UITableView和手势盒子subview放大后的平移手势都识别并响应了事件。

处理的办法有多种,此处的办法还是让两者完全不要共存,即当我们的手指落在手势盒子内时,禁掉UITableView的滚动——也就是禁掉UITableView的平移手势,这样手势盒子subview放大后的平移手势就能第一顺位响应事件了,当我们的手指从手势盒子上抬起时,再恢复UITableView的滚动;而当手指落在手势盒子外时,什么都不做,继续让UITableView滚动即可。

给手势盒子View添加一个手指落在手势盒子内的回调属性和一个手指从手势盒子上抬起的回调属性:

/// 手指落在手势盒子内的回调
@property (nonatomic, copy) void (^onPointerDown)(void);

/// 手指从手势盒子上抬起的回调
@property (nonatomic, copy) void (^onPointerUp)(void);

然后实现手势盒子Viewtouches...方法:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerDown != nil) {
        self.onPointerDown();
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerUp != nil) {
        self.onPointerUp();
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (self.onPointerUp != nil) {
        self.onPointerUp();
    }
}

然后在创建手势盒子View的地方做如下操作:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellId" forIndexPath:indexPath];
    
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.userInteractionEnabled = YES;
    imageView.image = [UIImage imageNamed:@"test.jpg"];
    self.gestureBoxView = [[GestureBoxView alloc] init];
    self.gestureBoxView.frame = CGRectMake(0, 0, 414, 414);
    self.gestureBoxView.subview = imageView;
    __weak typeof(self) weakSelf = self;
    self.gestureBoxView.onPointerDown = ^{
        weakSelf.tableView.scrollEnabled = NO;
    };
    self.gestureBoxView.onPointerUp = ^{
        weakSelf.tableView.scrollEnabled = YES;
    };
    [cell.contentView addSubview:self.gestureBoxView];
    
    return cell;
}

当然也需要在手势盒子ViewhitTest方法里做一下加固:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) { // 如果触摸的点在自己范围内,那就让自己的subview作为响应者
        self.tableView.scrollEnabled = NO;
        return self.subview;
    } else {  // 否则就采用系统默认的响应方式
        self.tableView.scrollEnabled = YES;
        return [super hitTest:point withEvent:event];
    }
}

- (UITableView *)tableView {
    UIView *tableView = self.superview;
    while (![tableView isKindOfClass:[UITableView class]] && tableView) {
        tableView = tableView.superview;
    }
    return (UITableView *)tableView;
}
Flutter里的实现

当我们把手势盒子放进ListView里,会发现手势盒子child放大后的平移手势和ListView的滚动事件存在冲突,冲突的现象是优先响应了ListView的平移手势,冲突的原因是ListView本质上是通过VerticalDrag手势实现的,而手势盒子里的平移手势是通过pan手势实现的,VerticalDrag手势和pan手势在竞技场里竞争时VerticalDrag手势会获胜,这是因为我们滑1个像素就会被判定为是个VerticalDrag手势,而pan手势需要滑2个像素才行。

处理的办法有多种,此处的办法还是让两者完全不要共存

4、Flutter:ListView套进bottomDialog里

当我们把ListView套进bottomDialog里,要求ListView滑动到顶部时,如果还要往下拖就把bottomDialog给下拉收回,但是会发现ListView的滑动手势和bottomDialog的下拉收回手势出现了冲突,冲突的现象是滑到ListView的顶部时,再往下拖也拖不动bottomDialog,冲突的原因一个事件已经在处理过程中了,那么这个事件结束处理事件之前,不可能再去处理别的事件。

处理的办法有多种,此处的办法是索性不要bottomDialog自带的拉回手势了,这样就不会有冲突了,然后我们再给bottomDialogchild加一个拖动实时变化高度的效果就完事了。

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  /// bottomDialog的初始高度
  double _bottomDialogInitialHeight = 368;

  /// stream上传递的是一个高度,用来改变bottomDialog的实时高度
  StreamController _streamController =
      StreamController.broadcast();

  /// 手指按下时的Y坐标
  double _pointerDownY = 0.0;

  /// listView的controller
  ScrollController _scrollController = ScrollController();

  @override
  void dispose() {
    super.dispose();

    _streamController.close();
    _scrollController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _showBottomDialog,
        child: Icon(Icons.arrow_upward),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  /// 弹出bottomDialog
  void _showBottomDialog() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      enableDrag: false,
      // 禁掉bottomDialog自带的下拉收回手势,我们自己实现
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(8),
          topRight: Radius.circular(8),
        ),
      ),
      builder: (context) {
        return StreamBuilder(
          // 因为我们弹出bottomDialog后,就算setState,bottomDialog.builder方法也不会再被触发了,这样就没办法改变bottomDialog的实时高度了
          // 所以我们用了StreamBuilder,它的stream上传递的东西只要有变化,就会实时触发StreamBuilder.builder方法,这样就可以改变bottomDialog的实时高度了
          initialData: _bottomDialogInitialHeight,
          stream: _streamController.stream,
          builder: (context, snapshot) {
            double bottomDialogCurrentHeight =
                snapshot.data ?? _bottomDialogInitialHeight;
            return Listener(
              behavior: HitTestBehavior.opaque,
              onPointerDown: (PointerDownEvent event) {
                _pointerDownY = event.position.dy + _scrollController.offset;
              },
              onPointerMove: (PointerMoveEvent event) {
                if (_scrollController.offset == 0) {
                  // listView滚动到顶部,我们还要往上滑,那么listView的offset会保持等于0,向下滑就大于0了
                  // 我们只有在listView滚动到顶部 && 还要往上滑的情况下再去改变bottomDialog的实时高度
                  // 就算ListView还在响应滑动手势,也没关系,因为Listener的move事件会穿透,照执行拖动不误
                  double distance = event.position.dy - _pointerDownY;
                  if (distance.abs() > 0) {
                    // 获取手指滑动的距离,计算bottomDialog的实时高度,并传递高度
                    double _currentHeight =
                        _bottomDialogInitialHeight - distance;

                    if (_currentHeight > _bottomDialogInitialHeight) {
                      return;
                    }
                    _streamController.sink.add(_currentHeight);
                  }
                }
              },
              onPointerUp: (PointerUpEvent event) {
                // 松手后,如果bottomDialog的高度比原来的一半还小,就消失,否则弹回去
                if (bottomDialogCurrentHeight <
                    _bottomDialogInitialHeight * 0.5) {
                  Navigator.pop(context);
                } else {
                  _streamController.sink.add(_bottomDialogInitialHeight);
                }
              },
              child: Container(
                height: bottomDialogCurrentHeight, // 改变bottomDialog的实时高度
                child: Column(
                  children: [
                    Container(
                      height: 50,
                      alignment: Alignment.center,
                      child: Text("我是底部dialog"),
                    ),
                    Container(
                      color: Colors.grey,
                      height: 0.5,
                    ),
                    Expanded(
                      child: ListView.builder(
                        controller: _scrollController,
                        physics: ClampingScrollPhysics(),
                        itemCount: 24,
                        itemBuilder: (BuildContext context, int index) {
                          return ListTile(
                            title: Text("$index"),
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            );
          },
        );
      },
    );
  }
}

5、Flutter:PageView套进PageView里

当我们把一个小PageView套进一个大PageView里,要求小PageView滑动到第一页或最后一页,还要往边界滑动时则开始滚动大PageView,会发现PageView和大PageView的滑动手势出现了冲突,冲突的现象也是滑到小PageView的边界时,再拖也是拖小PageView,拖不动大PageView,冲突的原因也是一个事件已经在处理过程中了,那么这个事件结束处理事件之前,不可能再去处理别的事件。

处理的办法有多种,此处的办法是PageView滑动到第一页还要向左滑或者小PageView滑动到最后一页还要向右滑,则禁掉小PageView的滑动,这样大PageView就能响应滑动了,向左滑还是向右滑的判断是通过给小PageView包个Listener来实现的。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({
    Key? key,
  }) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  /// 小PageView的当前下标
  int _currentIndex = 0;

  /// 手指按下时的X坐标和手指移动时的X坐标,用来判断是左滑还是右滑
  double _pointerDownX = 0.0;
  double _pointerMoveX = 0.0;

  /// 大于0代表左滑,小于0代表右滑,等于0代表没滑动
  double _deltaX = 0.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:
          _buildBigPageView(), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

  /// 大PageView
  Widget _buildBigPageView() {
    return PageView(
      allowImplicitScrolling: true,
      children: [
        Container(
          color: Colors.red,
          alignment: Alignment.center,
          child: Text("大PageView-1"),
        ),
        Container(
          child: Column(
            children: [
              Container(
                color: Colors.green,
                height: 368,
                alignment: Alignment.center,
                child: Text("大PageView-2"),
              ),
              Expanded(
                child: _buildSmallPageView(),
              )
            ],
          ),
        ),
        Container(
          color: Colors.blue,
          alignment: Alignment.center,
          child: Text("大PageView-3"),
        ),
      ],
    );
  }

  /// 小PageView
  Widget _buildSmallPageView() {
    var physics;
    if (_currentIndex == 0 && _deltaX > 0) {
      // 第一页 && 还要左滑,则禁掉滑动
      physics = NeverScrollableScrollPhysics();
    } else if (_currentIndex == 2 && _deltaX < 0) {
      // 最后一页 && 还要右滑,则禁掉滑动
      // 最后一题且切下一题,不可滑动
      physics = NeverScrollableScrollPhysics();
    } else {
      // 其余情况均可滑动
      physics = ClampingScrollPhysics();
    }

    return Listener(
      child: PageView(
        physics: physics,
        children: [
          Container(
            color: Colors.yellowAccent,
            alignment: Alignment.center,
            child: Text("小PageView-1"),
          ),
          Container(
            color: Colors.purpleAccent,
            alignment: Alignment.center,
            child: Text("小PageView-2"),
          ),
          Container(
            color: Colors.pinkAccent,
            alignment: Alignment.center,
            child: Text("小PageView-3"),
          ),
        ],
        onPageChanged: (int index) {
          _currentIndex = index;
        },
      ),
      onPointerDown: (PointerDownEvent event) {
        _pointerDownX = event.localPosition.dx;
      },
      onPointerMove: (PointerMoveEvent event) {
        _pointerMoveX = event.localPosition.dx;

        if (_deltaX == 0) {
          // 刷新小PageView的physics属性
          setState(() {
            _deltaX = _pointerMoveX - _pointerDownX;
          });
        }
      },
      onPointerUp: (PointerUpEvent event) {
        // 置位小PageView的physics属性
        setState(() {
          _pointerDownX = 0;
          _pointerMoveX = 0;
          _deltaX = 0;
        });
      },
    );
  }
}

你可能感兴趣的:(【跨平台开发Flutter】iOS和Flutter里的事件处理实例)