Menu contestuali e azioni al tocco prolungato in Flutter con GestureDetector e PopupMenuButton

Foto di Enrique Vidal Flores su Unsplash

GuideIntermedio35 min Flutter 3.x

Menu contestuali e azioni al tocco prolungato in Flutter con GestureDetector e PopupMenuButton

I gesti sono il cuore dell'interazione in un'app mobile. In questo tutorial vedremo come gestire i vari tipi di tocco in Flutter usando GestureDetector, come mostrare un menu contestuale con PopupMenuButton e come costruire un menu che appare al tocco prolungato in una posizione arbitraria dello schermo tramite showMenu.

Alla fine avrai una lista di elementi con azioni contestuali (modifica, condividi, elimina) attivabili con un long press, un pattern usato in moltissime app reali.

  1. 1

    Capire i gesti disponibili con GestureDetector

    GestureDetector è un widget invisibile che avvolge un figlio e ne intercetta i gesti. I callback più usati sono:

    • onTap: tocco singolo
    • onDoubleTap: doppio tocco
    • onLongPress: tocco prolungato
    • onLongPressStart: fornisce la posizione globale del tocco

    Partiamo con un esempio semplice che reagisce ai tre gesti principali mostrando uno SnackBar.

    GestureDetector(
      onTap: () => _mostraMessaggio(context, 'Tap singolo'),
      onDoubleTap: () => _mostraMessaggio(context, 'Doppio tap'),
      onLongPress: () => _mostraMessaggio(context, 'Long press'),
      child: Container(
        padding: const EdgeInsets.all(24),
        color: Colors.indigo.shade50,
        child: const Text('Prova i gesti qui'),
      ),
    );
    
    void _mostraMessaggio(BuildContext context, String testo) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(testo), duration: const Duration(milliseconds: 800)),
      );
    }

    Risultato atteso

    Toccando il riquadro appare uno SnackBar diverso a seconda del gesto eseguito.

  2. 2

    Preparare il modello dati e la lista

    Creiamo una semplice lista di note su cui applicheremo i menu contestuali. Definiamo un modello Nota e uno StatefulWidget che mantiene la lista in memoria.

    class Nota {
      final int id;
      String titolo;
      Nota({required this.id, required this.titolo});
    }
    
    class NotePage extends StatefulWidget {
      const NotePage({super.key});
      @override
      State<NotePage> createState() => _NotePageState();
    }
    
    class _NotePageState extends State<NotePage> {
      final List<Nota> _note = [
        Nota(id: 1, titolo: 'Spesa settimanale'),
        Nota(id: 2, titolo: 'Idee per il progetto'),
        Nota(id: 3, titolo: 'Chiamare il dentista'),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Le mie note')),
          body: ListView.builder(
            itemCount: _note.length,
            itemBuilder: (context, index) {
              final nota = _note[index];
              return ListTile(title: Text(nota.titolo));
            },
          ),
        );
      }
    }

    Risultato atteso

    Viene visualizzata una schermata con tre note in una lista scorrevole.

  3. 3

    Aggiungere un menu contestuale con PopupMenuButton

    PopupMenuButton è il modo più rapido per aggiungere un menu di azioni tramite un'icona (di solito i tre puntini). Aggiungiamolo come trailing di ogni ListTile. Il callback onSelected riceve il valore dell'elemento scelto.

    ListTile(
      title: Text(nota.titolo),
      trailing: PopupMenuButton<String>(
        icon: const Icon(Icons.more_vert),
        onSelected: (valore) => _gestisciAzione(valore, nota),
        itemBuilder: (context) => const [
          PopupMenuItem(
            value: 'modifica',
            child: ListTile(leading: Icon(Icons.edit), title: Text('Modifica')),
          ),
          PopupMenuItem(
            value: 'condividi',
            child: ListTile(leading: Icon(Icons.share), title: Text('Condividi')),
          ),
          PopupMenuDivider(),
          PopupMenuItem(
            value: 'elimina',
            child: ListTile(
              leading: Icon(Icons.delete, color: Colors.red),
              title: Text('Elimina', style: TextStyle(color: Colors.red)),
            ),
          ),
        ],
      ),
    )

    Risultato atteso

    Ogni nota mostra un'icona a tre puntini che, se toccata, apre un menu con le azioni Modifica, Condividi ed Elimina.

  4. 4

    Implementare la logica delle azioni

    Ora scriviamo il metodo _gestisciAzione che reagisce alla scelta dell'utente. Per l'eliminazione mostriamo un dialogo di conferma, per la modifica un semplice AlertDialog con un campo di testo.

    void _gestisciAzione(String azione, Nota nota) {
      switch (azione) {
        case 'modifica':
          _modificaNota(nota);
          break;
        case 'condividi':
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Condivido: ${nota.titolo}')),
          );
          break;
        case 'elimina':
          _confermaEliminazione(nota);
          break;
      }
    }
    
    Future<void> _confermaEliminazione(Nota nota) async {
      final conferma = await showDialog<bool>(
        context: context,
        builder: (ctx) => AlertDialog(
          title: const Text('Eliminare la nota?'),
          content: Text('Vuoi eliminare "${nota.titolo}"?'),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Annulla')),
            TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Elimina')),
          ],
        ),
      );
      if (conferma == true) {
        setState(() => _note.removeWhere((n) => n.id == nota.id));
      }
    }
    
    Future<void> _modificaNota(Nota nota) async {
      final controller = TextEditingController(text: nota.titolo);
      final nuovo = await showDialog<String>(
        context: context,
        builder: (ctx) => AlertDialog(
          title: const Text('Modifica nota'),
          content: TextField(controller: controller, autofocus: true),
          actions: [
            TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Annulla')),
            TextButton(onPressed: () => Navigator.pop(ctx, controller.text), child: const Text('Salva')),
          ],
        ),
      );
      if (nuovo != null && nuovo.trim().isNotEmpty) {
        setState(() => nota.titolo = nuovo.trim());
      }
    }

    Risultato atteso

    Selezionando Modifica si può rinominare la nota, Condividi mostra uno SnackBar ed Elimina chiede conferma prima di rimuovere l'elemento.

  5. 5

    Mostrare il menu al long press con showMenu

    Un pattern molto comune è far comparire il menu contestuale premendo a lungo sull'elemento, esattamente nel punto in cui l'utente ha toccato. Usiamo onLongPressStart per catturare la posizione globale e la funzione showMenu, che accetta un RelativeRect per posizionare il popup.

    Avvolgiamo il ListTile in un GestureDetector e riutilizziamo la logica delle azioni.

    Future<void> _mostraMenuContestuale(Offset posizione, Nota nota) async {
      final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
      final valore = await showMenu<String>(
        context: context,
        position: RelativeRect.fromRect(
          posizione & const Size(40, 40),
          Offset.zero & overlay.size,
        ),
        items: const [
          PopupMenuItem(value: 'modifica', child: Text('Modifica')),
          PopupMenuItem(value: 'condividi', child: Text('Condividi')),
          PopupMenuItem(value: 'elimina', child: Text('Elimina')),
        ],
      );
      if (valore != null) _gestisciAzione(valore, nota);
    }
    
    // Nell'itemBuilder della ListView:
    return GestureDetector(
      onLongPressStart: (dettagli) =>
          _mostraMenuContestuale(dettagli.globalPosition, nota),
      child: ListTile(title: Text(nota.titolo)),
    );

    Risultato atteso

    Tenendo premuto su una nota, il menu contestuale appare vicino al punto toccato con le stesse azioni.

  6. 6

    Feedback aptico e considerazioni finali

    Per rendere l'esperienza più naturale, aggiungiamo un leggero feedback aptico all'apertura del menu tramite HapticFeedback, incluso nel pacchetto services di Flutter.

    Best practice:

    • Preferisci PopupMenuButton per un menu sempre visibile (icona), showMenu + long press per azioni "nascoste".
    • Non abusare del doppio tap: molti utenti non lo scoprono.
    • Fornisci sempre un feedback visivo o aptico per confermare che il gesto è stato riconosciuto.
    • Ricorda che GestureDetector non aggiunge effetti ripple: se vuoi il feedback Material usa InkWell.
    import 'package:flutter/services.dart';
    
    onLongPressStart: (dettagli) {
      HapticFeedback.mediumImpact();
      _mostraMenuContestuale(dettagli.globalPosition, nota);
    },

    Risultato atteso

    Il long press produce una leggera vibrazione prima di mostrare il menu, migliorando la percezione dell'interazione.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!