Flutter指针事情和手势原理

Pointer Event

指针事情(也称为触摸事情) 分为三个阶段

  1. 手指按下
  2. 手指移动
  3. 手指抬起

Listener 组件

Flutter中运用Listener Widget来监听指针事情

Listener({
 Key key,
 this.onPointerDown, //手指按下回调
 this.onPointerMove, //手指移动回调
 this.onPointerUp,//手指抬起回调
 this.onPointerCancel,//指针事情撤销回调
 Widget child // 子组件
})
// 检查手指相关于容器的方位
class _PointerMoveIndicatorState extends State<PointerMoveIndicator> {
 PointerEvent? _event;
 @override
 Widget build(BuildContext context) {
  return Listener(
   child: Container(
    alignment: Alignment.center,
    color: Colors.blue,
    width: 300.0,
    height: 150.0,
    child: Text(
     '${_event?.localPosition ?? ''}',
     style: TextStyle(color: Colors.white),
    ),
   ),
   onPointerDown: (PointerDownEvent event) => setState(() => _event = event),
   onPointerMove: (PointerMoveEvent event) => setState(() => _event = event),
   onPointerUp: (PointerUpEvent event) => setState(() => _event = event),
  );
 }
}

Flutter指针事情和手势原理
PointerDownEvent、 PointerMoveEvent、 PointerUpEvent 都是PointerEvent的子类 PointerEvent参数

  • position:指针的全局坐标
  • localPosition: 指针自身布局坐标。
  • delta:两次PointerMoveEvent的间距。

忽略指针事情

  • 运用IgnorePointer和AbsorbPointer,让某个子树不呼应PointerEvent
  • AbsorbPointer自身会参与Hit Test(呼应指针事情),但其子树不行,而IgnorePointer自身不会参与
Listener(
 child: AbsorbPointer(
  child: Listener(
   child: Container(
    color: Colors.red,
    width: 200.0,
    height: 100.0,
   ),
   onPointerDown: (event)=>print("in"),
  ),
 ),
 onPointerDown: (event)=>print("up"),
)

点击Container时,因为它在AbsorbPointer的子树上,所以不会呼应指针事情,所以日志不会输出”in”,但AbsorbPointer自身是能够接纳指针事情的,所以会输出”up”。如果将AbsorbPointer换成IgnorePointer,那么两个都不会输出。

事情处理流程

  1. hit test

    当手指按下时,触发 PointerDownEvent 事情, 遍历当前烘托目标(render object)树,对每一个object进行hit test(射中测验),如果hit test经过,则该object添加到HitTestResult列表傍边

  2. event dispatch(事情分发)

    2.1 hit test完毕后,遍历 HitTestResult 列表,调用每一个Render Object的事情处理办法(handleEvent)

    2.2 随后当手指移动时,便会分发 PointerMoveEvent 事情。

  3. 事情完毕

    当手指抬( PointerUpEvent )起或事情撤销时(PointerCancelEvent),清空 HitTestResult 列表。

子Render Object比父object先呼应事情。
因为hit test依照深度优先遍历的,所以子object会比父object先参加 HitTestResult 列表。
// 事情处理流程
void _handlePointerEventImmediately(PointerEvent event) {
 HitTestResult? hitTestResult;
 if (event is PointerDownEvent ) {
  hitTestResult = HitTestResult();
  // 发起射中测验
  hitTest(hitTestResult, event.position);
  if (event is PointerDownEvent) {
   _hitTests[event.pointer] = hitTestResult;
  }
 } else if (event is PointerUpEvent || event is PointerCancelEvent) {
  //获取射中测验的成果,然后移除
  hitTestResult = _hitTests.remove(event.pointer);
 } else if (event.down) { // PointerMoveEvent
  //获取射中测验的成果
  hitTestResult = _hitTests[event.pointer];
 }
 // 事情分发
 if (hitTestResult != null) {
  dispatchEvent(event, hitTestResult);
 }
}

hit test过程

