手势组件详解

手势交互是移动应用的核心功能。Flutter 提供了丰富的手势识别组件,可以轻松处理用户的触摸交互。本教程将详细介绍各种手势组件。

重要提示:Flutter 3.28+ 进一步优化了手势识别的性能和响应速度。

1. GestureDetector 基础手势

GestureDetector 是最常用的手势识别组件,可以识别多种手势。

📐 GestureDetector 手势类型

👆
onTap
点击
👆👆
onDoubleTap
双击
⏱️
onLongPress
长按
↔️
onHorizontalDrag
水平拖动
↕️
onVerticalDrag
垂直拖动
✌️
onScale
缩放
class GestureDetectorExample extends StatefulWidget {
  const GestureDetectorExample({super.key});

  @override
  State createState() =>
      _GestureDetectorExampleState();
}

class _GestureDetectorExampleState extends State {
  String _gesture = '等待手势';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        // 点击手势
        onTap: () {
          setState(() {
            _gesture = '点击';
          });
        },
        onDoubleTap: () {
          setState(() {
            _gesture = '双击';
          });
        },
        onLongPress: () {
          setState(() {
            _gesture = '长按';
          });
        },
        // 滑动手势
        onHorizontalDragStart: (details) {
          setState(() {
            _gesture = '水平滑动开始';
          });
        },
        onHorizontalDragUpdate: (details) {
          setState(() {
            _gesture = '水平滑动: ${details.globalPosition.dx}';
          });
        },
        onHorizontalDragEnd: (details) {
          setState(() {
            _gesture = '水平滑动结束';
          });
        },
        // 缩放手势
        onScaleStart: (details) {
          setState(() {
            _gesture = '缩放开始';
          });
        },
        onScaleUpdate: (details) {
          setState(() {
            _gesture = '缩放: ${details.scale}';
          });
        },
        onScaleEnd: (details) {
          setState(() {
            _gesture = '缩放结束';
          });
        },
        child: Container(
          width: 200,
          height: 200,
          color: Colors.blue,
          child: Center(
            child: Text(
              _gesture,
              style: const TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

2. InkWell 涟漪效果

InkWell 提供了 Material Design 的涟漪效果。

📐 InkWell 涟漪效果

点击前

点击我

点击时 (涟漪效果)

点击我

Material Design 规范的点击反馈效果

InkWell(
  onTap: () {
    print('点击了卡片');
  },
  onLongPress: () {
    print('长按了卡片');
  },
  splashColor: Colors.blue.withOpacity(0.3),
  highlightColor: Colors.blue.withOpacity(0.1),
  borderRadius: BorderRadius.circular(12),
  child: Container(
    width: 200,
    height: 100,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(12),
    ),
    child: const Center(
      child: Text(
        '点击我',
        style: TextStyle(color: Colors.white),
      ),
    ),
  ),
)

3. Dismissible 滑动删除

Dismissible 用于实现滑动删除功能。

📐 Dismissible 滑动删除效果

🗑️
← 向左滑动删除

← 滑动方向可配置 (startToEnd / endToStart)

class DismissibleExample extends StatelessWidget {
  const DismissibleExample({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 10,
      itemBuilder: (context, index) {
        return Dismissible(
          key: Key('item_$index'),
          onDismissed: (direction) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('删除了项目 $index')),
            );
          },
          direction: DismissDirection.endToStart,
          background: Container(
            color: Colors.red,
            alignment: Alignment.centerRight,
            padding: const EdgeInsets.only(right: 20),
            child: const Icon(Icons.delete, color: Colors.white),
          ),
          secondaryBackground: Container(
            color: Colors.green,
            alignment: Alignment.centerLeft,
            padding: const EdgeInsets.only(left: 20),
            child: const Icon(Icons.archive, color: Colors.white),
          ),
          child: ListTile(
            title: Text('项目 $index'),
            subtitle: Text('这是项目 $index 的描述'),
          ),
        );
      },
    );
  }
}

4. Draggable 拖拽

Draggable 用于实现可拖拽的组件。

📐 Draggable 拖拽效果

静态状态

A
B
C

拖拽状态

A
B
C
A

拖拽时显示 feedback,原位置显示 childWhenDragging(半透明)

class DraggableExample extends StatefulWidget {
  const DraggableExample({super.key});

  @override
  State createState() => _DraggableExampleState();
}

class _DraggableExampleState extends State {
  List _items = ['A', 'B', 'C', 'D', 'E'];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('拖拽下面的字母'),
        const SizedBox(height: 20),
        Wrap(
          spacing: 10,
          children: _items.map((item) {
            return Draggable(
              data: item,
              child: DraggableItem(text: item),
              feedback: DraggableItem(
                text: item,
                isDragging: true,
              ),
              childWhenDragging: DraggableItem(
                text: item,
                isDragging: false,
                opacity: 0.3,
              ),
            );
          }).toList(),
        ),
      ],
    );
  }
}

class DraggableItem extends StatelessWidget {
  final String text;
  final bool isDragging;
  final double? opacity;

