简介
Flutter中的Widget虽然没有提供类似onTouch的方法用来监听用户在屏幕上的手势事件。但是Fluuter提供了Listener组件和GestureDetector组件来处理用户的手势事件。
本文主要讲解在实际开发中如何使用这两个组件完成对手势事件的监听处理,最后通过实战来加深大家的理解。
Listener
构造函数
const Listener({
super.key,
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerHover,
this.onPointerCancel,
this.onPointerPanZoomStart,
this.onPointerPanZoomUpdate,
this.onPointerPanZoomEnd,
this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild,
super.child,
}) : assert(behavior != null);
常用手势回调
- onPointerDown 手指按下回调
- onPointerMove 手指移动回调
- onPointerUp 手指抬起回调
- onPointerHover 指针没有按下的移动事件(多用于对鼠标的监听)
- onPointerCancel 触摸事件取消回调
- onPointerPanZoomStart 缩放事件开始回调
- onPointerPanZoomUpdate 缩放事件更新回调
- onPointerPanZoomEnd 缩放事件结束回调
Listener是Flutter原始的手势监听组件,类似Android中View的onTouch函数,在Flutter中可以借助Listener完成对Widget组件触摸事件的监听。当需要对某个Widget的触摸事件进行拦截,不再分发时,Android中需要实现ViewGroup的InterceptTouchEvent对事件进行拦截,在Flutter中也提供IgnorePointer和AbsorbPointer两个组件阻止子Widget接收手势事件。(关于IgnorePointer和AbsorbPointer使用起来太简单了,只需要将Listener中child包裹起来就行。本文不再介绍使用)
GestureDetector
尽管Flutter已经提供了Listener来监听手势事件,但是如果需要双击,长按等常见的操作,需要开发人员自己实现起来太麻烦。于是Flutter还提供了GestureDetector组件,用来处理常见的手势事件,GestureDetector组件是基于Listener组件实现的,功能更加齐全。
构造函数
GestureDetector提供了太多的事件监听,由于篇幅较长,本文只列举常用手势事件。
GestureDetector({
super.key,
this.child,
this.onTapDown,
this.onTapUp,
this.onTap,
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
this.onVerticalDragEnd,
this.onHorizontalDragDown,
this.onHorizontalDragStart,
this.onHorizontalDragUpdate,
this.onHorizontalDragEnd,
.........
})
常用手势回调
- onTapDown 按下事件回调
- onTapUp 抬起事件回调
- onTap 单击事件回调
- onTapCancel 取消点击回调
- onDoubleTap 双击回调
- onLongPress 长按回调
- onVerticalDragDown 垂直方向按下
- onVerticalDragStart 垂直方向开始移动
- onVerticalDragUpdate 垂直方向移动
- onVerticalDragEnd 垂直方向移动结束
- onHorizontalDragDown 水平方向按下
- onHorizontalDragStart 水平方向开始移动
- onHorizontalDragUpdate 水平方向移动
- onHorizontalDragEnd 水平方向移动结束
GestureDetector组件提供了很多基本手势事件的监听,相比较Android原生更加灵活,使用起来比较简单。由于Flutter中的Widget大多数没有提供点击事件,所以如果想要添加点击事件,只能使用GestureDetector组件实现。
实战
介绍完Flutter中的事件基本知识,想必大家还是有些迷惑,接下来通过实现一个屏幕上的拖拽组件,帮助大家加深理解。
话不多说,先看效果。
功能分析
- 初始位置
- 全局拖拽
- 边界处理
- 弹簧效果
实现步骤
1. 初始位置
首先要想实现自定义初始位置需要借助Positioned组件完成,通过指定left,top两个属性值,确定红色方块的位置。
final Offset initOffset; //初始位置
//生命成员变量, late 表示延迟加载
late double _offsetLeft;
late double _offsetTop;
_offsetLeft = widget.initOffset.dx;
_offsetTop = widget.initOffset.dy;
return Positioned(
left: _offset.dx,
top: _offset.dy,
.....
}
2. 全局拖拽
想要实现全局拖拽我们首先要确定这里使用GestureDetector组件完成,当然使用Listener组件也可以实现(读者自行尝试)。本文主要通过使用GestureDetector组件完成。
GestureDetector(
onPanStart: (DragStartDetails details) {
_selfWidth = context.size?.width ??0;
_selfHeight =context.size?.height ??0;
},
onPanUpdate: (DragUpdateDetails details) {
_offset += Offset(details.delta.dx, details.delta.dy);
_offsetTop += details.delta.dy;
_offsetLeft += details.delta.dx;
setState(() {});
},
onPanEnd: (DragEndDetails details) {
if (_offsetLeft > _screenWidth / 2 - _selfWidth / 2) {
_offsetLeft =
_screenWidth - _selfWidth - (widget.marginRight ?? 0.0);
} else {
_offsetLeft = 0.00 + (widget.marginLeft ?? 0.0);
}
},
onTap: widget.onPressed,
child: widget.child,
),
onPanStart : 开始拖拽回调
onPanUpdate:拖拽更新回调
onPanUpdate: 拖拽结束回调
使用_selfWidth,_selfHeight 记录方块的初始化位置,当手指开始拖拽时会触发onPanUpdate回调。通过DragUpdateDetails可以获取拖拽过程的距离,并将数据传递给_offsetTop和_offsetLeft,调用setState方法实时更新自己的位置。
3. 边界处理
当我们将方块拖拽到屏幕两边时,方块不能消失,始终显示在屏幕内。
onPanUpdate: (DragUpdateDetails details) {
_offset += Offset(details.delta.dx, details.delta.dy);
_offsetTop += details.delta.dy;
_offsetLeft += details.delta.dx;
//拖到顶部不可见位置时,将_offsetTop 设置为初始值显示
if (_offsetTop < 0) _offsetTop = 0 + (widget.marginTop ?? 0.0);
//拖到左边不可见位置时,将_offsetTop 设置为初始值显示
if (_offsetLeft < 0) _offsetLeft = 0 + (widget.marginLeft ?? 0.0);
//拖到右边不可见位置时,_offsetLeft 设置为屏幕宽度显示
if (_offsetLeft + _selfWidth > _screenWidth) {
_offsetLeft = _screenWidth - _selfWidth;
}
//拖到底部边不可见位置时,_offsetTop 设置为屏幕高度显示
if (_offsetTop + _selfHeight > _screenHeight) {
_offsetTop = _screenHeight - _selfHeight;
}
setState(() {});
},
4. 弹簧效果
要想实现弹簧效果,需要使用动画实现,Flutter提供了强大的动画机制帮助我们实现各种复杂的效果。
首先定义动画控制器AnimationController
_animationController = AnimationController.unbounded(vsync: this);
_animationController.addListener(() {
_offset = _animation.value;
setState(() {});
});
当拖拽手势结束时调用动画控制器执行动画。
onPanEnd: (DragEndDetails details) {
//区分屏幕中间
if (_offsetLeft > _screenWidth / 2 - _selfWidth / 2) {
_offsetLeft =
_screenWidth - _selfWidth - (widget.marginRight ?? 0.0);
} else {
_offsetLeft = 0.00 + (widget.marginLeft ?? 0.0);
}
//开始执行动画
startAnimation(details.velocity.pixelsPerSecond, size);
},
开始执行动画效果。
void startAnimation(Offset pixelsPerSecond, Size size) {
_animation = _animationController.drive(
Tween<Offset>(begin: _offset, end:
Offset(_offsetLeft, _offsetTop)));
SpringSimulation simulation = SpringSimulation(
SpringDescription(mass: _mass, stiffness: _stiffness, damping: _damping),
0,
1,
-Offset(pixelsPerSecond.dx / size.width, pixelsPerSecond.dy / size.height)
.distance,
);
_animationController.animateWith(simulation);
}
弹簧效果SpringSimulation, 涉及三个参数mass:弹簧质量,stiffness:弹簧系数,damping:阻尼系数,可以通过设置这三个属性得到不同的弹簧强度动画。
5. 完整代码
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class OverlayButton extends StatefulWidget {
const OverlayButton(
{Key? key,
required this.child,
required this.parentKey,
required this.initOffset,
required this.onPressed,
this.marginLeft,
this.marginTop,
this.marginBottom,
this.marginRight,
this.mass,
this.stiffness,
this.damping})
: super(key: key);
final Widget child; //子widget
final Offset initOffset; //初始位置
final GlobalKey parentKey; //父控件的key
final VoidCallback onPressed; //点击事件
final double? marginLeft; //外边距,距离左边
final double? marginTop; //外边距,距离上边
final double? marginRight; //外边距,距离右边
final double? marginBottom; //外边距,距离下边
final double? mass; //弹簧质量
final double? stiffness; //弹簧系数
final double? damping; //阻尼系数
@override
State createState() => _OverlayButtonState();
}
class _OverlayButtonState extends State<OverlayButton>
with SingleTickerProviderStateMixin {
late double _offsetLeft;
late double _offsetTop;
late AnimationController _animationController;
late Animation<Offset> _animation;
late Offset _offset;
late double _screenWidth;
late double _screenHeight;
late double _selfWidth = 0.0;
late double _selfHeight = 0.0;
late double _mass;
late double _stiffness;
late double _damping;
@override
void initState() {
super.initState();
_mass = widget.mass ?? 20;
_stiffness = widget.stiffness ?? 400;
_damping = widget.damping ?? 0.75;
_offsetLeft = widget.initOffset.dx;
_offsetTop = widget.initOffset.dy;
_offset = widget.initOffset;
_animationController = AnimationController.unbounded(vsync: this);
_animationController.addListener(() {
_offset = _animation.value;
setState(() {});
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final RenderBox parentRenderBox =
widget.parentKey.currentContext?.findRenderObject() as RenderBox;
_screenHeight = parentRenderBox.size.height - (widget.marginBottom ?? 0.0);
});
}
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
_screenWidth = size.width;
return Positioned(
left: _offset.dx,
top: _offset.dy,
child: GestureDetector(
onPanStart: (DragStartDetails details) {
_animationController.stop(canceled: true);
_selfWidth = context.size?.width ??0;
_selfHeight =context.size?.height ??0;
},
onPanUpdate: (DragUpdateDetails details) {
_offset += Offset(details.delta.dx, details.delta.dy);
_offsetTop += details.delta.dy;
_offsetLeft += details.delta.dx;
if (_offsetTop < 0) _offsetTop = 0 + (widget.marginTop ?? 0.0);
if (_offsetLeft < 0) _offsetLeft = 0 + (widget.marginLeft ?? 0.0);
if (_offsetLeft + _selfWidth > _screenWidth) {
_offsetLeft = _screenWidth - _selfWidth;
}
if (_offsetTop + _selfHeight > _screenHeight) {
_offsetTop = _screenHeight - _selfHeight;
}
setState(() {});
},
onPanEnd: (DragEndDetails details) {
if (_offsetLeft > _screenWidth / 2 - _selfWidth / 2) {
_offsetLeft =
_screenWidth - _selfWidth - (widget.marginRight ?? 0.0);
} else {
_offsetLeft = 0.00 + (widget.marginLeft ?? 0.0);
}
startAnimation(details.velocity.pixelsPerSecond, size);
},
onTap: widget.onPressed,
child: widget.child,
),
);
}
void startAnimation(Offset pixelsPerSecond, Size size) {
_animation = _animationController.drive(
Tween<Offset>(begin: _offset, end: Offset(_offsetLeft, _offsetTop)));
SpringSimulation simulation = SpringSimulation(
SpringDescription(mass: _mass, stiffness: _stiffness, damping: _damping),
0,
1,
-Offset(pixelsPerSecond.dx / size.width, pixelsPerSecond.dy / size.height)
.distance,
);
_animationController.animateWith(simulation);
}
@override
void dispose() {
super.dispose();
_animationController.dispose();
}
}
总结
Flutter是一门新的语言,需要不断的理解和练习,通过练习才能将学到的知识融会贯通。Listener组件和GestureDetector组件学起来比较简单,本文只介绍了GestureDetector组件实现拖拽方块,希望读者学完本篇文章后,尝试使用Listener组件完成同样的效果。
关注小编,学习不迷路!