@override
void hitTest(HitTestResult result, Offset position) {
 //从烘托树(RenderObject)根节点开端依照深度优先顺序递归进行射中测验
 renderView.hitTest(result, position: position);
 // 调用 GestureBinding 的 hitTest()办法
 super.hitTest(result, position);
}
// Render View的 hit test过程
bool hitTest(HitTestResult result, { Offset position }) {
 if (child != null)
  child.hitTest(result, position: position); //递归对子树进行射中测验
 //Render View根节点始终被添加到HitTestResult列表中
 result.add(HitTestEntry(this));
 return true;
}
// 以RenderBox 为例说明child的hitTest过程
bool hitTest(HitTestResult result, { @required Offset position }) {
 if (_size.contains(position)) { // 判别事情的触发方位是否坐落组件范围内
  // hitTestChildren: 判别有没有子节点经过hit test
  // 重写hitTestSelf函数并返回true,"强行声明”当前节点经过了射中测验
  if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
   result.add(BoxHitTestEntry(this, position));
   return true; // 当前节点经过hit test
  }
 }
 return false;
}

event dispatch(事情分发)

// 事情分发
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
 // 遍历HitTestResult,调用每一个节点的 handleEvent
 for (final HitTestEntry entry in hitTestResult.path) {
  entry.target.handleEvent(event.transformed(entry.transform), entry);
 }
}

所以组件只需重写 handleEvent 就能够处理事情。

GestureDetector(手势辨认控件)

  • 用于手势辨认的Widget
  • 内部封装了 Listener

点击、双击、长按

// 运用GestureDetector对Container进行手势辨认,触发后, 在Container上显现事情名
class _GestureTestState extends State<GestureTest> {
 String _operation = "No Gesture detected!"; //保存事情名
 @override
 Widget build(BuildContext context) {
  return Center(
   child: GestureDetector(
    child: Container(
     alignment: Alignment.center,
     color: Colors.blue,
     width: 200.0,
     height: 100.0,
     child: Text(
      _operation,
      style: TextStyle(color: Colors.white),
     ),
    ),
    onTap: () => updateText("Tap"), //点击
    onDoubleTap: () => updateText("DoubleTap"), //双击
    onLongPress: () => updateText("LongPress"), //长按
   ),
  );
 }
 void updateText(String text) {
  //更新显现的事情名
  setState(() {
   _operation = text;
  });
 }
}

Flutter指针事情和手势原理

拖动、滑动

  • GestureDetector 关于拖动和滑动事情是没有区别的,本质上是相同的。
  • GestureDetector 会将要监听的组件的原点(左上角)作为本次手势的原点,当用户在监听的组件上按下手指时,手势辨认就会开端。下面咱们看一个拖动圆形字母A的示例:
class _Drag extends StatefulWidget {
 @override
 _DragState createState() => _DragState();
}
class _DragState extends State<_Drag> with SingleTickerProviderStateMixin {
 double _top = 0.0; //距顶部的偏移
 double _left = 0.0;//距左面的偏移
 @override
 Widget build(BuildContext context) {
  return Stack(
   children: <Widget>[
    Positioned(
     top: _top,
     left: _left,
     child: GestureDetector(
      child: CircleAvatar(child: Text("A")),
      //手指按下时会触发此回调
      onPanDown: (DragDownDetails e) {
       //打印手指按下的方位(相关于屏幕,不是父组件)
       print("用户手指按下:${e.globalPosition}");
      },
      //手指滑动时会触发此回调
      onPanUpdate: (DragUpdateDetails e) {
       //用户手指滑动时,更新偏移,重新构建
       setState(() {
        _left += e.delta.dx;
        _top += e.delta.dy;
       });
      },
      onPanEnd: (DragEndDetails e){
       //打印滑动完毕时在x、y轴上的速度
       print(e.velocity);
      },
     ),
    )
   ],
  );
 }
}

Flutter指针事情和手势原理

I/flutter ( 8513): 用户手指按下:Offset(26.3, 101.8)
I/flutter ( 8513): Velocity(235.5, 125.8)

缩放

