Flutter: GestureDetector verwenden und Touch-Ereignisse
Zuletzt aktualisiert:
6 Min. Lesezeit

Mit GestureDetector hängen Sie Touch-Ereignisse an jedes Widget, das von sich aus nicht antippbar ist: einfaches und doppeltes Tippen, langes Drücken, Ziehen, Skalieren mit zwei Fingern. Das Widget zeichnet selbst nichts; es beobachtet nur die Fingerbewegungen über seinem Kind. Es ist das Werkzeug für alle Stellen, an denen fertige Buttons nicht ausreichen – etwa beim Zoomen eines Bildes oder beim Ziehen einer Karte.
Live-Demo
Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Grundlegende Verwendung
Übergeben Sie das Widget, das antippbar werden soll, als child und füllen Sie den passenden Callback:
GestureDetector(
onTap: () {
print('Angetippt!');
},
child: Container(
padding: const EdgeInsets.all(20),
color: Colors.blue,
child: const Text('Tipp mich an'),
),
)Unterstützte Gestenarten
Tipp-Ereignisse
GestureDetector(
onTap: () => print('Einfaches Tippen'),
onDoubleTap: () => print('Doppeltippen'),
onLongPress: () => print('Langes Drücken'),
onTapDown: (details) => print('Finger aufgesetzt: ${details.localPosition}'),
onTapUp: (details) => print('Finger angehoben'),
onTapCancel: () => print('Tippen abgebrochen'),
child: MyWidget(),
)onTapDown und onTapUp liefern zusätzlich die Koordinaten der Berührung; auf einer Zeichenfläche oder bei einem eigenen Schieberegler stammt die Information „wo wurde getippt“ von hier. onTapCancel wird ausgelöst, wenn der Finger aufgesetzt wurde, die Bewegung dann aber in ein Ziehen überging.
Zieh-Ereignisse
Für das Ziehen gibt es drei Familien: nur horizontal, nur vertikal und in jede Richtung (Pan). Wenn Sie nur die benötigte Richtung beobachten, erhalten Sie genauere Ergebnisse und weniger Konflikte mit scrollbaren Listen.
// Horizontales Ziehen
GestureDetector(
onHorizontalDragStart: (details) => print('Horizontales Ziehen gestartet'),
onHorizontalDragUpdate: (details) => print('Delta: ${details.delta.dx}'),
onHorizontalDragEnd: (details) => print('Geschwindigkeit: ${details.primaryVelocity}'),
child: MyWidget(),
)
// Vertikales Ziehen
GestureDetector(
onVerticalDragUpdate: (details) => print('Delta: ${details.delta.dy}'),
onVerticalDragEnd: (details) => print('Geschwindigkeit: ${details.primaryVelocity}'),
child: MyWidget(),
)
// Ziehen in beliebige Richtung
GestureDetector(
onPanStart: (details) => print('Pan gestartet'),
onPanUpdate: (details) => print('Position: ${details.localPosition}'),
onPanEnd: (details) => print('Pan beendet'),
child: MyWidget(),
)primaryVelocity in onHorizontalDragEnd ist die Geschwindigkeit des Fingers im Moment des Loslassens. Das Vorzeichen verrät die Richtung: positiv bedeutet ein Wischen nach rechts, negativ eines nach links.
Skalieren (Pinch)
GestureDetector(
onScaleStart: (details) => print('Skalierung gestartet'),
onScaleUpdate: (details) {
print('Skalierung: ${details.scale}');
print('Rotation: ${details.rotation}');
},
onScaleEnd: (details) => print('Skalierung beendet'),
child: MyWidget(),
)Ein Detail wird leicht übersehen: details.scale beginnt bei jeder Geste wieder bei 1.0, ist also relativ zu dieser Geste. Für einen Zoom, der erhalten bleibt, müssen Sie die Startskalierung speichern und multiplizieren; das Mini-Szenario weiter unten zeigt, wie.
GestureDetector oder InkWell?
Beide fangen Tippen ab; der Unterschied liegt in der Rückmeldung. GestureDetector gibt keinerlei visuelle Reaktion. InkWell zeichnet den Ripple-Effekt von Material Design und sagt dem Nutzer damit: „Deine Berührung wurde erkannt“:
InkWell(
onTap: () => print('Angetippt'),
onLongPress: () => print('Lange gedrückt'),
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Zeile mit Ripple'),
),
)| GestureDetector | InkWell | |
|---|---|---|
| Visuelle Rückmeldung | Keine | Ripple und Hervorhebung |
| Ziehen, Skalieren | Ja | Nein |
| Braucht einen Material-Vorfahren? | Nein | Ja |
| Typischer Einsatz | Bild, Zeichenfläche, eigene Interaktion | Listenzeile, Karte, eigener Button |
Als Faustregel: Alles, was sich wie ein Button verhält, bekommt ein InkWell (oder gleich einen fertigen Button); alles, was mehr als Tippen braucht, einen GestureDetector. Am häufigsten sitzt InkWell in einer Karte; das behandle ich im Card-Beitrag.
HitTestBehavior
Der Parameter behavior legt fest, in welchem Bereich eine Berührung erkannt wird. Standard (wenn ein Kind vorhanden ist) ist deferToChild: Nur die Punkte, an denen das Kind tatsächlich etwas zeichnet, fangen die Berührung ab.
GestureDetector(
behavior: HitTestBehavior.opaque, // Auch in leeren Bereichen erkennen
onTap: () => print('Angetippt'),
child: const SizedBox(
width: 200,
height: 200,
child: Center(child: Text('Tippen')),
),
)deferToChild: Nur die gezeichneten Teile des Kindes erhalten die Berührung. Tippen in die Lücken einer Fläche ohne Farbe bewirkt nichts.opaque: Das gesamte Rechteck des Widgets erhält die Berührung und lässt sie nicht zu dahinterliegenden Widgets durch.translucent: Die gesamte Fläche erhält die Berührung, dahinterliegende Widgets können dieselbe Berührung aber ebenfalls sehen.
Wann verwenden – und wann nicht?
Setzen Sie GestureDetector für Interaktionen ein, die kein fertiges Widget abdeckt: Doppeltippen zum Liken, den berührten Punkt auf einem Bild ermitteln, ein eigenes Ziehverhalten, Skalieren per Pinch.
Für die folgenden Aufgaben bringt Flutter besser geeignete Lösungen mit:
- Button-Verhalten:
ElevatedButton,IconButton,InkWell. Sie liefern Rückmeldung, Fokusbehandlung und Informationen für die Barrierefreiheit von selbst. Wer einenContainerper GestureDetector zum Button macht, verliert all das. - Wischen zum Löschen:
Dismissible. Animation und Schwellenlogik sind bereits eingebaut. - Zoomen und Verschieben:
InteractiveViewer. Für Inhalte wie Karten oder große Bilder, samt Begrenzung der Ränder. - Drag-and-drop:
DraggableundDragTarget. - Inhalte scrollen: scrollbare Widgets wie ListView. Wer innerhalb einer scrollbaren Liste auf ein Ziehen in dieselbe Richtung hört, lässt beide Seiten gegeneinander antreten.
Häufige Fehler
1. Leere Fläche nimmt das Tippen nicht an
Symptom: Sie haben eine Row oder einen farblosen Container in einen GestureDetector gepackt; ein Tipp auf den Text funktioniert, ein Tipp in die Lücke daneben nicht. Ursache ist das Standardverhalten deferToChild. Die Lösung ist eine Zeile:
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _openDetail,
child: Row(
children: const [Text('Titel'), Spacer(), Icon(Icons.chevron_right)],
),
)2. InkWell ohne Material-Vorfahren oder unsichtbarer Ripple
Das erste Symptom ist der Fehler „No Material widget found“ auf rotem Bildschirm: InkWell zeichnet seinen Ripple auf das nächstgelegene Material-Widget und wirft einen Fehler, wenn keines da ist. Scaffold, Card und Dialog stellen diesen Vorfahren bereits; außerhalb davon müssen Sie das InkWell in ein Material einpacken.
Das zweite Symptom ist tückischer: kein Fehler, aber auch kein Ripple. Ursache ist meist ein farbiger Container als Kind des InkWell; der Ripple wird auf das Material gezeichnet, und die deckende Farbe liegt darüber. Geben Sie die Farbe dem Material statt dem Kind:
Material(
color: Colors.orange,
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('Der Ripple ist sichtbar'),
),
),
)3. onPan und onScale gemeinsam verwenden
Symptom: im Debug-Modus der Fehler „Incorrect GestureDetector arguments“, dessen Beschreibung erklärt, dass Scale eine Obermenge von Pan ist. Brauchen Sie Verschieben und Zoomen, verwenden Sie nur die onScale*-Callbacks; wie weit sich der Finger bewegt hat, steht in details.focalPointDelta. Ebenso führt es zum selben Fehler, alle drei Familien – horizontal, vertikal und Pan – gleichzeitig zu definieren.
4. Einfaches Tippen wird träge, sobald onDoubleTap dazukommt
Sind onTap und onDoubleTap beide definiert, wartet Flutter nach der ersten Berührung einen kurzen Moment, ob eine zweite folgt. Dadurch reagiert das einfache Tippen spürbar verzögert. Wird das Doppeltippen nicht wirklich gebraucht, lassen Sie es weg; wird es gebraucht, legen Sie keine kritische Aktion auf das einfache Tippen.
Mini-Szenario: ein zoombares Foto
Stellen Sie sich eine Fotokarte vor: Doppeltippen vergibt ein Like, zwei Finger zoomen, langes Drücken stellt die Originalgröße wieder her.
class ZoomablePhoto extends StatefulWidget {
const ZoomablePhoto({super.key, required this.image});
final ImageProvider image;
@override
State<ZoomablePhoto> createState() => _ZoomablePhotoState();
}
class _ZoomablePhotoState extends State<ZoomablePhoto> {
double _scale = 1.0;
double _startScale = 1.0;
bool _liked = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTap: () => setState(() => _liked = !_liked),
onLongPress: () => setState(() => _scale = 1.0),
onScaleStart: (_) => _startScale = _scale,
onScaleUpdate: (details) {
setState(() {
// details.scale ist relativ zu dieser Geste, daher mit dem Startwert multiplizieren
_scale = (_startScale * details.scale).clamp(1.0, 4.0);
});
},
child: Stack(
alignment: Alignment.topRight,
children: [
ClipRect(
child: Transform.scale(
scale: _scale,
child: Image(image: widget.image, fit: BoxFit.cover),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Icon(
_liked ? Icons.favorite : Icons.favorite_border,
color: Colors.white,
),
),
],
),
);
}
}In onScaleStart merken wir uns die aktuelle Skalierung; ohne das würde das Foto bei jedem neuen Pinch auf 1.0 zurückspringen. clamp begrenzt die Skalierung, und ClipRect verhindert, dass das wachsende Bild über die Karte hinausragt. Weil hier kein onTap definiert ist, spielt die Doppeltipp-Verzögerung keine Rolle. Soll der Zoom nicht sprunghaft, sondern mit weichem Übergang zurückgesetzt werden, lässt sich der Ansatz aus dem Beitrag zu AnimatedContainer und impliziten Animationen direkt übertragen. Und wenn Sie ein vollwertiges Verschieben des Bildes brauchen, ist der Wechsel zu InteractiveViewer die richtige Entscheidung, statt eigene Mathematik zu schreiben.
Ereignisse im Überblick
| Ereignis | Beschreibung |
|---|---|
onTap |
Einfaches Tippen |
onDoubleTap |
Doppeltippen |
onLongPress |
Langes Drücken |
onHorizontalDrag* |
Horizontales Ziehen |
onVerticalDrag* |
Vertikales Ziehen |
onPan* |
Ziehen in beliebige Richtung |
onScale* |
Zoomen und Drehen per Pinch |
Häufig gestellte Fragen
Was ist der Unterschied zwischen GestureDetector und InkWell?
GestureDetector erkennt nur die Geste, gibt keine visuelle Rückmeldung und unterstützt auch Ereignisse wie Ziehen und Skalieren. InkWell bietet Tippen und langes Drücken zusammen mit dem Material-Ripple, braucht dafür aber einen Material-Vorfahren.
Warum reagiert GestureDetector nicht, wenn ich auf eine leere Fläche tippe?
Beim Standard HitTestBehavior.deferToChild erhalten nur die Punkte die Berührung, an denen das Kind etwas zeichnet. Wenn Sie den Parameter behavior auf HitTestBehavior.opaque setzen, wird die gesamte Fläche des Widgets antippbar.
Kann man onPan und onScale zusammen verwenden?
Nein, Flutter meldet das im Debug-Modus als Fehler, weil eine Scale-Geste das Verschieben bereits einschließt. Verwenden Sie nur die onScale-Callbacks und lesen Sie die Bewegung aus focalPointDelta.
Pinch-Zoom: GestureDetector oder InteractiveViewer?
Ist Zoomen und Verschieben eine Standardanforderung, erledigt InteractiveViewer das mit weniger Code und eingebauten Grenzen. Soll an die Skalierung weiteres Verhalten geknüpft oder die Geste vollständig angepasst werden, wählen Sie GestureDetector.
Verwandte Artikel
Flutter: Card-Widget und Einsatz im Design
Card in Flutter: Material-3-Varianten Card.filled und Card.outlined, clipBehavior bei Bildern, antippbare Karten mit InkWell und typische Fehler.
Flutter: Helles und dunkles Theme mit ThemeData
Ein Flutter-Theme mit ThemeData aufbauen: ColorScheme.fromSeed, helles und dunkles Theme, ThemeMode, Theme.of, TextTheme, Komponenten-Themes, ThemeExtension.
Flutter: TextField und TextEditingController verwenden
TextField in Flutter: TextEditingController und dispose, InputDecoration, Tastaturtypen, inputFormatters, Passwortfelder, FocusNode und onChanged im Vergleich.