Irbisa · Cheatsheet

Gestures & Pointer Input

Gesture arena, hit testing, drag and scale, custom recognizers, keyboard, hover, drag-and-drop

Middle Developer · 20 questions

  1. 01

    What is the difference between GestureDetector, InkWell and Listener, and when do you use each?

    Listener reports raw pointer events and never competes for them; GestureDetector turns pointer events into gestures (tap, drag, scale) through recognizers that compete in the gesture arena; InkWell handles taps and also paints the Material ripple, hover and focus highlight.

  2. 02

    What is the difference between IgnorePointer and AbsorbPointer?

    Both stop a subtree from reacting to touches.

    // A decorative layer on top: taps fall through to the list below.Stack(  children: [    const ProductList(),    IgnorePointer(child: ConfettiLayer(controller: _confetti)),  ],…
  3. 03

    How do you implement swipe-to-delete with Dismissible, and why must its key come from the item rather than the index?

    Wrap each row in Dismissible, remove the item from your data in onDismissed, and key the row by the item's identity, such as ValueKey(item.id).

    ListView.builder(  itemCount: tasks.length,  itemBuilder: (context, i) {    final task = tasks[i];    return Dismissible(      key: ValueKey(task.id), // identity, not position…
  4. 04

    How does Flutter's gesture arena decide which gesture wins, and why does onTap fire only when the finger lifts?

    Every recognizer under the finger joins an arena for that pointer; each can accept or reject, and the last one left wins.

  5. 05

    What do HitTestBehavior.deferToChild, opaque and translucent do, and why do taps on empty space inside a GestureDetector get lost?

    HitTestBehavior decides whether a detector counts as hit where its child paints nothing, and whether widgets behind it are hit-tested too.

    // Dead zone: the gap between the icon and the text paints nothing.GestureDetector(  onTap: _open, // behavior: deferToChild by default  child: const Row(    mainAxisAlignment: MainAxisAlignment.spaceBetween,    children: [Icon(Icons.folder), Text('Reports')],…
  6. 06

    In this code, what is printed when the user taps the star once, and what when they tap the padding around it?

    GestureDetector(  onTap: () => print('card'),  onDoubleTap: () => print('card: double'),  child: Padding(    padding: const EdgeInsets.all(24),    child: GestureDetector(      onTap: () => print('star'),      child: const Icon(Icons.star),    ),  ),)

    Tapping the star prints star, but about 300 ms after the finger lifts; tapping the padding prints nothing.

  7. 07

    An overlay with HitTestBehavior.translucent sits over a card so that both react to a tap. What does this print when the card is tapped, and why?

    Stack(  children: [    GestureDetector(      onTap: () => print('card'),      child: const ColoredBox(color: Colors.white, child: SizedBox.expand()),    ),    Positioned.fill(      child: GestureDetector(        behavior: HitTestBehavior.translucent,        onTap: () => print('overlay'),      ),    ),  ],)

    Only overlay.

  8. 08

    Find the bug: leak_tracker reports an undisposed TapGestureRecognizer for this terms-of-service link, and the count grows on every rebuild.

    class TermsNotice extends StatelessWidget {  const TermsNotice({super.key, required this.onOpenTerms});  final VoidCallback onOpenTerms;   @override  Widget build(BuildContext context) => Text.rich(        TextSpan(text: 'By continuing you accept the ', children: [          TextSpan(            text: 'Terms of Service',            style: const TextStyle(decoration: TextDecoration.underline),            recognizer: TapGestureRecognizer()..onTap = onOpenTerms,          ),        ]),      );}

    Every build creates a new TapGestureRecognizer and nothing ever disposes it, because a TextSpan does not own its recognizer.

  9. 09

    You add onDoubleTap to a list row that already has onTap, and the whole screen starts to feel laggy: what did that one line cost?

    Every single tap on that row now fires about 300 ms late: the arena can no longer resolve at pointer-up, because the double-tap recognizer holds it for kDoubleTapTimeout in case a second tap arrives.

  10. 10

    Swipe-to-archive rows sit inside a vertical ListView, and a diagonal swipe sometimes scrolls the list and sometimes moves the row: what decides it?

    Both drags are in the same arena, and each accepts as soon as the finger travels kTouchSlop (18 logical px by default) along its own axis, so on a diagonal the axis crossed first wins: a steep swipe scrolls the list, a flat one moves the row.

    // Pan needs 36 px in any direction before it claims, so the row starts late and you also get dy.GestureDetector(  onPanUpdate: (d) => setState(() => _dx += d.delta.dx),  child: const MessageRow(),);…
  11. 11

    This pinch-to-zoom code throws in debug, and once that is fixed the zoom runs away after a few frames. What is wrong with it?

    GestureDetector(  onPanUpdate: (d) => setState(() => _offset += d.delta),  onScaleUpdate: (d) => setState(() => _scale *= d.scale),  child: Transform.translate(    offset: _offset,    child: Transform.scale(scale: _scale, child: const MapSurface()),  ),)

    Two bugs. GestureDetector asserts when pan and scale callbacks are both set, because scale is a superset of pan; and ScaleUpdateDetails.scale is measured from the start of the gesture, not from the previous frame, so multiplying it in on every update compounds it.

  12. 12

    How do you build drag-and-drop with Draggable and DragTarget, and why does the dragged copy often render without styles?

    A Draggable<T> carries a typed data payload and shows feedback under the finger; a DragTarget<T> accepts it in onAcceptWithDetails.

    LongPressDraggable<Task>( // waits, so the ListView keeps its scroll  data: task,  feedback: Material( // the Overlay has no Material above it    elevation: 8,    borderRadius: BorderRadius.circular(12),    child: SizedBox(width: 260, child: TaskCard(task: task)),…
  13. 13

    Find the bug: the sticker jumps down and sideways the moment the user starts dragging it, then follows the finger at an offset.

    class _StickerBoardState extends State<StickerBoard> {  Offset _pos = const Offset(40, 40);  @override  Widget build(BuildContext context) => Stack(        children: [          Positioned(            left: _pos.dx,            top: _pos.dy,            child: GestureDetector(              onPanUpdate: (d) => setState(() => _pos = d.globalPosition),              child: const StickerCard(),            ),          ),        ],      );}

    d.globalPosition is where the finger is on the screen, but Positioned wants the card's top-left corner in the Stack's own coordinates, so the card jumps by the Stack's offset (app bar, padding) plus the point where it was grabbed.

  14. 14

    Your swipe-to-delete commits on a flick but not when the user drags far, pauses and then lifts: what is DragEndDetails.velocity reporting?

    Velocity.zero.

    // Inside the State of a swipe row; _c is an AnimationController (0..1 = row travel).static const double _width = 320;static const _spring = SpringDescription(mass: 1, stiffness: 500, damping: 30); void _onUpdate(DragUpdateDetails d) => _c.value -= d.primaryDelta! / _width;…
  15. 15

    How do you add double-tap-to-zoom to an InteractiveViewer, and why does the image jump when the user pinches during the zoom animation?

    InteractiveViewer has no double-tap zoom, so you animate its TransformationController yourself; the jump comes from two writers to one Matrix4, your animation and the user's pinch.

    class _ZoomableState extends State<Zoomable> with SingleTickerProviderStateMixin {  final _transform = TransformationController();  late final _anim = AnimationController(    vsync: this,    duration: const Duration(milliseconds: 250),  )..addListener(() => _transform.value = _zoom!.value);…
  16. 16

    How do you add keyboard shortcuts with Shortcuts, Actions and Focus, and why does Ctrl+S sometimes do nothing at all?

    Shortcuts maps a key combination to an Intent, Actions maps that intent type to the code to run, and the lookup starts from the focused widget, so with no focus inside the subtree the shortcut never fires.

    class SaveIntent extends Intent {  const SaveIntent();} Widget buildEditor() {  return Shortcuts(…
  17. 17

    A button in the corner of a card is dead: no ripple, no callback, nothing in the logs. How do you find out where that tap actually goes?

    Follow the pointer step by step and make the framework print each one: did the hit test reach the button, did something above it swallow the event, or did its recognizer lose the arena.

    import 'package:flutter/gestures.dart';import 'package:flutter/rendering.dart'; void main() {  debugPaintPointersEnabled = true; // highlights every box the hit test reaches  debugPrintHitTestResults = true; // prints the hit path, deepest first…
  18. 18

    What does RawGestureDetector give you over GestureDetector, and how do you write a custom recognizer such as a two-finger-only drag?

    RawGestureDetector takes recognizer factories, so you can configure stock recognizers beyond GestureDetector (supportedDevices, allowedButtonsFilter, a custom long-press duration) or plug in your own; GestureDetector is a thin wrapper around it.

    class TwoFingerDragRecognizer extends OneSequenceGestureRecognizer {  ValueChanged<Offset>? onDrag;  final Set<int> _pointers = {};   @override  void addAllowedPointer(PointerDownEvent event) {…
  19. 19

    How do mouse wheel, trackpad and stylus input reach a Flutter canvas, and what does a canvas built only on touch gestures miss on desktop?

    Not all of it goes through the gesture arena: a wheel tick is a PointerScrollEvent on the pointer-signal channel, a trackpad pinch is PointerPanZoom*, a pen is a stylus pointer with pressure; a touch-only canvas misses wheel zoom, trackpad pinch, hover and pen pressure.

    Listener(  onPointerSignal: (event) {    if (event is! PointerScrollEvent) return;    // Claim the tick, or a scroll view behind the canvas scrolls on it too.    GestureBinding.instance.pointerSignalResolver.register(event, (e) {      final dy = (e as PointerScrollEvent).scrollDelta.dy;…
  20. 20

    With VoiceOver or TalkBack on, your drag-to-set brightness control cannot be moved at all. What happens to pointer input when a screen reader is running?

    The screen reader takes over the touch screen: your recognizers no longer see the user's swipes, and the platform sends SemanticsActions to the focused semantics node instead.

    // Pointer-only: with a screen reader on, the user's swipes never arrive here.Listener(  onPointerMove: (e) => _setBrightness(e.localPosition.dx / _width),  child: const BrightnessBar(),);…