【Flutter】手势操作

4,985 阅读4分钟

GestureDetector组件

GestureDetector是Flutter开发中通用的手势操作组件,支持点击、双击、长按、拖拽、缩放等常用手势操作。 通过源码可以发现GestureDetector是StatelessWidget无状态组件,根据手势识别类型分为八种类型手势:TapGestureRecognizer、DoubleTapGestureRecognizer、LongPressGestureRecognizer、VerticalDragGestureRecognizer、HorizontalDragGestureRecognizer、PanGestureRecognizer、ScaleGestureRecognizer、ForcePressGestureRecognizer。

源码分析

在GestureDetector的build方法中实现RawGestureDetector将以上八种手势识别器生成结果交给RawGestureDetector操作。

build方法

return RawGestureDetector(
  gestures: gestures,
  behavior: behavior,
  excludeFromSemantics: excludeFromSemantics,
  child: child,
);

RawGestureDetector

RawGestureDetector是有状态组件,主要看RawGestureDetectorState的实现。initState方法中_syncAll主要对手势识别器进行初始化。

@override
void initState() {
  super.initState();
  _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
  _syncAll(widget.gestures);
}

再看build实现,手势操作监听主要由Listener的onPointerDown监听控制。Listener可以理解为是GestureDetector元祖。

@override
Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if (!widget.excludeFromSemantics)
    result = _GestureSemantics(
      child: result,
      assignSemantics: _updateSemanticsForRenderObject,
    );
  return result;
}

_handlePointerDown方法中将手势点击事件下发到各个手势识别器当中

void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (GestureRecognizer recognizer in _recognizers.values)
    recognizer.addPointer(event);
}

调用识别器的addPointer方法,addAllowedPointer在各个Recognizer的实现可能不同,这里举例BaseTapGestureRecognizer。将event赋值给_down后等待_deadline定时器操作,定时时间到后调用_checkDown确认是否有点击

void addPointer(PointerDownEvent event) {
  _pointerToKind[event.pointer] = event.kind;
  if (isPointerAllowed(event)) {
    addAllowedPointer(event);
  } else {
    handleNonAllowedPointer(event);
  }
}
/// TapGestureRecognizer对Recognizer的addAllowedPointer实现
@override
void addAllowedPointer(PointerDownEvent event) {
  if (state == GestureRecognizerState.ready) {
    // `_down` must be assigned in this method instead of `handlePrimaryPointer`,
    // because `acceptGesture` might be called before `handlePrimaryPointer`,
    // which relies on `_down` to call `handleTapDown`.
    _down = event;
  }
  super.addAllowedPointer(event);
}

接着调用handleTapDown方法执行onTapDown或是onSecondaryTapDown方法回调给上层。

@protected
@override
void handleTapDown({PointerDownEvent down}) {
  final TapDownDetails details = TapDownDetails(
    globalPosition: down.position,
    localPosition: down.localPosition,
    kind: getKindForPointer(down.pointer),
  );
  switch (down.buttons) {
    case kPrimaryButton:
      if (onTapDown != null)
        invokeCallback<void>('onTapDown', () => onTapDown(details));
      break;
    case kSecondaryButton:
      if (onSecondaryTapDown != null)
        invokeCallback<void>('onSecondaryTapDown',
          () => onSecondaryTapDown(details));
      break;
    default:
  }
}

手势操作

点击(Tap)& 双击(DoubleTap)& 长按(LongPress)

通用的操作方法,例如点击、双击、长按功能,只需要编写Function就能实现功能。当然在这些操作过程中也具备中间过程的状态监听,例如点击按下时,抬起时,取消点击等。

GestureDetector(
  onTap: (){
    showSnack(context, "OnTap");
  },
  onDoubleTap: (){
    showSnack(context, "onDoubleTap");
  },
  onLongPress: (){
    showSnack(context, "onLongPress");
  },
  child: Transform.translate(
    offset: offset,
    child: Text(
      "Transform",
      style: TextStyle(fontSize: 25),
    ),
  ),
);

拖拽(Drag)

GestureDetector为拖拽功能提供水平方向拖拽(Horizontal drag)、垂直方向拖拽(Vertical drag)、同时支持两个方向拖拽(Pan)。 垂直方向拖拽

GestureDetector(
  child: Container(
    child: Center(
      child: Text(verticalDragEvent),
    ),
    color: Colors.red,
  ),
  onVerticalDragStart: (DragStart) {},
  onVerticalDragUpdate: (DragUpdate) {
    verticalDragEvent = DragUpdate.delta.toString();
    setState(() {});
  },
  onVerticalDragDown: (DragDown) {},
  onVerticalDragCancel: () {},
  onVerticalDragEnd: (DragEnd) {},
)