class _Scale extends StatefulWidget {
 const _Scale({Key? key}) : super(key: key);
 @override
 _ScaleState createState() => _ScaleState();
}
class _ScaleState extends State<_Scale> {
 double _width = 200.0; //经过修改图片宽度来达到缩放效果
 @override
 Widget build(BuildContext context) {
  return Center(
   child: GestureDetector(
    //指定宽度,高度自适应
    child: Image.asset("./images/sea.png", width: _width),
    onScaleUpdate: (ScaleUpdateDetails details) {
     setState(() {
      //缩放倍数在0.8到10倍之间
      _width=200*details.scale.clamp(.8, 10.0);
     });
    },
   ),
  );
 }
}

Flutter指针事情和手势原理

GestureRecognizer(手势辨认器)

  • GestureDetector内部是运用GestureRecognizer来辨认各种手势的
  • GestureRecognizer经过Listener将指针事情转换为手势 TextSpan不是一个widget,但是能够接纳GestureRecognizer来辨认手势
// 点击富文本时文本变色
import 'package:flutter/gestures.dart';
class _GestureRecognizer extends StatefulWidget {
 const _GestureRecognizer({Key? key}) : super(key: key);
 @override
 _GestureRecognizerState createState() => _GestureRecognizerState();
}
class _GestureRecognizerState extends State<_GestureRecognizer> {
 TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();
 bool _toggle = false; //变色开关
 @override
 void dispose() {
  //用到GestureRecognizer的话一定要调用其dispose办法释放资源
  _tapGestureRecognizer.dispose();
  super.dispose();
 }
 @override
 Widget build(BuildContext context) {
  return Center(
   child: Text.rich(
    TextSpan(
     children: [
      TextSpan(text: "你好国际"),
      TextSpan(
       text: "点我变色",
       style: TextStyle(
        fontSize: 30.0,
        color: _toggle ? Colors.blue : Colors.red,
       ),
       recognizer: _tapGestureRecognizer
        ..onTap = () {
         setState(() {
          _toggle = !_toggle;
         });
        },
      ),
      TextSpan(text: "你好国际"),
     ],
    ),
   ),
  );
 }
}

Flutter指针事情和手势原理

手势辨认原理

手势的辨认是在事情分发阶段的,GestureDetector 是一个 StatelessWidget, 包含了 RawGestureDetector,咱们看一下它的 build 办法实现:

@override
Widget build(BuildContext context) {
 final gestures = <Type, GestureRecognizerFactory>{};
 // 构建 TapGestureRecognizer
 if (onTapDown != null ||
   onTapUp != null ||
   onTap != null ||
 ) {
  gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
   () => TapGestureRecognizer(debugOwner: this),
   (TapGestureRecognizer instance) {
    instance
     ..onTapDown = onTapDown
     ..onTapUp = onTapUp
     ..onTap = onTap
   },
  );
 }

 return RawGestureDetector(
  gestures: gestures, // 传入手势辨认器
  behavior: behavior,
  child: child,
 );
}
@override
Widget build(BuildContext context) {
  // 运用 Listener 监听指针事情 
 Widget result = Listener(
  onPointerDown: _handlePointerDown,
  behavior: widget.behavior ?? _defaultBehavior,
  child: widget.child,
 );
} 

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

持续看下 TapGestureRecognizer 的几个相关办法

class CustomTapGestureRecognizer1 extends TapGestureRecognizer {
 void addPointer(PointerDownEvent event) {
  //会将 handleEvent 回调添加到 pointerRouter 中
  GestureBinding.instance!.pointerRouter.addRoute(event.pointer, handleEvent);
 }

 @override
 void handleEvent(PointerEvent event) {
  //进行手势辨认,并决定是是调用 acceptGesture 仍是 rejectGesture,
 }

 @override
 void acceptGesture(int pointer) {
  // 竞赛胜出会调用
 }
 @override
 void rejectGesture(int pointer) {
  // 竞赛失败会调用
 }
}
  1. PointerDownEvent 事情触发时,调用 TapGestureRecognizer 的 addPointer,在 addPointer 中将 handleEvent 办法添加到 pointerRouter 中保存起来。

  2. 手势产生变化时, 从pointerRouter中取出 GestureRecognizer 的 handleEvent 办法进行手势辨认。

  3. 同一个手势应该只有一个GestureRecognizer收效,所以引进手势竞技场(Arena)的概念

    3.1 每一个GestureRecognizer都是一个GestureArenaMember,当产生指针事情时,他们都要在Arena去竞赛本次事情的处理权,终究只有一个“竞赛者”会胜出(win)。

    3.2 竞技场管理者(GestureArenaManager)就会告诉其他竞赛者失败。

    3.3 胜出者的 acceptGesture 会被调用,其他的 rejectGesture 将会被调用。

