【Flutter】手势监测GestureDetector[ˈdʒestʃər dɪˈtektər]的属性介绍及应用

属性介绍

GestureWidget除key以外有39个属性,但大部分是成组出现的。大致分为以下几组:child、tap、longPress、drag、forcePress、pan、scale、behavior以及excludeFromSemantics,下边列出了其中比较常用的几个。

tap–点击事件

不同于Android中通过View.SetOnClickListener()来给组件添加点击事件,Flutter中绝大多数的组件是不能响应点击事件的。通常做法是在目标Widget外层包裹一个GestureDetector,这也就是child属性的作用,GestureDetector的大小等于子Widget的大小,也就是触控区域的大小。

  1. onTap() :发生点击。
  2. onDoubleTap():双击。
  3. onTapCancel():手指离开屏幕时,触控位置不在GestureDetector范围内。
  4. onTapDown(TapXXXDetails) :手指落在触控范围内时的回调,details参数中包含触控点的全局位置(相对于屏幕左上角)和本地位置(相对于触控区左上角)。
  5. onTapUp(TapXXXDetails):同上。

longPress–长按事件

  1. onLongPress() :手指在触控区停留一段时间,检测出长按(不用放手)。
  2. onLongPressStart(LongPressXXXDetails):同上,获得检测到长按时的触控点坐标(Global和Local)。
  3. onLongPressMoveUpdate(LongPressXXXDetails):检测到长按,手指在屏幕内(而非仅仅在触控区域内),得到触控点坐标。
  4. onLongPressUp() :检测到长按,手指离开屏幕时。
  5. onLongPressEnd(LongPressXXXDetails):同上,获得该点坐标。

drag–拖拽事件

  1. onVerticalDragDown(DragDownDetails) :实际上不用拖拽,当手指接触触控区,就会产生该回调,获得该点坐标。
  2. onVerticalDragStart(DragStartDetails):开始拖拽,details不仅包含该点坐标,而且记录了开始拖拽的时间戳
  3. onVerticalDragUpdate(DragUpdateDetails):纵向拖拽过程中,参数不仅包含该点坐标,且记录了当前时间戳可以与startDrag的时间戳联合使用。
  4. onVerticalDragEnd(DragEndDetails) :产生纵向拖拽,手指离开屏幕,获得该点坐标。
  5. onVerticalDragCancel():点击了触控区但没有产生拖拽,或者横向拖拽至触控区外。
  6. Horizontal横向,略。

forcePress–压感检测

哇咔咔,GestureDetector这么厉害,居然支持压力检测。
这里的压力是没有单位的,默认范围是startPressure0.0-peakPressure1.0代表从没有压力到最大压力。

  1. onForcePressStart(ForcePressDetails) :触碰屏幕且有一定压力,压力值大于startPressure,获得压力值和触控点坐标。
  2. onForcePressPeak(ForcePressDetails):触屏屏幕的压力大于peakPreasure,获得压力值和触控点坐标。
  3. onForcePressUpdate(ForcePressDetails):已经被检测到产生压力的情况下,在屏幕上滑动,或者在原地改变压力大小,亦或是两者都有。同时获得压力值和触控点坐标。
  4. onForcePressEnd(ForcePressDetails) :已检测到产生压力的情况下,手指离开屏幕。

然而,在所有的压力检测回调中都有这样一则注释:

/// Note that this callback will only be fired on 
final GestureForcePressEndCallback onForcePressEnd;

也就是说,只有在具有压感屏的设备上,这些API才起作用。

pan-综合横向与纵向拖拽的拖拽事件

  1. onPanDown(DragDownDetails) :手指与屏幕接触时,获得该点坐标。
  2. onPanStart(DragStartDetails):手指开始在屏幕上移动。
  3. onPanCancel()
  4. 取消拖拽,暂未回调到该方法。
  5. onPanEnd(DragEndDetails) :产生拖拽,手指离开屏幕,获得该点坐标。
  6. onPanUpdate(DragUpdateDetails):在出发onPanDown的前提下,手指在屏幕上滑动,获得触控点坐标。

应用:做一个并没有什么用的摇杆–JoyStick。

效果图

解释效果图,末尾简单展示了代码。

如效果图所示:墨绿色部分为触控区,也就是GestureDetector本尊了。其中有一个灰色半透明的圆,称之为摇杆盘,摇杆盘内又有一个黑色半透明的圆,称之为摇杆。

实现思路

