Introduzione
Le interazioni tattili sono il cuore di ogni applicazione mobile. In Flutter, la gestione dei gesti è potente e flessibile grazie a un sistema stratificato che va dal semplice GestureDetector fino ai GestureRecognizer personalizzati. In questo articolo vedremo come intercettare tap, drag, scale e come risolvere i conflitti tra gesti concorrenti.
GestureDetector: il punto di partenza
Il widget GestureDetector è il modo più immediato per rendere interattivo qualsiasi widget. Non ha una rappresentazione grafica propria: si limita ad avvolgere un child e a notificare gli eventi.
GestureDetector(
onTap: () => print('Tap singolo'),
onDoubleTap: () => print('Doppio tap'),
onLongPress: () => print('Pressione prolungata'),
child: Container(
padding: const EdgeInsets.all(24),
color: Colors.blue,
child: const Text('Toccami'),
),
)
Attenzione all'area di hit test
Un errore comune è avvolgere un Container senza colore o dimensioni: le zone trasparenti non ricevono eventi di default. Per intercettare i tap anche su aree vuote si usa behavior:
GestureDetector(
behavior: HitTestBehavior.opaque, // riceve tap su tutta l'area
onTap: () => print('Tap ovunque nell\'area'),
child: const SizedBox(width: 200, height: 100),
)
I valori possibili sono:
deferToChild: solo i child rispondono (default)opaque: l'intera area risponde e blocca i widget sottostantitranslucent: l'area risponde ma lascia passare gli eventi sotto
Gestire il drag e lo spostamento
Per trascinare un elemento possiamo usare i callback onPanUpdate che forniscono il delta dello spostamento.
class DraggableBox extends StatefulWidget {
const DraggableBox({super.key});
@override
State<DraggableBox> createState() => _DraggableBoxState();
}
class _DraggableBoxState extends State<DraggableBox> {
Offset _position = Offset.zero;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
left: _position.dx,
top: _position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
_position += details.delta;
});
},
child: Container(
width: 80,
height: 80,
color: Colors.deepPurple,
),
),
),
],
);
}
}
Gesti di scala e rotazione
I callback onScaleStart, onScaleUpdate e onScaleEnd gestiscono pinch-to-zoom e rotazioni. Nota che scale e pan non possono coesistere nello stesso GestureDetector, perché lo scale include già lo spostamento.
GestureDetector(
onScaleUpdate: (details) {
setState(() {
_scale = (_baseScale * details.scale).clamp(0.5, 4.0);
_rotation = details.rotation;
});
},
child: Transform(
transform: Matrix4.identity()
..scale(_scale)
..rotateZ(_rotation),
alignment: Alignment.center,
child: Image.asset('assets/foto.jpg'),
),
)
L'arena dei gesti: risolvere i conflitti
Quando più GestureRecognizer vogliono lo stesso puntatore, Flutter usa la Gesture Arena. Ogni recognizer "compete" e chi vince ottiene l'evento, gli altri vengono scartati. Questo spiega perché un tap all'interno di una ListView scrollabile funziona: il recognizer del tap vince se il dito non si muove, mentre lo scroll vince se c'è movimento.
RawGestureDetector e recognizer personalizzati
Quando serve un controllo fine, si usa RawGestureDetector con una mappa di factory. È utile per personalizzare il comportamento di un recognizer, come consentire più tap simultanei:
RawGestureDetector(
gestures: {
TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(),
(recognizer) {
recognizer.onTap = () => print('Tap personalizzato');
},
),
},
child: Container(color: Colors.orange, height: 100),
)
Listener: eventi puntatore a basso livello
Se hai bisogno di intercettare gli eventi grezzi del puntatore senza il sistema dell'arena, usa il widget Listener:
Listener(
onPointerDown: (event) => print('Giù: ${event.position}'),
onPointerMove: (event) => print('Muove: ${event.delta}'),
onPointerUp: (event) => print('Su'),
child: Container(color: Colors.green, height: 100),
)
Best practice
- Preferisci widget dedicati come
InkWellquando vuoi il feedback visivo Material (effetto ripple). - Evita di annidare troppi
GestureDetector: complica la risoluzione dei conflitti. - Usa
behavior: HitTestBehavior.opaqueper zone tappabili con spazi vuoti. - Ricordati che
onPanUpdateeonScaleUpdatenon convivono nello stesso detector.
Conclusione
Il sistema di gestione dei gesti di Flutter offre livelli di astrazione adatti a ogni esigenza: dal comodo GestureDetector per i casi comuni, fino a RawGestureDetector e Listener per scenari avanzati. Comprendere la Gesture Arena è la chiave per costruire interazioni robuste e prive di conflitti.
