Flutter Gesture Detector

Handle user touch interactions in Flutter apps

👆 What is GestureDetector?

GestureDetector is a Flutter widget that detects user gestures like taps, swipes, drags, and long presses. It makes any widget interactive by wrapping it and responding to touch events.


// Simple tap detection
GestureDetector(
  onTap: () => print('Tapped!'),
  child: Container(color: Colors.blue),
)
                                    

Gesture Types

👆

Tap Gestures

Detect single taps, double taps, and long presses on widgets. Perfect for buttons, cards, and interactive elements in your app.

onTap onDoubleTap
👈

Drag Gestures

Handle drag movements in any direction. Track drag start, update, and end events for creating draggable widgets and custom interactions.

onPan onDrag
🔄

Scale Gestures

Detect pinch-to-zoom and rotation gestures. Useful for image viewers, maps, and any content that needs zoom functionality.

onScale Pinch
⬅️

Swipe Gestures

Recognize swipe directions for navigation and dismissible items. Implement swipe-to-delete, page transitions, and gesture-based navigation.

Horizontal Vertical

🔹 Basic Tap Gestures

🔸 Simple Tap Detection

// Detect single tap
GestureDetector(
  onTap: () {
    print('Widget tapped!');
  },
  child: Container(
    padding: EdgeInsets.all(20),
    color: Colors.blue,
    child: Text('Tap Me'),
  ),
)

🔸 Double Tap

// Detect double tap
GestureDetector(
  onDoubleTap: () {
    print('Double tapped!');
  },
  child: Container(
    padding: EdgeInsets.all(20),
    color: Colors.green,
    child: Text('Double Tap Me'),
  ),
)

🔸 Long Press

// Detect long press
GestureDetector(
  onLongPress: () {
    print('Long pressed!');
  },
  child: Container(
    padding: EdgeInsets.all(20),
    color: Colors.orange,
    child: Text('Long Press Me'),
  ),
)

🔸 Multiple Gestures

// Handle multiple gesture types
GestureDetector(
  onTap: () => print('Tapped'),
  onDoubleTap: () => print('Double Tapped'),
  onLongPress: () => print('Long Pressed'),
  child: Container(
    padding: EdgeInsets.all(20),
    color: Colors.purple,
    child: Text('Try Different Gestures'),
  ),
)

🔹 Drag Gestures

🔸 Horizontal Drag

// Detect horizontal drag
class DraggableBox extends StatefulWidget {
  @override
  _DraggableBoxState createState() => _DraggableBoxState();
}

class _DraggableBoxState extends State {
  double xPosition = 0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onHorizontalDragUpdate: (details) {
        setState(() {
          xPosition += details.delta.dx;
        });
      },
      child: Transform.translate(
        offset: Offset(xPosition, 0),
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
          child: Center(child: Text('Drag Me')),
        ),
      ),
    );
  }
}

🔸 Vertical Drag

// Detect vertical drag
double yPosition = 0;

GestureDetector(
  onVerticalDragUpdate: (details) {
    setState(() {
      yPosition += details.delta.dy;
    });
  },
  child: Transform.translate(
    offset: Offset(0, yPosition),
    child: Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
  ),
)

🔸 Pan Gesture (Any Direction)

// Drag in any direction
class FreeDraggable extends StatefulWidget {
  @override
  _FreeDraggableState createState() => _FreeDraggableState();
}

class _FreeDraggableState extends State {
  Offset position = Offset(0, 0);
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        setState(() {
          position += details.delta;
        });
      },
      child: Transform.translate(
        offset: position,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.green,
          child: Center(child: Text('Drag Anywhere')),
        ),
      ),
    );
  }
}

🔸 Drag Start and End

// Track drag lifecycle
GestureDetector(
  onPanStart: (details) {
    print('Drag started at: ${details.globalPosition}');
  },
  onPanUpdate: (details) {
    print('Dragging: ${details.delta}');
  },
  onPanEnd: (details) {
    print('Drag ended with velocity: ${details.velocity}');
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.orange,
  ),
)

🔹 Scale and Rotation Gestures

🔸 Pinch to Zoom

// Implement pinch-to-zoom
class ZoomableWidget extends StatefulWidget {
  @override
  _ZoomableWidgetState createState() => _ZoomableWidgetState();
}

class _ZoomableWidgetState extends State {
  double scale = 1.0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleUpdate: (details) {
        setState(() {
          scale = details.scale;
        });
      },
      child: Transform.scale(
        scale: scale,
        child: Container(
          width: 200,
          height: 200,
          color: Colors.blue,
          child: Center(child: Text('Pinch to Zoom')),
        ),
      ),
    );
  }
}

🔸 Rotation Gesture

// Rotate widget with gesture
class RotatableWidget extends StatefulWidget {
  @override
  _RotatableWidgetState createState() => _RotatableWidgetState();
}

class _RotatableWidgetState extends State {
  double rotation = 0.0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleUpdate: (details) {
        setState(() {
          rotation = details.rotation;
        });
      },
      child: Transform.rotate(
        angle: rotation,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.purple,
          child: Center(child: Text('Rotate Me')),
        ),
      ),
    );
  }
}