刚刚上面列出了GestureDetector的绝大部分属性,不难想到,面对这种同时具有横向和纵向拖拽的交互,用pan就够了。在拖拽过程中会不断地用到坐标,因此采用Stack+Position,于是GestureDetector的子widget如下:
子Widget
1.手指接触触控区时,触发onPanDown(DragDownDetail),根据当前坐标,给摇杆盘进行定位,使得当前位置为摇杆滑动起始位置。
2.手指滑动过程中,摇杆盘的位置不变。摇杆的位置分两种情况:
1)触控点在灰色圈内时(半径为R-r),触控点的位置就是摇杆的圆心位置。
范围内

2)触控点在灰圈外时,触控点与摇杆盘圆心连成的直线与灰圆有两个交点,其中距离触控点较近的点即为摇杆的圆心。这里涉及直线与圆的交点问题,如果线性代数学得好(矩阵,线性方程组),程序就是计算机,否则像我一样,程序只是计算器。

因为模拟了摇杆控制小球移动,所以需要根据触控点与盘圆心的距离计算相对速度。而速度是矢量,已知两点坐标,求出直线斜率。利用arc函数求出直线与x轴夹角,观察规律将该夹角转化为与x轴正方向的夹角,以此求出速度的方向。
示意图
3.结束滑动时,触发onPanEnd(DragXXXDetails),将摇杆盘和摇杆圆心回滚到初始位置。

代码实现

import 'dart:async';
import 'dart:math';

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

class JoyStick extends StatefulWidget {
 @override
 _JoyStickState createState() => _JoyStickState();

 final stickSide; //摇杆区域边长
 final stickBgRadius; //摇杆盘半径
 final stickRadius; //摇杆指示器半径
 final Function(Offset) onSpeedChanged; //速度变化回调

 JoyStick(
     {@required this.onSpeedChanged,
     this.stickSide = 300.0,
     this.stickBgRadius = 50.0,
     this.stickRadius = 20.0}) {
   assert(this.stickRadius > 0.0);
   assert(this.stickBgRadius > 0.0);
   assert(this.stickRadius > 0.0);
   assert(this.onSpeedChanged != null);
   assert(this.stickSide > stickBgRadius * 2);
   assert(this.stickBgRadius > stickRadius);
 }
}

class _JoyStickState extends State<JoyStick> {
 var _stickBgOffset; //遥感盘圆心
 var _stickOffset; //摇杆圆心
 var _defaultStickBgOffset; //摇杆盘默认圆心,用于结束滑动式回滚到初始位置
 var _defaultStickOffset;
 var _isDraggingEffectively = false; //有效拖拽

 @override
 void initState() {
   super.initState();
   _defaultStickBgOffset = Offset(
       //以触控区的中心作为摇杆初始圆心位置,但Position的坐标,为Widget矩形左上角的位置,因此需要根据圆心坐标和半径求出真正的位置坐标
       sqrt(pow(widget.stickSide / 2 - widget.stickBgRadius, 2)),
       sqrt(pow(widget.stickSide / 2 - widget.stickBgRadius, 2)));
   _defaultStickOffset = Offset(
       sqrt(pow(widget.stickSide / 2 - widget.stickRadius, 2)),
       sqrt(pow(widget.stickSide / 2 - widget.stickRadius, 2)));
   _stickBgOffset = _defaultStickBgOffset;
   _stickOffset = _defaultStickOffset;
 }

 @override
 Widget build(BuildContext context) {
   return Material(
       color: Colors.yellow.withAlpha(100),
       child: GestureDetector(
         onPanDown: (DragDownDetails detail) {
           Offset bgOffset = _calculateBgOffset(detail.localPosition);
           if (bgOffset.dx >= 0.0 &&
               bgOffset.dx <= widget.stickSide - widget.stickBgRadius * 2 &&
               bgOffset.dy >= 0.0 &&
               bgOffset.dy <= widget.stickSide - widget.stickBgRadius * 2) {
             _isDraggingEffectively =
                 true; //避免摇杆显示不全,因此实际触控区比GestureDetector小一点,标记属于有效滑动。
             setState(() {
               _stickBgOffset = bgOffset;
               _stickOffset = _calculateStickOffset(
                   detail.localPosition); //摇杆盘满足条件时,摇杆必定满足条件(因为摇杆必定在内部)
             });
           }
         },
         onPanCancel: () {
           print("panCancel");
         },
         onPanEnd: (_) {//滑动结束
           _isDraggingEffectively = false;
           widget.onSpeedChanged(Offset(0.0, 0.0));//速度变为0
           setState(() {//位置回滚
             _stickBgOffset = _defaultStickBgOffset;
             _stickOffset = _defaultStickOffset;
           });
         },
         onPanUpdate: (DragUpdateDetails details) {
           if (!_isDraggingEffectively) return;
           if (_isStickInBg(details.localPosition)) {//当触控点在遥感盘内部-摇杆半径时
             setState(() {
               _stickOffset = _calculateStickOffset(details.localPosition);
             });
           } else {
             setState(() {
               _stickOffset = _getRealOffset(details.localPosition);
             });
           }
           _calculateV(_stickOffset); //计算速度
         },
         child: Stack(
           alignment: Alignment.center,
           children: <Widget>[
             Container(
               //左上角60.0,297.3
               height: widget.stickSide,
               width: widget.stickSide,
               color: Colors.brown.withAlpha(100),
             ),
             Positioned(
               left: _stickBgOffset.dx,
               top: _stickBgOffset.dy,
               child: CircleAvatar(
                 radius: widget.stickBgRadius,
                 backgroundColor: Colors.grey.withAlpha(100),
               ),
             ),
             Positioned(
               left: _stickOffset.dx,
               top: _stickOffset.dy,
               child: CircleAvatar(
                 radius: widget.stickRadius,
                 backgroundColor: Colors.black.withAlpha(100),
               ),
             )
           ],
         ),
       ));
 }

