Flutter Lesson 32 of 83 3 min read
Gestures in Flutter: GestureDetector, InkWell and Dismissible
Handle taps, long presses, drags and swipes in Flutter with GestureDetector and InkWell, and build swipe-to-delete with Dismissible.
On this page
Buttons handle taps for you. To make any widget respond to touch, wrap it in a gesture widget.
InkWell: a tap with a ripple #
Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => debugPrint('Card tapped'),
onLongPress: () => debugPrint('Card long-pressed'),
child: const Padding(
padding: EdgeInsets.all(24),
child: Text('Tap me'),
),
),
)
InkWell shows the Material ripple, so the user sees their touch register. It needs a Material ancestor such as Card, Scaffold or Material to draw on. Use it for anything that should feel like a button.
GestureDetector: every gesture, no visual effect #
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: TapBox()))));
class TapBox extends StatefulWidget {
const TapBox({super.key});
@override
State<TapBox> createState() => _TapBoxState();
}
class _TapBoxState extends State<TapBox> {
String _last = 'Try a gesture';
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _last = 'Tap'),
onDoubleTap: () => setState(() => _last = 'Double tap'),
onLongPress: () => setState(() => _last = 'Long press'),
child: Container(
width: 220,
height: 220,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.teal.shade100,
borderRadius: BorderRadius.circular(24),
),
child: Text(_last, style: const TextStyle(fontSize: 20)),
),
);
}
}
| Callback | Fires on |
|---|---|
onTap, onDoubleTap, onLongPress | Taps |
onTapDown, onTapUp | Finger down and up, with the position |
onPanStart, onPanUpdate, onPanEnd | Dragging in any direction |
onHorizontalDragUpdate, onVerticalDragUpdate | Dragging along one axis |
onScaleUpdate | Pinch to zoom and rotate |
Empty areas do not receive taps #
By default a GestureDetector only reacts where its child paints something. A Row with a gap in the middle ignores taps on the gap. Fix it with behavior:
GestureDetector(
behavior: HitTestBehavior.opaque, // the whole area responds
onTap: _open,
child: const Row(children: [Text('Label'), Spacer(), Icon(Icons.chevron_right)]),
)
Dragging #
class _DraggableDotState extends State<DraggableDot> {
Offset _position = const Offset(100, 100);
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) => setState(() => _position += details.delta),
child: const CircleAvatar(radius: 28, child: Icon(Icons.open_with)),
),
),
],
);
}
}
details.delta is how far the finger moved since the last update.
Swipe to delete with Dismissible #
ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
final item = _items[index];
return Dismissible(
key: ValueKey(item), // must be unique for each item
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),
),
onDismissed: (_) {
setState(() => _items.removeAt(index));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$item removed'),
action: SnackBarAction(
label: 'Undo',
onPressed: () => setState(() => _items.insert(index, item)),
),
),
);
},
child: ListTile(title: Text(item)),
);
},
)
You must remove the item from your data in onDismissed. If it is still in the list on the next build, Flutter throws an error. To ask first, return a Future<bool> from confirmDismiss.
Zoom and pan #
InteractiveViewer adds pinch-to-zoom and panning to any child, with no gesture code.
InteractiveViewer(
minScale: 1,
maxScale: 4,
child: Image.network('https://picsum.photos/800/600'),
)
Reordering a list #
ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
},
children: [
for (final item in _items) ListTile(key: ValueKey(item), title: Text(item)),
],
)
Blocking touches #
AbsorbPointer(absorbing: _loading, child: const MyForm()) // swallows touches
IgnorePointer(child: const DecorativeOverlay()) // lets touches pass through
Guidelines #
- Make tap targets at least 48 by 48 logical pixels.
- Give visible feedback for every touch: a ripple, a colour change or movement.
- Hidden gestures such as swipe and long press are hard to discover. Offer a visible alternative too.
- For a tappable icon, use
IconButton, which already has the right size, ripple and tooltip.
Try it yourself #
Build a list of tasks where swiping left deletes with an Undo snackbar, tapping toggles a strikethrough, and long-pressing opens a bottom sheet with “Edit” and “Share” options.