水平方向拖拽

GestureDetector(
  child: Container(
    child: Center(
      child: Text(horizontalDragEvent),
    ),
    color: Colors.blue,
  ),
  onHorizontalDragStart: (DragStart) {},
  onHorizontalDragUpdate: (DragUpdate) {
    horizontalDragEvent = DragUpdate.delta.toString();
    setState(() {});
  },
  onHorizontalDragDown: (DragDown) {},
  onHorizontalDragCancel: () {},
  onHorizontalDragEnd: (DragEnd) {},
),

垂直水平拖拽

GestureDetector(
  child: Container(
    child: Center(
      child: Text(panDragEvent),
    ),
    color: Colors.green,
  ),
  onPanStart: (panStart) {},
  onPanDown: (panDown) {},
  onPanEnd: (panEnd) {},
  onPanUpdate: (panUpdate) {
    panDragEvent = panUpdate.delta.toString();
    setState(() {});
  },
  onPanCancel: () {},
),

偏移量(globalPosition与localPosition)

在GestureDetector中有两个重要属性值globalPosition和localPosition,两者都是Offset对象。globalPosition就像它的命名表示当前手势触点在全局坐标系位置与对应组件顶点坐标的偏移量(dx,dy),而localPosition则就表示当前手势触点在对应组件坐标系位置与对应组件顶点坐标的偏移量(dx,dy)。

例如如下代码中,为Container设置了GestureDetector手势监听,在update回调中获取updateDetail对象,在Text中显示globalPosition偏移量。从中获取到的globalPosition和localPosition中dx的值相同,dy却不同。也就是因为Scaffold中设置了AppBar,相对于body他的全局坐标系并非它自身。但若将Scaffold中的AppBar去除,让body撑满整个Scaffold,那么在手势监听中获取到的globalPosition和localPosition偏移量将相同。

需要注意的是对于globalPosition在安卓中还包含了手机状态栏的高度。

MaterialApp(
      theme: AppTheme.themes[store.state.appThemeState.themeType],
      home: Scaffold(
        appBar: AppBar(),
        body: GestureDetector(
          onPanStart: (detail) {
            showLog(detail.runtimeType, detail.localPosition,
                detail.globalPosition);
          },
          onPanUpdate: (detail) {
            showLog(detail.runtimeType, detail.localPosition,
                detail.globalPosition);
            setState(() {
              offsetText = "globalPosition: ${Offset(detail.globalPosition.dx, detail.globalPosition.dy).toString()} \n"
                  "localPosition: ${Offset(detail.localPosition.dx, detail.localPosition.dy).toString()}";
            });
          },
          onPanEnd: (detail) {
            setState(() {
              offsetText = "end";
            });
          },
          child: Container(
            color: Colors.red,
            width: double.infinity,
            height: double.infinity,
            child: Center(
              child: Text(
                 offsetText,
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 25,
                ),
              ),
            ),
          ),
        ),
      ),
    );

可以看到当Scaffold包含和未包含AppBar时,Container两个偏移量输出的差异。

缩放(Scale)

缩放操作则需要两个指针对象配合才能发挥作用。当在屏幕上只有一个指针对象时也就是一个触点时,对应的缩放比为1和角度为0;当屏幕上出现两个指针对象时,根据两个初始位置作为基准点,也就是起始位置初始化缩放比为1和角度为0,通过手势变化得出两点的距离位置和夹角大小来计算缩放比和角度值。

GestureDetector(
  child: Transform(
    origin: Offset(size.width / 2, size.height / 2),
    transform: Matrix4.rotationZ(rotation).scaled(scale),
    child: Image.asset("res/img/jay.jpg"),
  ),
  onScaleStart: (scaleStartDetail) {},
  onScaleUpdate: (scaleUpdateDetail) {
    scale = scaleUpdateDetail.scale;
    rotation = scaleUpdateDetail.rotation;
    setState(() {});
  },
  onScaleEnd: (scaleEndDetail) {},
),

在onScaleUpdate方法中获取ScaleUpdateDetails对象,其包含了两触点实现的缩放值scale、角度值rotation、偏移量focalPoint信息。通过上述代码并可实现手势再结合上Transform实现组件的缩放和旋转功能。

🚀完整代码看这里🚀

结尾

手势简单使用并不特别难,难点在多种手势组合使用以及手势冲突的处理。像是SingleChildScrollView、Draggable等组件都有涉及到手势操作。或许查看源码是学习手势操作和自定义各种手势交互的最佳方式。