《Flutter全栈开发实战指南:从零到高级》- 19 -手势识别
引言
在移动应用开发中,流畅自然的手势交互是提升用户体验的关键。今天我们来深入探讨Flutter中的手势识别,带你从0-1掌握这个强大的交互工具。
1. GestureDetector
1.1 GestureDetector原理
下面我们先通过一个架构图来加深理解GestureDetector的工作原理:
graph TB
A[触摸屏幕] --> B[RawPointerEvent事件产生]
B --> C[GestureDetector接收事件]
C --> D[手势识别器分析]
D --> E{匹配手势类型}
E -->|匹配成功| F[触发对应回调]
E -->|匹配失败| G[事件传递给其他组件]
F --> H[更新UI状态]
G --> I[父组件处理]
核心原理解析:
-
事件传递机制
- Flutter使用冒泡机制传递触摸事件
- 从最内层组件开始,向外层组件传递
- 每个GestureDetector都可以拦截和处理事件
-
多手势竞争
- 多个手势识别器竞争处理同一组触摸事件
- 通过规则决定哪个识别器获胜
- 获胜者将处理后续的所有相关事件
-
命中测试
- 确定触摸事件发生在哪个组件上
- 通过
HitTestBehavior控制测试行为
1.2 基础手势识别
下面演示一个基础手势识别案例:
class BasicGestureExample extends StatefulWidget {
@override
_BasicGestureExampleState createState() => _BasicGestureExampleState();
}
class _BasicGestureExampleState extends State<BasicGestureExample> {
String _gestureStatus = '等待手势...';
Color _boxColor = Colors.blue;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('基础手势识别')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 手势检测区域
GestureDetector(
onTap: () {
setState(() {
_gestureStatus = '单击 detected';
_boxColor = Colors.green;
});
},
onDoubleTap: () {
setState(() {
_gestureStatus = '双击 detected';
_boxColor = Colors.orange;
});
},
onLongPress: () {
setState(() {
_gestureStatus = '长按 detected';
_boxColor = Colors.red;
});
},
onPanUpdate: (details) {
setState(() {
_gestureStatus = '拖拽中: ${details.delta}';
_boxColor = Colors.purple;
});
},
onScaleUpdate: (details) {
setState(() {
_gestureStatus = '缩放: ${details.scale.toStringAsFixed(2)}';
_boxColor = Colors.teal;
});
},
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: _boxColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
)
],
),
child: Icon(
Icons.touch_app,
color: Colors.white,
size: 50,
),
),
),
SizedBox(height: 30),
// 状态显示
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_gestureStatus,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
SizedBox(height: 20),
// 手势说明
_buildGestureInstructions(),
],
),
),
);
}
Widget _buildGestureInstructions() {
return Container(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInstructionItem('单击', '快速点击一次'),
_buildInstructionItem('双击', '快速连续点击两次'),
_buildInstructionItem('长按', '按住不放'),
_buildInstructionItem('拖拽', '按住并移动'),
_buildInstructionItem('缩放', '双指捏合或展开'),
],
),
);
}
Widget _buildInstructionItem(String gesture, String description) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Text(gesture, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(width: 16),
Text(description, style: TextStyle(fontSize: 14, color: Colors.grey[600])),
],
),
);
}
}
1.3 手势识别器类型总结
下面我们总结下手势识别器都包含哪些类型,并了解各种手势识别器的特性:
| 手势类型 | 识别器 | 触发条件 | 应用场景 |
|---|---|---|---|
| 点击 | onTap | 快速触摸释放 | 按钮点击、项目选择 |
| 双击 | onDoubleTap | 快速连续两次点击 | 图片放大/缩小、点赞 |
| 长按 | onLongPress | 长时间按住 | 显示上下文菜单、拖拽准备 |
| 拖拽 | onPanUpdate | 按住并移动 | 滑动删除、元素拖拽 |
| 缩放 | onScaleUpdate | 双指捏合/展开 | 图片缩放、地图缩放 |
| 垂直拖拽 | onVerticalDragUpdate | 垂直方向拖拽 | 滚动列表、下拉刷新 |
| 水平拖拽 | onHorizontalDragUpdate | 水平方向拖拽 | 页面切换、轮播图 |
1.4 多手势间竞争规则
我们先来演示下不同手势的触发效果
![]()
- 竞争规则
![]()
2. 拖拽与缩放
2.1 实现原理
拖拽功能的实现基于以下事件序列:
sequenceDiagram
participant U as 用户
participant G as GestureDetector
participant S as State
U->>G: 手指按下 (onPanStart)
G->>S: 记录起始位置
Note over S: 设置_dragging = true
loop 拖拽过程
U->>G: 手指移动 (onPanUpdate)
G->>S: 更新位置数据
S->>S: setState() 触发重建
Note over S: 根据delta更新坐标
end
U->>G: 手指抬起 (onPanEnd)
G->>S: 结束拖拽状态
Note over S: 设置_dragging = false
2.2 拖拽功能
下面是拖拽功能核心代码实现:
class DraggableBox extends StatefulWidget {
@override
_DraggableBoxState createState() => _DraggableBoxState();
}
class _DraggableBoxState extends State<DraggableBox> {
// 位置状态
double _positionX = 0.0;
double _positionY = 0.0;
// 拖拽状态
bool _isDragging = false;
double _startX = 0.0;
double _startY = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('拖拽盒子')),
body: Stack(
children: [
// 背景网格
_buildBackgroundGrid(),
// 拖拽盒子
Positioned(
left: _positionX,
top: _positionY,
child: GestureDetector(
onPanStart: _handlePanStart,
onPanUpdate: _handlePanUpdate,
onPanEnd: _handlePanEnd,
child: AnimatedContainer(
duration: Duration(milliseconds: 100),
width: 120,
height: 120,
decoration: BoxDecoration(
color: _isDragging ? Colors.blue[700] : Colors.blue[500],
borderRadius: BorderRadius.circular(12),
boxShadow: _isDragging ? [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 15,
offset: Offset(0, 8),
)
] : [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: Offset(0, 4),
)
],
border: Border.all(
color: Colors.white,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_isDragging ? Icons.touch_app : Icons.drag_handle,
color: Colors.white,
size: 40,
),
SizedBox(height: 8),
Text(
_isDragging ? '拖拽中...' : '拖拽我',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
// 位置信息
Positioned(
bottom: 20,
left: 20,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'位置: (${_positionX.toStringAsFixed(1)}, '
'${_positionY.toStringAsFixed(1)})',
style: TextStyle(color: Colors.white),
),
),
),
],
),
);
}
void _handlePanStart(DragStartDetails details) {
setState(() {
_isDragging = true;
_startX = details.globalPosition.dx - _positionX;
_startY = details.globalPosition.dy - _positionY;
});
}
void _handlePanUpdate(DragUpdateDetails details) {
setState(() {
_positionX = details.globalPosition.dx - _startX;
_positionY = details.globalPosition.dy - _startY;
// 限制在屏幕范围内
final screenWidth = MediaQuery.of(context).size.width;
final screenHeight = MediaQuery.of(context).size.height;
_positionX = _positionX.clamp(0.0, screenWidth - 120);
_positionY = _positionY.clamp(0.0, screenHeight - 200);
});
}
void _handlePanEnd(DragEndDetails details) {
setState(() {
_isDragging = false;
});
}
Widget _buildBackgroundGrid() {
return Container(
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: _GridPainter(),
),
);
}
}
class _GridPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.grey[300]!
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
// 绘制网格
const step = 40.0;
for (double x = 0; x < size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint);
}
for (double y = 0; y < size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
2.3 缩放功能
缩放功能涉及到矩阵变换,下面是核心代码实现:
class ZoomableImage extends StatefulWidget {
final String imageUrl;
const ZoomableImage({required this.imageUrl});
@override
_ZoomableImageState createState() => _ZoomableImageState();
}
class _ZoomableImageState extends State<ZoomableImage> {
// 变换控制器
Matrix4 _transform = Matrix4.identity();
Matrix4 _previousTransform = Matrix4.identity();
// 缩放限制
final double _minScale = 0.5;
final double _maxScale = 4.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('可缩放图片')),
body: Center(
child: GestureDetector(
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
onDoubleTap: _onDoubleTap,
child: Transform(
transform: _transform,
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 10,
offset: Offset(0, 4),
)
],
image: DecorationImage(
image: NetworkImage(widget.imageUrl),
fit: BoxFit.cover,
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _resetTransform,
child: Icon(Icons.refresh),
),
);
}
void _onScaleStart(ScaleStartDetails details) {
_previousTransform = _transform;
}
void _onScaleUpdate(ScaleUpdateDetails details) {
setState(() {
// 计算新的缩放比例
double newScale = _getScale(_previousTransform) * details.scale;
newScale = newScale.clamp(_minScale, _maxScale);
// 创建变换矩阵
_transform = Matrix4.identity()
..scale(newScale)
..translate(
details.focalPoint.dx / newScale - details.localFocalPosition.dx,
details.focalPoint.dy / newScale - details.localFocalPosition.dy,
);
});
}
void _onDoubleTap() {
setState(() {
// 双击切换原始大小和放大状态
final currentScale = _getScale(_transform);
final targetScale = currentScale == 1.0 ? 2.0 : 1.0;
_transform = Matrix4.identity()..scale(targetScale);
});
}
void _resetTransform() {
setState(() {
_transform = Matrix4.identity();
});
}
double _getScale(Matrix4 matrix) {
// 从变换矩阵中提取缩放值
return matrix.getMaxScaleOnAxis();
}
}
3. 手势冲突解决
3.1 手势冲突类型分析
手势冲突主要分为三种类型,我们可以用下面的UML图来表示:
classDiagram
class GestureConflict {
<<enumeration>>
ParentChild
Sibling
SameType
}
class ParentChildConflict {
+String description
+Solution solution
}
class SiblingConflict {
+String description
+Solution solution
}
class SameTypeConflict {
+String description
+Solution solution
}
GestureConflict <|-- ParentChildConflict
GestureConflict <|-- SiblingConflict
GestureConflict <|-- SameTypeConflict
具体冲突类型说明:
-
父子组件冲突
- 现象:父组件和子组件都有相同类型的手势识别
- 案例:可点击的卡片中包含可点击的按钮
- 解决方法:使用
HitTestBehavior控制事件传递
-
兄弟组件冲突
- 现象:相邻组件的手势区域重叠
- 案例:两个重叠的可拖拽元素
- 解决方法:使用
Listener精确控制事件处理
-
同类型手势冲突
- 现象:同一组件注册了多个相似手势
- 案例:同时监听点击和双击
- 解决方法:设置手势识别优先级
3.2 冲突解决具体方案
方案1:使用HitTestBehavior
class HitTestBehaviorExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
// 父组件手势
onTap: () => print('父组件点击'),
behavior: HitTestBehavior.translucent, // 关键设置
child: Container(
color: Colors.blue[100],
padding: EdgeInsets.all(50),
child: GestureDetector(
// 子组件手势
onTap: () => print('子组件点击'),
child: Container(
width: 200,
height: 200,
color: Colors.red[100],
child: Center(child: Text('点击测试区域')),
),
),
),
),
);
}
}
方案2:使用IgnorePointer和AbsorbPointer
class PointerControlExample extends StatefulWidget {
@override
_PointerControlExampleState createState() => _PointerControlExampleState();
}
class _PointerControlExampleState extends State<PointerControlExample> {
bool _ignoreChild = false;
bool _absorbPointer = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('指针控制案例')),
body: Column(
children: [
// 控制面板
_buildControlPanel(),
Expanded(
child: Stack(
children: [
// 底层组件
GestureDetector(
onTap: () => print('底层组件被点击'),
child: Container(
color: Colors.blue[200],
child: Center(child: Text('底层组件')),
),
),
// 根据条件包装子组件
if (_ignoreChild)
IgnorePointer(
child: _buildTopLayer('IgnorePointer'),
)
else if (_absorbPointer)
AbsorbPointer(
child: _buildTopLayer('AbsorbPointer'),
)
else
_buildTopLayer('正常模式'),
],
),
),
],
),
);
}
Widget _buildControlPanel() {
return Container(
padding: EdgeInsets.all(16),
color: Colors.grey[100],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () => setState(() {
_ignoreChild = false;
_absorbPointer = false;
}),
child: Text('正常'),
),
ElevatedButton(
onPressed: () => setState(() {
_ignoreChild = true;
_absorbPointer = false;
}),
child: Text('IgnorePointer'),
),
ElevatedButton(
onPressed: () => setState(() {
_ignoreChild = false;
_absorbPointer = true;
}),
child: Text('AbsorbPointer'),
),
],
),
);
}
Widget _buildTopLayer(String mode) {
return Positioned(
bottom: 50,
right: 50,
child: GestureDetector(
onTap: () => print('顶层组件被点击 - $mode'),
child: Container(
width: 200,
height: 150,
color: Colors.red[200],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('顶层组件'),
Text('模式: $mode', style: TextStyle(fontWeight: FontWeight.bold)),
],
),
),
),
),
);
}
}
4. 自定义手势识别
4.1 架构图
自定义手势识别器的实现基于以下类结构:
graph TD
A[GestureRecognizer] --> B[OneSequenceGestureRecognizer]
B --> C[自定义识别器]
C --> D[addPointer]
C --> E[handleEvent]
C --> F[resolve]
D --> G[开始跟踪指针]
E --> H[处理事件序列]
F --> I[决定竞争结果]
H --> J{Ptr Down}
H --> K{Ptr Move}
H --> L{Ptr Up}
J --> M[记录起始状态]
K --> N[更新手势数据]
L --> O[触发最终回调]
4.2 实现自定义滑动手势
// 自定义滑动手势
class SwipeGestureRecognizer extends OneSequenceGestureRecognizer {
final VoidCallback? onSwipeLeft;
final VoidCallback? onSwipeRight;
final VoidCallback? onSwipeUp;
final VoidCallback? onSwipeDown;
// 配置参数
static const double _minSwipeDistance = 50.0; // 最小滑动距离
static const double _minSwipeVelocity = 100.0; // 最小滑动速度
// 状态变量
Offset? _startPosition;
Offset? _currentPosition;
int? _trackedPointer;
DateTime? _startTime;
@override
void addPointer(PointerDownEvent event) {
print('跟踪指针: ${event.pointer}');
startTrackingPointer(event.pointer);
_startPosition = event.position;
_currentPosition = event.position;
_trackedPointer = event.pointer;
_startTime = DateTime.now();
// 声明参与竞争
resolve(GestureDisposition.accepted);
}
@override
void handleEvent(PointerEvent event) {
if (event.pointer != _trackedPointer) return;
if (event is PointerMoveEvent) {
_currentPosition = event.position;
} else if (event is PointerUpEvent) {
_evaluateSwipe();
stopTrackingPointer(event.pointer);
_reset();
} else if (event is PointerCancelEvent) {
stopTrackingPointer(event.pointer);
_reset();
}
}
void _evaluateSwipe() {
if (_startPosition == null || _currentPosition == null || _startTime == null) {
return;
}
final offset = _currentPosition! - _startPosition!;
final distance = offset.distance;
final duration = DateTime.now().difference(_startTime!);
final velocity = distance / duration.inMilliseconds * 1000;
print('滑动评估 - 距离: ${distance.toStringAsFixed(1)}, '
'速度: ${velocity.toStringAsFixed(1)}, 方向: $offset');
// 检查是否达到滑动阈值
if (distance >= _minSwipeDistance && velocity >= _minSwipeVelocity) {
// 判断滑动方向
if (offset.dx.abs() > offset.dy.abs()) {
// 水平滑动
if (offset.dx > 0) {
print('向右滑动');
onSwipeRight?.call();
} else {
print('向左滑动');
onSwipeLeft?.call();
}
} else {
// 垂直滑动
if (offset.dy > 0) {
print('向下滑动');
onSwipeDown?.call();
} else {
print('向上滑动');
onSwipeUp?.call();
}
}
} else {
print('滑动未达到阈值');
}
}
void _reset() {
_startPosition = null;
_currentPosition = null;
_trackedPointer = null;
_startTime = null;
}
@override
void didStopTrackingLastPointer(int pointer) {
print('停止跟踪指针: $pointer');
}
@override
String get debugDescription => 'swipe_gesture';
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
stopTrackingPointer(pointer);
_reset();
}
}
// 使用自定义手势的组件
class SwipeDetector extends StatelessWidget {
final Widget child;
final VoidCallback? onSwipeLeft;
final VoidCallback? onSwipeRight;
final VoidCallback? onSwipeUp;
final VoidCallback? onSwipeDown;
const SwipeDetector({
Key? key,
required this.child,
this.onSwipeLeft,
this.onSwipeRight,
this.onSwipeUp,
this.onSwipeDown,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return RawGestureDetector(
gestures: {
SwipeGestureRecognizer: GestureRecognizerFactoryWithHandlers<
SwipeGestureRecognizer>(
() => SwipeGestureRecognizer(),
(SwipeGestureRecognizer instance) {
instance
..onSwipeLeft = onSwipeLeft
..onSwipeRight = onSwipeRight
..onSwipeUp = onSwipeUp
..onSwipeDown = onSwipeDown;
},
),
},
child: child,
);
}
}
// 调用规则
class SwipeExample extends StatefulWidget {
@override
_SwipeExampleState createState() => _SwipeExampleState();
}
class _SwipeExampleState extends State<SwipeExample> {
String _swipeDirection = '等待滑动手势...';
Color _backgroundColor = Colors.white;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('自定义滑动手势')),
body: SwipeDetector(
onSwipeLeft: () => _handleSwipe('左滑', Colors.red[100]!),
onSwipeRight: () => _handleSwipe('右滑', Colors.blue[100]!),
onSwipeUp: () => _handleSwipe('上滑', Colors.green[100]!),
onSwipeDown: () => _handleSwipe('下滑', Colors.orange[100]!),
child: Container(
color: _backgroundColor,
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.swipe, size: 80, color: Colors.grey),
SizedBox(height: 20),
Text(
_swipeDirection,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'在任意位置滑动试试',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
SizedBox(height: 30),
_buildDirectionIndicators(),
],
),
),
),
);
}
void _handleSwipe(String direction, Color color) {
setState(() {
_swipeDirection = '检测到: $direction';
_backgroundColor = color;
});
// 2秒后恢复初始状态
Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_swipeDirection = '等待滑动手势...';
_backgroundColor = Colors.white;
});
}
});
}
Widget _buildDirectionIndicators() {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Icon(Icons.arrow_upward, size: 40, color: Colors.green),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Icon(Icons.arrow_back, size: 40, color: Colors.red),
Text('滑动方向', style: TextStyle(fontSize: 16)),
Icon(Icons.arrow_forward, size: 40, color: Colors.blue),
],
),
Icon(Icons.arrow_downward, size: 40, color: Colors.orange),
],
),
);
}
}
5. 交互式画板案例
5.1 画板应用架构设计
graph TB
A[DrawingBoard] --> B[Toolbar]
A --> C[CanvasArea]
B --> D[ColorPicker]
B --> E[BrushSizeSlider]
B --> F[ActionButtons]
C --> G[GestureDetector]
G --> H[CustomPaint]
H --> I[DrawingPainter]
I --> J[Path数据]
subgraph 状态管理
K[DrawingState]
L[Path列表]
M[当前设置]
end
J --> L
D --> M
E --> M
5.2 画板应用实现
// 绘图路径数据类
class DrawingPath {
final List<Offset> points;
final Color color;
final double strokeWidth;
final PaintMode mode;
DrawingPath({
required this.points,
required this.color,
required this.strokeWidth,
this.mode = PaintMode.draw,
});
}
enum PaintMode { draw, erase }
// 主画板组件
class DrawingBoard extends StatefulWidget {
@override
_DrawingBoardState createState() => _DrawingBoardState();
}
class _DrawingBoardState extends State<DrawingBoard> {
// 绘图状态
final List<DrawingPath> _paths = [];
DrawingPath? _currentPath;
// 画笔设置
Color _selectedColor = Colors.black;
double _strokeWidth = 3.0;
PaintMode _paintMode = PaintMode.draw;
// 颜色选项
final List<Color> _colorOptions = [
Colors.black,
Colors.red,
Colors.blue,
Colors.green,
Colors.orange,
Colors.purple,
Colors.brown,
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('交互式画板'),
backgroundColor: Colors.deepPurple,
actions: [
IconButton(
icon: Icon(Icons.undo),
onPressed: _undo,
tooltip: '撤销',
),
IconButton(
icon: Icon(Icons.delete),
onPressed: _clear,
tooltip: '清空',
),
],
),
body: Column(
children: [
// 工具栏
_buildToolbar(),
// 画布区域
Expanded(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey[100]!, Colors.grey[200]!],
),
),
child: GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: CustomPaint(
painter: _DrawingPainter(_paths),
size: Size.infinite,
),
),
),
),
// 状态栏
_buildStatusBar(),
],
),
);
}
Widget _buildToolbar() {
return Container(
padding: EdgeInsets.all(12),
color: Colors.white,
child: Column(
children: [
// 颜色选择
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('颜色:', style: TextStyle(fontWeight: FontWeight.bold)),
Wrap(
spacing: 8,
children: _colorOptions.map((color) {
return GestureDetector(
onTap: () => setState(() {
_selectedColor = color;
_paintMode = PaintMode.draw;
}),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _selectedColor == color ?
Colors.black : Colors.transparent,
width: 3,
),
),
),
);
}).toList(),
),
// 橡皮擦按钮
GestureDetector(
onTap: () => setState(() {
_paintMode = PaintMode.erase;
}),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: _paintMode == PaintMode.erase ?
Colors.grey[300] : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.auto_fix_high,
color: _paintMode == PaintMode.erase ?
Colors.red : Colors.grey,
),
),
),
],
),
SizedBox(height: 12),
// 笔刷大小
Row(
children: [
Text('笔刷大小:', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(
child: Slider(
value: _strokeWidth,
min: 1,
max: 20,
divisions: 19,
onChanged: (value) => setState(() {
_strokeWidth = value;
}),
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: Text(
'${_strokeWidth.toInt()}px',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
],
),
);
}
Widget _buildStatusBar() {
return Container(
padding: EdgeInsets.all(8),
color: Colors.black87,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_paintMode == PaintMode.draw ? '绘图模式' : '橡皮擦模式',
style: TextStyle(color: Colors.white),
),
Text(
'路径数量: ${_paths.length}',
style: TextStyle(color: Colors.white),
),
],
),
);
}
void _onPanStart(DragStartDetails details) {
setState(() {
_currentPath = DrawingPath(
points: [details.localPosition],
color: _paintMode == PaintMode.erase ? Colors.white : _selectedColor,
strokeWidth: _paintMode == PaintMode.erase ? _strokeWidth * 2 : _strokeWidth,
mode: _paintMode,
);
_paths.add(_currentPath!);
});
}
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
_currentPath?.points.add(details.localPosition);
});
}
void _onPanEnd(DragEndDetails details) {
_currentPath = null;
}
void _undo() {
if (_paths.isNotEmpty) {
setState(() {
_paths.removeLast();
});
}
}
void _clear() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('清空画板'),
content: Text('确定要清空所有绘图吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
TextButton(
onPressed: () {
setState(() {
_paths.clear();
});
Navigator.pop(context);
},
child: Text('清空'),
),
],
),
);
}
}
// 绘图绘制器
class _DrawingPainter extends CustomPainter {
final List<DrawingPath> paths;
_DrawingPainter(this.paths);
@override
void paint(Canvas canvas, Size size) {
// 绘制背景网格
_drawBackgroundGrid(canvas, size);
// 绘制所有路径
for (final path in paths) {
final paint = Paint()
..color = path.color
..strokeWidth = path.strokeWidth
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
// 绘制路径
if (path.points.length > 1) {
final pathPoints = Path();
pathPoints.moveTo(path.points[0].dx, path.points[0].dy);
for (int i = 1; i < path.points.length; i++) {
pathPoints.lineTo(path.points[i].dx, path.points[i].dy);
}
canvas.drawPath(pathPoints, paint);
}
}
}
void _drawBackgroundGrid(Canvas canvas, Size size) {
final gridPaint = Paint()
..color = Colors.grey[300]!
..strokeWidth = 0.5;
const gridSize = 20.0;
// 绘制垂直线
for (double x = 0; x < size.width; x += gridSize) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
// 绘制水平线
for (double y = 0; y < size.height; y += gridSize) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
6. 性能优化
6.1 手势性能优化策略
下面我们可以详细了解各种优化策略的效果:
| 优化策略 | 解决方法 | 应用场景 |
|---|---|---|
| 减少GestureDetector嵌套 | 合并相邻手势检测器 | 复杂布局、列表项 |
| 使用InkWell替代 | 简单点击使用InkWell | 按钮、列表项点击 |
| 合理使用HitTestBehavior | 精确控制命中测试范围 | 重叠组件、透明区域 |
| 避免频繁setState | 使用TransformController | 拖拽、缩放操作 |
| 列表项手势优化 | 使用NotificationListener | 长列表、复杂手势 |
6.2 实际案例优化
class OptimizedGestureExample extends StatefulWidget {
@override
_OptimizedGestureExampleState createState() => _OptimizedGestureExampleState();
}
class _OptimizedGestureExampleState extends State<OptimizedGestureExample> {
final TransformationController _transformController = TransformationController();
final List<Widget> _items = [];
@override
void initState() {
super.initState();
// 初始化
_initializeItems();
}
void _initializeItems() {
for (int i = 0; i < 50; i++) {
_items.add(
OptimizedListItem(
index: i,
onTap: () => print('Item $i tapped'),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('优化手势')),
body: Column(
children: [
// 可缩放拖拽区域
Expanded(
flex: 2,
child: InteractiveViewer(
transformationController: _transformController,
boundaryMargin: EdgeInsets.all(20),
minScale: 0.1,
maxScale: 4.0,
child: Container(
color: Colors.blue[50],
child: Center(
child: FlutterLogo(size: 150),
),
),
),
),
// 优化列表
Expanded(
flex: 3,
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
// 可以在这里处理滚动优化
return false;
},
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) => _items[index],
),
),
),
],
),
);
}
@override
void dispose() {
_transformController.dispose();
super.dispose();
}
}
// 优化的列表项组件
class OptimizedListItem extends StatelessWidget {
final int index;
final VoidCallback onTap;
const OptimizedListItem({
required this.index,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
elevation: 2,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$index',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
SizedBox(width: 16),
Expanded(
child: Text(
'优化列表项 $index',
style: TextStyle(fontSize: 16),
),
),
Icon(Icons.chevron_right, color: Colors.grey),
],
),
),
),
),
);
}
}
总结
至此,手势识别相关知识点全部讲完了,通过本节的学习,我们掌握了Flutter手势识别的完整知识体系:GestureDetector,拖拽与缩放,手势冲突解决,自定义手势识别
对于不同阶段的开发者,建议按以下路径学习:
graph LR
A[初学者] --> B[基础手势]
B --> C[拖拽缩放]
C --> D[中级开发者]
D --> E[手势冲突解决]
E --> F[性能优化]
F --> G[高级开发者]
G --> H[自定义手势]
H --> I[复杂交互系统]
如果觉得这篇文章对你有帮助,别忘了一键三连(点赞、关注、收藏)!你的支持是我持续创作的最大动力!有任何问题欢迎在评论区留言,我会及时解答!