🔸 Combined Scale and Rotation

// Scale and rotate together
class TransformableWidget extends StatefulWidget {
  @override
  _TransformableWidgetState createState() => _TransformableWidgetState();
}

class _TransformableWidgetState extends State {
  double scale = 1.0;
  double rotation = 0.0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleUpdate: (details) {
        setState(() {
          scale = details.scale;
          rotation = details.rotation;
        });
      },
      child: Transform.scale(
        scale: scale,
        child: Transform.rotate(
          angle: rotation,
          child: Container(
            width: 150,
            height: 150,
            color: Colors.teal,
            child: Center(child: Text('Transform Me')),
          ),
        ),
      ),
    );
  }
}

🔹 Practical Examples

🔸 Like Button with Animation

// Interactive like button
class LikeButton extends StatefulWidget {
  @override
  _LikeButtonState createState() => _LikeButtonState();
}

class _LikeButtonState extends State {
  bool isLiked = false;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          isLiked = !isLiked;
        });
      },
      child: Icon(
        isLiked ? Icons.favorite : Icons.favorite_border,
        color: isLiked ? Colors.red : Colors.grey,
        size: 40,
      ),
    );
  }
}

🔸 Swipe to Delete

// Swipe to dismiss item
Dismissible(
  key: Key(item.id),
  direction: DismissDirection.endToStart,
  onDismissed: (direction) {
    setState(() {
      items.removeAt(index);
    });
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Item deleted')),
    );
  },
  background: Container(
    color: Colors.red,
    alignment: Alignment.centerRight,
    padding: EdgeInsets.only(right: 20),
    child: Icon(Icons.delete, color: Colors.white),
  ),
  child: ListTile(title: Text(item.name)),
)

🔸 Custom Button with Feedback

// Button with visual feedback
class CustomButton extends StatefulWidget {
  final String text;
  final VoidCallback onPressed;
  
  CustomButton({required this.text, required this.onPressed});
  
  @override
  _CustomButtonState createState() => _CustomButtonState();
}

class _CustomButtonState extends State {
  bool isPressed = false;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => isPressed = true),
      onTapUp: (_) => setState(() => isPressed = false),
      onTapCancel: () => setState(() => isPressed = false),
      onTap: widget.onPressed,
      child: AnimatedContainer(
        duration: Duration(milliseconds: 100),
        padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
        decoration: BoxDecoration(
          color: isPressed ? Colors.blue[700] : Colors.blue,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          widget.text,
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

🔸 Image Viewer with Zoom

// Zoomable image viewer
class ImageViewer extends StatefulWidget {
  final String imageUrl;
  
  ImageViewer({required this.imageUrl});
  
  @override
  _ImageViewerState createState() => _ImageViewerState();
}

class _ImageViewerState extends State {
  double scale = 1.0;
  double previousScale = 1.0;
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleStart: (details) {
        previousScale = scale;
      },
      onScaleUpdate: (details) {
        setState(() {
          scale = previousScale * details.scale;
          scale = scale.clamp(1.0, 4.0); // Limit zoom
        });
      },
      onDoubleTap: () {
        setState(() {
          scale = scale > 1.0 ? 1.0 : 2.0; // Toggle zoom
        });
      },
      child: Transform.scale(
        scale: scale,
        child: Image.network(widget.imageUrl),
      ),
    );
  }
}

🔹 InkWell vs GestureDetector

🔸 InkWell (Material Ripple Effect)

Use InkWell when you want Material Design ripple effects on tap.

// InkWell with ripple effect
InkWell(
  onTap: () => print('Tapped'),
  child: Container(
    padding: EdgeInsets.all(20),
    child: Text('Tap for Ripple'),
  ),
)

🔸 GestureDetector (No Visual Feedback)

Use GestureDetector for custom gestures or when you don't want ripple effects.

// GestureDetector without ripple
GestureDetector(
  onTap: () => print('Tapped'),
  child: Container(
    padding: EdgeInsets.all(20),
    child: Text('Tap without Ripple'),
  ),
)

🔹 Common Gesture Patterns

Gesture Callbacks:

  • onTap: Single tap
  • onDoubleTap: Double tap
  • onLongPress: Press and hold
  • onPanStart: Drag begins
  • onPanUpdate: Drag in progress
  • onPanEnd: Drag ends
  • onScaleStart: Pinch/zoom begins
  • onScaleUpdate: Pinch/zoom in progress
  • onScaleEnd: Pinch/zoom ends

🔹 Best Practices

  • Use InkWell for Material Design buttons with ripple effects
  • Use GestureDetector for custom gestures and complex interactions
  • Provide visual feedback for all interactive elements
  • Don't nest multiple GestureDetectors unnecessarily
  • Consider accessibility - ensure gestures work with screen readers
  • Test gestures on real devices, not just emulators
  • Use appropriate gesture types for the interaction (tap vs long press)

🧠 Test Your Knowledge

Which widget provides Material ripple effects?