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 sottostanti
  • translucent: 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 InkWell quando vuoi il feedback visivo Material (effetto ripple).
  • Evita di annidare troppi GestureDetector: complica la risoluzione dei conflitti.
  • Usa behavior: HitTestBehavior.opaque per zone tappabili con spazi vuoti.
  • Ricordati che onPanUpdate e onScaleUpdate non 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.