  const DraggableItem({
    super.key,
    required this.text,
    this.isDragging = false,
    this.opacity,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 60,
      height: 60,
      decoration: BoxDecoration(
        color: isDragging ? Colors.blue : Colors.grey[300],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(
        child: Text(
          text,
          style: TextStyle(
            color: isDragging ? Colors.white : Colors.black,
            fontSize: 24,
          ),
        ),
      ),
    );
  }
}

5. DragTarget 拖放目标

DragTarget 用于接收拖拽的组件。

📐 DragTarget 拖放效果

空闲状态

拖拽
拖放目标
已接收: 0 项

悬停状态

拖拽
拖放目标
已接收: 0 项
拖拽

悬停时高亮显示(绿色背景+绿色边框),释放时触发 onAccept

class DragTargetExample extends StatefulWidget {
  const DragTargetExample({super.key});

  @override
  State createState() => _DragTargetExampleState();
}

class _DragTargetExampleState extends State {
  List _acceptedItems = [];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Draggable(
          data: '可拖拽项',
          child: const DraggableItem(text: '拖拽我'),
          feedback: const DraggableItem(text: '拖拽我', isDragging: true),
        ),
        const SizedBox(height: 40),
        DragTarget(
          onAcceptWithDetails: (details) {
            setState(() {
              _acceptedItems.add(details.data);
            });
          },
          builder: (context, candidateData, rejectedData) {
            return Container(
              width: 200,
              height: 200,
              decoration: BoxDecoration(
                color: candidateData.isNotEmpty
                    ? Colors.green.withOpacity(0.5)
                    : Colors.grey[300],
                border: Border.all(
                  color: candidateData.isNotEmpty ? Colors.green : Colors.grey,
                  width: 2,
                ),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Center(
                child: Text(
                  '拖放目标\n已接收: ${_acceptedItems.length} 项',
                  textAlign: TextAlign.center,
                ),
              ),
            );
          },
        ),
      ],
    );
  }
}

6. PageView 页面滑动

PageView 用于实现可滑动的页面视图。

📐 PageView 页面滑动效果

页面 1 (当前)

页面 1

页面 2 (当前)

页面 2

页面 3 (当前)

页面 3

左右滑动切换页面,底部指示器显示当前页面

class PageViewExample extends StatefulWidget {
  const PageViewExample({super.key});

  @override
  State createState() => _PageViewExampleState();
}

class _PageViewExampleState extends State {
  final PageController _pageController = PageController();
  int _currentPage = 0;

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SizedBox(
          height: 300,
          child: PageView.builder(
            controller: _pageController,
            onPageChanged: (index) {
              setState(() {
                _currentPage = index;
              });
            },
            itemCount: 5,
            itemBuilder: (context, index) {
              return Container(
                color: Colors.primaries[index % Colors.primaries.length],
                child: Center(
                  child: Text(
                    '页面 ${index + 1}',
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 32,
                    ),
                  ),
                ),
              );
            },
          ),
        ),
        const SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: List.generate(
            5,
            (index) => Container(
              margin: const EdgeInsets.symmetric(horizontal: 4),
              width: 8,
              height: 8,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: _currentPage == index ? Colors.blue : Colors.grey,
              ),
            ),
          ),
        ),
      ],
    );
  }
}

7. RefreshIndicator 下拉刷新

RefreshIndicator 用于实现下拉刷新功能。

📐 RefreshIndicator 下拉刷新效果

正常状态

项目 0
项目 1
项目 2
项目 3
项目 4

下拉刷新状态

正在刷新...
刷新项目 0
刷新项目 1
刷新项目 2
刷新项目 3

下拉列表触发刷新,显示旋转动画和刷新提示

class RefreshIndicatorExample extends StatefulWidget {
  const RefreshIndicatorExample({super.key});

  @override
  State createState() =>
      _RefreshIndicatorExampleState();
}

class _RefreshIndicatorExampleState extends State {
  List _items = List.generate(10, (index) => '项目 $index');

  Future _refresh() async {
    await Future.delayed(const Duration(seconds: 2));
    setState(() {
      _items = List.generate(10, (index) => '刷新项目 $index');
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _refresh,
      child: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_items[index]),
          );
        },
      ),
    );
  }
}

⏱️ 手势识别时序图

用户
GestureDetector
Widget
1. 触摸开始
用户手指按下 → GestureDetector.onTapDown → 开始识别
2. 手势处理
手指移动/抬起 → GestureDetector.onTap/onDoubleTap/onLongPress → 触发回调
3. 手势结束
手指离开 → GestureDetector.onTapUp → 完成识别

📊 手势类型对比

Flutter 手势类型对比表
手势 回调方法 触发条件 使用场景
点击 onTap 快速点击 按钮、卡片
双击 onDoubleTap 快速双击 点赞、放大
长按 onLongPress 长按500ms 菜单、选择
拖拽 onPanUpdate 手指移动 滑动、移动
缩放 onScaleUpdate 双指缩放 图片缩放

手势最佳实践

  • 优先使用 InkWell 而不是 GestureDetector 来实现点击效果
  • 为手势操作提供视觉反馈(涟漪效果、高亮等)
  • 合理设置手势的识别区域,避免误触
  • 使用 Dismissible 时提供明确的删除提示
  • 拖拽操作应该有清晰的视觉指示
  • 为长按操作提供上下文菜单
  • 测试手势在不同设备上的表现
提示:使用 RawGestureDetector 可以实现更复杂的手势识别。
下一步:掌握手势组件后,继续学习导航组件。