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.
Drag Gestures
Handle drag movements in any direction. Track drag start, update, and end events for creating draggable widgets and custom interactions.
Scale Gestures
Detect pinch-to-zoom and rotation gestures. Useful for image viewers, maps, and any content that needs zoom functionality.
Swipe Gestures
Recognize swipe directions for navigation and dismissible items. Implement swipe-to-delete, page transitions, and gesture-based navigation.
🔹 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)