 ///通过圆心位置求摇杆盘实际位置
 ///已知圆心和半径,可求。
 _calculateBgOffset(fingerOffset) {
   var dx = sqrt(pow(fingerOffset.dx - widget.stickBgRadius, 2)) *
       (fingerOffset.dx > widget.stickBgRadius ? 1 : -1);
   var dy = sqrt(pow(fingerOffset.dy - widget.stickBgRadius, 2)) *
       (fingerOffset.dy > widget.stickBgRadius ? 1 : -1);
   return Offset(dx, dy);
 }

 ///判断如果以当前触控点为圆心,摇杆会不会超出遥感盘范围
 _isStickInBg(fingerOffset) {
   var radius = widget.stickBgRadius - widget.stickRadius;
   var bgX = _stickBgOffset.dx + widget.stickBgRadius; //遥感盘的圆心
   var bgY = _stickBgOffset.dy + widget.stickBgRadius;
   return pow(fingerOffset.dx - bgX, 2) + pow(fingerOffset.dy - bgY, 2) <=
       pow(radius, 2);
 }

 ///通过圆心位置求摇杆位置
 ///已知圆心和半径,可求。
 _calculateStickOffset(fingerOffset) {
   var dx = sqrt(pow(fingerOffset.dx - widget.stickRadius, 2)) *
       (fingerOffset.dx > widget.stickRadius ? 1 : -1);
   var dy = sqrt(pow(fingerOffset.dy - widget.stickRadius, 2)) *
       (fingerOffset.dy > widget.stickRadius ? 1 : -1);
   return Offset(dx, dy);
 }

 ///触控点处在遥感盘外部时,计算摇杆位置
 _getRealOffset(fingerOffset) {
   var r = widget.stickBgRadius - widget.stickRadius;
   var x1 = _stickBgOffset.dx + widget.stickBgRadius; //遥感盘的圆心
   var y1 = _stickBgOffset.dy + widget.stickBgRadius;
   var x2 = fingerOffset.dx;
   var y2 = fingerOffset.dy;
   if (x1 == x2) {
     //斜率不存在的情况
     return Offset(x1, y1 + r * (y2 > y1 ? 1 : -1));
   }
   var k = (y2 - y1) / (x2 - x1);

   ///遥感盘的圆心,与手指触控点连成的直线,与摇杆内圆的交点就是摇杆的圆心。
   ///两点,直线方程,(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)
   ///内圆方程:点到圆心的距离等于半径。pow(x - x1) + pow(y - y1) = pow(r)
   ///联立,得到二元一次方程。
   var a = pow(k, 2) + 1;
   var b = (2 * pow(k, 2) * x1 + 2 * x1) * -1;
   var c = (pow(k, 2) + 1) * pow(x1, 2) - pow(r, 2);
   var m = pow(b, 2) - 4 * a * c; //m = b方减去四ac。

   ///过圆心,肯定有两个解,省略判断条件m > 0
   var rx1 = ((-b + sqrt(m)) / (2 * a));
   var rx2 = ((-b - sqrt(m)) / (2 * a));
   var realX = x2 > x1 ? max(rx1, rx2) : min(rx1, rx2);
   var realY = k * (realX - x1) + y1;
   return _calculateStickOffset(Offset(realX, realY));
 }

