Gestures
Flutter has two gesture layers: high-level GestureDetector (taps, drags, scales) and low-level RawGestureDetector + custom recognisers. Use GestureDetector for 90% of needs, swap to InkWell for Material ripple, and reach for the lower-level APIs only when you need to disambiguate (drag vs scroll, pan vs scale).
Tap, double-tap, drag, swipe-to-dismiss, pinch
EXAMPLE
import 'package:flutter/material.dart';
// 1) Tap, long-press, double-tap
class TapDemo extends StatelessWidget {
const TapDemo({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => debugPrint('tap'),
onDoubleTap: () => debugPrint('double tap'),
onLongPress: () => debugPrint('long press'),
child: Container(width: 120, height: 80, color: Colors.indigo,
alignment: Alignment.center,
child: const Text('Tap me', style: TextStyle(color: Colors.white))),
);
}
}
// 2) Drag — track translation while pointer is down
class DragBox extends StatefulWidget {
const DragBox({super.key});
@override
State<DragBox> createState() => _DragBoxState();
}
class _DragBoxState extends State<DragBox> {
Offset pos = Offset.zero;
@override
Widget build(BuildContext c) {
return Stack(children: [
Positioned(
left: pos.dx, top: pos.dy,
child: GestureDetector(
onPanUpdate: (d) => setState(() => pos += d.delta),
child: Container(width: 80, height: 80, color: Colors.orange),
),
),
]);
}
}
// 3) Swipe-to-dismiss row (Dismissible)
class SwipeList extends StatefulWidget {
const SwipeList({super.key});
@override
State<SwipeList> createState() => _SwipeListState();
}
class _SwipeListState extends State<SwipeList> {
final items = List.generate(20, (i) => 'Item $i');
@override
Widget build(BuildContext c) {
return ListView(children: [
for (final id in List.of(items))
Dismissible(
key: ValueKey(id),
direction: DismissDirection.endToStart,
background: Container(color: Colors.red, alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16), child: const Icon(Icons.delete, color: Colors.white)),
confirmDismiss: (_) async {
return await showDialog<bool>(context: c, builder: (_) => AlertDialog(
title: const Text('Delete?'),
actions: [
TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('No')),
TextButton(onPressed: () => Navigator.pop(c, true), child: const Text('Yes')),
],
)) ?? false;
},
onDismissed: (_) => setState(() => items.remove(id)),
child: ListTile(title: Text(id)),
),
]);
}
}
// 4) Pinch + rotate via scale gesture
class PinchImage extends StatefulWidget {
const PinchImage({super.key, required this.url});
final String url;
@override
State<PinchImage> createState() => _PinchImageState();
}
class _PinchImageState extends State<PinchImage> {
double scale = 1, baseScale = 1;
double rotation = 0, baseRotation = 0;
@override
Widget build(BuildContext c) {
return GestureDetector(
onScaleStart: (d) {
baseScale = scale; baseRotation = rotation;
},
onScaleUpdate: (d) {
setState(() {
scale = (baseScale * d.scale).clamp(0.5, 5.0);
rotation = baseRotation + d.rotation;
});
},
child: Transform(
alignment: Alignment.center,
transform: Matrix4.identity()..scale(scale)..rotateZ(rotation),
child: Image.network(widget.url),
),
);
}
}
// 5) Ink ripple (Material feedback) — use InkWell, not GestureDetector
class RippleCard extends StatelessWidget {
const RippleCard({super.key, required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext c) {
return Card(
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: const Padding(padding: EdgeInsets.all(16), child: Text('Tap me with ripple')),
),
);
}
}
// 6) Disambiguation — scroll vs swipe
// In a vertical ListView with horizontal-swipe items, set direction explicitly
// and use Dismissible's direction to limit the gesture; this lets ListView
// own vertical, Dismissible own horizontal.
Why it matters
Default to InkWell over GestureDetector inside Material apps — InkWell handles the visual feedback (ripple, hover) plus the same tap callbacks, which keeps the UX consistent with the rest of the platform. Reach for GestureDetector only when you need a gesture InkWell does not expose (long press in some configurations, multi-finger).
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
GestureDetector(
onTap: () => print('tap'),
onLongPress: () => print('long press'),
onPanUpdate: (d) => print(d.delta),
child: const Icon(Icons.touch_app, size: 64),
)
Try it Yourself »
Discussion
Loading…