手势竞赛的两个比如

  1. 如果一个组件一起监听水平和垂直方向的拖动手势,当斜着拖动时哪个方向的拖动手势回调会被触发?

    取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事情竞赛中就胜出。

  2. 一个ListView,它有个ListView子组件,如果滑动这个子ListView,因为子ListView胜出而获得滑动事情的处理权, 所以父ListView不会滑动。

多手势抵触

因为手势竞赛终究只有一个胜出者,所以经过 GestureDetector 监听多种手势时,可能会产生抵触。假设有一个widget,它能够左右拖动,现在咱们也想检测在它上面手指按下和抬起的事情,代码如下:

class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
 double _left = 0.0;
 @override
 Widget build(BuildContext context) {
  return Stack(
   children: <Widget>[
    Positioned(
     left: _left,
     child: GestureDetector(
       child: CircleAvatar(child: Text("A")), //要拖动和点击的widget
       onHorizontalDragUpdate: (DragUpdateDetails details) {
        setState(() {
         _left += details.delta.dx;
        });
       },
       onHorizontalDragEnd: (details){
        print("onHorizontalDragEnd");
       },
       onTapDown: (details){
        print("down");
       },
       onTapUp: (details){
        print("up");
       },
     ),
    )
   ],
  );
 }
}

现在按住圆形“A”拖动然后抬起手指,控制台日志如下:

I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd

没有打印”up”,因为在拖动时,刚开端按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),打印”down”,而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp产生了抵触,因为是在拖动的语义中,所以onHorizontalDragEnd胜出,就会打印 “onHorizontalDragEnd”。

处理手势抵触

有如下两种办法:

1. Listener

竞赛只是针对手势的,而 Listener 是监听原始指针事情,指针事情并非语义的手势,所以不会走手势竞赛的逻辑,所以不会相互影响。

Positioned(
 top:80.0,
 left: _leftB,
 child: Listener(
  onPointerDown: (details) {
   print("down");
  },
  onPointerUp: (details) {
   //会触发
   print("up");
  },
  child: GestureDetector(
   child: CircleAvatar(child: Text("B")),
   onHorizontalDragUpdate: (DragUpdateDetails details) {
    setState(() {
     _leftB += details.delta.dx;
    });
   },
   onHorizontalDragEnd: (details) {
    print("onHorizontalDragEnd");
   },
  ),
 ),
)

2. 自定义 Recognizer

自定义手势辨认器(Recognizer),重写rejectGesture 办法:在里面调用acceptGesture 办法,强制变成竞赛的成功者了,这样它的回调也就会执行。

class CustomTapGestureRecognizer extends TapGestureRecognizer {
 @override
 void rejectGesture(int pointer) {
  //super.rejectGesture(pointer);
  //宣布成功
  super.acceptGesture(pointer);
 }
}
//创建一个新的GestureDetector,用自定义的 CustomTapGestureRecognizer 替换默许的
RawGestureDetector customGestureDetector({
 GestureTapCallback? onTap,
 GestureTapDownCallback? onTapDown,
 Widget? child,
}) {
 return RawGestureDetector(
  child: child,
  gestures: {
   CustomTapGestureRecognizer:
     GestureRecognizerFactoryWithHandlers<CustomTapGestureRecognizer>(
    () => CustomTapGestureRecognizer(),
    (detector) {
     detector.onTap = onTap;
    },
   )
  },
 );
}
customGestureDetector( // 替换 GestureDetector
 onTap: () => print("2"),
 child: Container(
  width: 200,
  height: 200,
  color: Colors.red,
  alignment: Alignment.center,
  child: GestureDetector(
   onTap: () => print("1"),
   child: Container(
    width: 50,
    height: 50,
    color: Colors.grey,
   ),
  ),
 ),
);