 ///计算小球速度
 _calculateV(_stickOffset) {
   var stickOx = _stickOffset.dx + widget.stickRadius; //摇杆圆心
   var stickOy = _stickOffset.dy + widget.stickRadius;

   ///速度是矢量,先求一下大小,设置滑动到边缘为五倍速度,圆心速度为0,单位长度,单位增加。
   var oX1 = _stickBgOffset.dx + widget.stickBgRadius; //遥感盘的圆心
   var oY1 = _stickBgOffset.dy + widget.stickBgRadius;
   var maxX = widget.stickBgRadius - widget.stickRadius; //最大直线位移
   var unitX = maxX / 5; //单位长度
   var x = sqrt(pow(stickOx - oX1, 2) + pow(stickOy - oY1, 2)); //实际位移
   var v = unitX == 0.0 ? 0.0 : x / unitX; //速度
   var alpha = _getRadToPositiveX(Offset(stickOx, stickOy), Offset(oX1, oY1));//获得速度与X轴正向夹角
   var vX = v * cos(alpha);
   var vY = v * sin(alpha);
   widget.onSpeedChanged(Offset(vX, vY));
 }

 ///p为点,O为原点,求pO与x轴正向夹角
 _getRadToPositiveX(p, O) {
   if (p.dx == O.dx) {
     return p.dy > O.dy ? 3 / 2 * pi : pi / 2;
   }
   var k = (p.dy - O.dy) / (p.dx - O.dx);
   var alphaToX = atan(k); //arctan,直线斜倾角(与x轴夹角)结果为-pi/2到pi/2之间
   var alphaToPositiveX;
   //一象限rad * -1,二象限pi - rad,三象限rad * -1 + pi,四象限2 * pi - rad
   if (p.dx > O.dx && (p.dy <= O.dy)) {
     //1
     alphaToPositiveX = alphaToX * -1;
   } else if (p.dx <= O.dx && p.dy <= O.dy) {
     //2
     alphaToPositiveX = pi - alphaToX;
   } else if (p.dx <= O.dx && p.dy > O.dy) {
     //3
     alphaToPositiveX = alphaToX * -1 + pi;
   } else {
     //4
     alphaToPositiveX = 2 * pi - alphaToX;
   }
   return alphaToPositiveX;
 }

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


///使用示例
class JoyStickDemo extends StatefulWidget {
 @override
 _JoyStickDemoState createState() => _JoyStickDemoState();
}

class _JoyStickDemoState extends State<JoyStickDemo> {
 var _ballRadius = 15.0; //小球半径
 var _ballOffsetX = 0.0;
 var _ballOffsetY = 0.0;
 var _vX = 0.0; //初始速度为0
 var _vY = 0.0;
 var _unitV = 0.8; //单位速度
 Timer _timer;

 @override
 void initState() {
   super.initState();
   _timer = Timer.periodic(const Duration(milliseconds: 16), (value) {
     //1000/60,即每秒60帧。
     if (_vX == 0 && _vY == 0) return;
     var newX = _ballOffsetX + _unitV * _vX;
     var newY = _ballOffsetY +
         _unitV * _vY * -1; //乘-1的原因是数学坐标y正方向向上,而Stack的位置坐标y轴正方向向下
     var rightBound = MediaQuery.of(context).size.width -
         2 * _ballRadius; //右边界--屏幕宽度减去小球直径。
     var bottomBound = MediaQuery.of(context).size.height - 2 * _ballRadius;
     if (newX < 0.0) newX = 0.0; //左边界
     if (newX > rightBound) newX = rightBound; //右边界
     if (newY < 0.0) newY = 0.0;
     if (newY > bottomBound) newY = bottomBound;
     if (newX.isNaN || newY.isNaN)
       return; //即便已经解决了会出现NaN的情况(x1 == x2,斜率不存在),但还是加一下判断吧,以防万一。
     setState(() {
       _ballOffsetX = newX;
       _ballOffsetY = newY;
     });
   });
 }

 @override
 void dispose() {
   _timer.cancel();
   _timer = null;
   super.dispose();
 }

 @override
 Widget build(BuildContext context) {
   return Stack(
     children: <Widget>[
       Container(
         color: Colors.greenAccent,
       ),
       Positioned(
         left: _ballOffsetX,
         top: _ballOffsetY,
         child: CircleAvatar(
           backgroundColor: Colors.red,
           radius: _ballRadius,
         ),
       ),
       Positioned(
           bottom: 0.0,
           right: 0.0,
           child: JoyStick(
             onSpeedChanged: (offset) {
               setState(() {
                 _vX = offset.dx;
                 _vY = offset.dy;
               });
             },
           ))
     ],
   );
 }
}

源码:JoyStick On GitHub

发布了2 篇原创文章 · 获赞 1 · 访问量 33

猜你喜欢

转载自blog.csdn.net/weixin_43879272/article/details/103882323