iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Gestures

Flutter has two gesture layers: high-level GestureDetector (taps, drags, scales) and low-level RawGestureDetector + custom recognisers. Use GestureDetector for 90% of needs, swap to InkWell for Material ripple, and reach for the lower-level APIs only when you need to disambiguate (drag vs scroll, pan vs scale).

Tap, double-tap, drag, swipe-to-dismiss, pinch

EXAMPLE
import 'package:flutter/material.dart';

// 1) Tap, long-press, double-tap
class TapDemo extends StatelessWidget {
  const TapDemo({super.key});
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap:        () => debugPrint('tap'),
      onDoubleTap:  () => debugPrint('double tap'),
      onLongPress:  () => debugPrint('long press'),
      child: Container(width: 120, height: 80, color: Colors.indigo,
                       alignment: Alignment.center,
                       child: const Text('Tap me', style: TextStyle(color: Colors.white))),
    );
  }
}

// 2) Drag — track translation while pointer is down
class DragBox extends StatefulWidget {
  const DragBox({super.key});
  @override
  State<DragBox> createState() => _DragBoxState();
}
class _DragBoxState extends State<DragBox> {
  Offset pos = Offset.zero;

  @override
  Widget build(BuildContext c) {
    return Stack(children: [
      Positioned(
        left: pos.dx, top: pos.dy,
        child: GestureDetector(
          onPanUpdate: (d) => setState(() => pos += d.delta),
          child: Container(width: 80, height: 80, color: Colors.orange),
        ),
      ),
    ]);
  }
}

// 3) Swipe-to-dismiss row (Dismissible)
class SwipeList extends StatefulWidget {
  const SwipeList({super.key});
  @override
  State<SwipeList> createState() => _SwipeListState();
}
class _SwipeListState extends State<SwipeList> {
  final items = List.generate(20, (i) => 'Item $i');

  @override
  Widget build(BuildContext c) {
    return ListView(children: [
      for (final id in List.of(items))
        Dismissible(
          key: ValueKey(id),
          direction: DismissDirection.endToStart,
          background: Container(color: Colors.red, alignment: Alignment.centerRight,
              padding: const EdgeInsets.only(right: 16), child: const Icon(Icons.delete, color: Colors.white)),
          confirmDismiss: (_) async {
            return await showDialog<bool>(context: c, builder: (_) => AlertDialog(
              title: const Text('Delete?'),
              actions: [
                TextButton(onPressed: () => Navigator.pop(c, false), child: const Text('No')),
                TextButton(onPressed: () => Navigator.pop(c, true),  child: const Text('Yes')),
              ],
            )) ?? false;
          },
          onDismissed: (_) => setState(() => items.remove(id)),
          child: ListTile(title: Text(id)),
        ),
    ]);
  }
}

// 4) Pinch + rotate via scale gesture
class PinchImage extends StatefulWidget {
  const PinchImage({super.key, required this.url});
  final String url;
  @override
  State<PinchImage> createState() => _PinchImageState();
}
class _PinchImageState extends State<PinchImage> {
  double scale = 1, baseScale = 1;
  double rotation = 0, baseRotation = 0;

  @override
  Widget build(BuildContext c) {
    return GestureDetector(
      onScaleStart: (d) {
        baseScale = scale; baseRotation = rotation;
      },
      onScaleUpdate: (d) {
        setState(() {
          scale = (baseScale * d.scale).clamp(0.5, 5.0);
          rotation = baseRotation + d.rotation;
        });
      },
      child: Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()..scale(scale)..rotateZ(rotation),
        child: Image.network(widget.url),
      ),
    );
  }
}

// 5) Ink ripple (Material feedback) — use InkWell, not GestureDetector
class RippleCard extends StatelessWidget {
  const RippleCard({super.key, required this.onTap});
  final VoidCallback onTap;
  @override
  Widget build(BuildContext c) {
    return Card(
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(8),
        child: const Padding(padding: EdgeInsets.all(16), child: Text('Tap me with ripple')),
      ),
    );
  }
}

// 6) Disambiguation — scroll vs swipe
// In a vertical ListView with horizontal-swipe items, set direction explicitly
// and use Dismissible's direction to limit the gesture; this lets ListView
// own vertical, Dismissible own horizontal.

Why it matters

Default to InkWell over GestureDetector inside Material apps — InkWell handles the visual feedback (ripple, hover) plus the same tap callbacks, which keeps the UX consistent with the rest of the platform. Reach for GestureDetector only when you need a gesture InkWell does not expose (long press in some configurations, multi-finger).

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
GestureDetector(
    onTap: () => print('tap'),
    onLongPress: () => print('long press'),
    onPanUpdate: (d) => print(d.delta),
    child: const Icon(Icons.touch_app, size: 64),
)
Try it Yourself »

Discussion

Loading…