FlutterGestureDetector
FlutterGestureDetector wraps Flutter’s native GestureDetector widget, enabling rich gesture interactions in your WebF applications. It provides native-level gesture recognition for tap, double tap, long press, pan (drag), pinch-to-scale, and rotation gestures.
Why Use FlutterGestureDetector?
While you can use standard web event handlers like onClick and onTouchMove, FlutterGestureDetector provides:
- Native gesture recognition using Flutter’s gesture system
- Multi-touch support for pinch and rotate gestures
- Better performance with native gesture processing
- Consistent behavior across platforms (iOS and Android)
- Advanced gestures like double tap and long press with proper timing
Basic Usage
import { FlutterGestureDetector } from '@openwebf/react-core-ui';
function GestureExample() {
const handleTap = (event: CustomEvent) => {
console.log('Tapped!', event.detail);
};
const handleDoubleTap = (event: CustomEvent) => {
console.log('Double tapped!', event.detail);
};
return (
<FlutterGestureDetector
onTap={handleTap}
onDoubletap={handleDoubleTap}
>
<div className="gesture-area">
Tap or double tap me!
</div>
</FlutterGestureDetector>
);
}Tap Gestures
Handle single tap, double tap, and long press events:
function TapGestures() {
const [tapCount, setTapCount] = useState(0);
const [doubleTapCount, setDoubleTapCount] = useState(0);
const [longPressCount, setLongPressCount] = useState(0);
const [isLongPressing, setIsLongPressing] = useState(false);
return (
<FlutterGestureDetector
onTap={(e) => {
setTapCount(prev => prev + 1);
console.log('Tap at:', e.detail);
}}
onDoubletap={(e) => {
setDoubleTapCount(prev => prev + 1);
console.log('Double tap at:', e.detail);
}}
onLongpress={(e) => {
setLongPressCount(prev => prev + 1);
setIsLongPressing(true);
console.log('Long press started:', e.detail);
}}
onLongpressend={(e) => {
setIsLongPressing(false);
console.log('Long press ended:', e.detail);
}}
>
<div className="tap-area">
<p>Taps: {tapCount}</p>
<p>Double Taps: {doubleTapCount}</p>
<p>Long Presses: {longPressCount}</p>
<p>Status: {isLongPressing ? 'Long Pressing...' : 'Ready'}</p>
</div>
</FlutterGestureDetector>
);
}Pan (Drag) Gestures
Track dragging movements with start, update, and end events:
function PanGestures() {
const [panState, setPanState] = useState({
isPanning: false,
position: { x: 0, y: 0 },
delta: { x: 0, y: 0 },
velocity: { x: 0, y: 0 }
});
return (
<FlutterGestureDetector
onPanstart={(e) => {
const { x, y } = e.detail;
setPanState(prev => ({
...prev,
isPanning: true,
position: { x, y },
delta: { x: 0, y: 0 }
}));
}}
onPanupdate={(e) => {
const { x, y, deltaX, deltaY } = e.detail;
setPanState(prev => ({
...prev,
position: { x, y },
delta: { x: deltaX, y: deltaY }
}));
}}
onPanend={(e) => {
const { velocityX, velocityY } = e.detail;
setPanState(prev => ({
...prev,
isPanning: false,
velocity: { x: velocityX, y: velocityY }
}));
}}
>
<div className="pan-area">
<p>Status: {panState.isPanning ? 'Panning' : 'Idle'}</p>
<p>Position: ({panState.position.x.toFixed(0)}, {panState.position.y.toFixed(0)})</p>
<p>Delta: ({panState.delta.x.toFixed(0)}, {panState.delta.y.toFixed(0)})</p>
<p>Velocity: ({panState.velocity.x.toFixed(2)}, {panState.velocity.y.toFixed(2)})</p>
</div>
</FlutterGestureDetector>
);
}Pan event details:
x,y- Current position coordinatesdeltaX,deltaY- Movement delta from previous updatevelocityX,velocityY- Gesture velocity on end (for inertia effects)
Scale & Rotate Gestures
Handle pinch-to-zoom and rotation with multi-touch:
function ScaleRotateGestures() {
const [scaleState, setScaleState] = useState({
isScaling: false,
scale: 1.0,
rotation: 0,
focalPoint: { x: 0, y: 0 }
});
return (
<FlutterGestureDetector
onScalestart={(e) => {
setScaleState(prev => ({
...prev,
isScaling: true
}));
}}
onScaleupdate={(e) => {
const { scale, rotation, focalPointX, focalPointY } = e.detail;
setScaleState(prev => ({
...prev,
scale,
rotation,
focalPoint: { x: focalPointX, y: focalPointY }
}));
}}
onScaleend={(e) => {
setScaleState(prev => ({
...prev,
isScaling: false
}));
}}
>
<div
className="scale-area"
style={{
transform: `scale(${scaleState.scale}) rotate(${scaleState.rotation}rad)`,
transformOrigin: `${scaleState.focalPoint.x}px ${scaleState.focalPoint.y}px`
}}
>
<p>Status: {scaleState.isScaling ? 'Scaling' : 'Idle'}</p>
<p>Scale: {scaleState.scale.toFixed(2)}x</p>
<p>Rotation: {(scaleState.rotation * 180 / Math.PI).toFixed(0)}°</p>
<p>Focal Point: ({scaleState.focalPoint.x.toFixed(0)}, {scaleState.focalPoint.y.toFixed(0)})</p>
</div>
</FlutterGestureDetector>
);
}Scale event details:
scale- Scale factor (1.0 = original size)rotation- Rotation in radiansfocalPointX,focalPointY- Center point of the pinch gesture
Complete Gesture Handler
Combine multiple gestures in a single component:
function CompleteGestureExample() {
const [gestureInfo, setGestureInfo] = useState({
currentGesture: 'none',
details: ''
});
return (
<FlutterGestureDetector
onTap={(e) => {
setGestureInfo({
currentGesture: 'tap',
details: `Tapped at (${e.detail.x}, ${e.detail.y})`
});
}}
onDoubletap={(e) => {
setGestureInfo({
currentGesture: 'double-tap',
details: 'Double tapped!'
});
}}
onLongpress={(e) => {
setGestureInfo({
currentGesture: 'long-press',
details: 'Long press started'
});
}}
onLongpressend={(e) => {
setGestureInfo({
currentGesture: 'long-press-end',
details: 'Long press ended'
});
}}
onPanstart={(e) => {
setGestureInfo({
currentGesture: 'pan',
details: `Pan started at (${e.detail.x}, ${e.detail.y})`
});
}}
onPanupdate={(e) => {
setGestureInfo({
currentGesture: 'pan',
details: `Delta: (${e.detail.deltaX.toFixed(0)}, ${e.detail.deltaY.toFixed(0)})`
});
}}
onPanend={(e) => {
setGestureInfo({
currentGesture: 'pan-end',
details: `Velocity: (${e.detail.velocityX.toFixed(2)}, ${e.detail.velocityY.toFixed(2)})`
});
}}
onScalestart={(e) => {
setGestureInfo({
currentGesture: 'scale',
details: 'Scale started'
});
}}
onScaleupdate={(e) => {
setGestureInfo({
currentGesture: 'scale',
details: `Scale: ${e.detail.scale.toFixed(2)}x, Rotation: ${(e.detail.rotation * 180 / Math.PI).toFixed(0)}°`
});
}}
onScaleend={(e) => {
setGestureInfo({
currentGesture: 'scale-end',
details: 'Scale ended'
});
}}
>
<div className="interactive-area">
<h3>Try all gestures:</h3>
<ul>
<li>Tap</li>
<li>Double Tap</li>
<li>Long Press</li>
<li>Pan (Drag)</li>
<li>Pinch (two fingers)</li>
<li>Rotate (two fingers)</li>
</ul>
<div className="gesture-status">
<strong>Current:</strong> {gestureInfo.currentGesture}<br />
<strong>Details:</strong> {gestureInfo.details}
</div>
</div>
</FlutterGestureDetector>
);
}Props
| Prop | Type | Description |
|---|---|---|
onTap | (event: CustomEvent) => void | Single tap event |
onDoubletap | (event: CustomEvent) => void | Double tap event |
onLongpress | (event: CustomEvent) => void | Long press started |
onLongpressend | (event: CustomEvent) => void | Long press ended |
onPanstart | (event: CustomEvent) => void | Pan gesture started |
onPanupdate | (event: CustomEvent) => void | Pan gesture updated |
onPanend | (event: CustomEvent) => void | Pan gesture ended |
onScalestart | (event: CustomEvent) => void | Scale/rotate gesture started |
onScaleupdate | (event: CustomEvent) => void | Scale/rotate gesture updated |
onScaleend | (event: CustomEvent) => void | Scale/rotate gesture ended |
className | string | CSS class name |
style | CSSProperties | Inline styles |
children | ReactNode | Content to wrap with gesture detection |
Event Details
Tap Events (onTap, onDoubletap)
{
x: number; // X coordinate
y: number; // Y coordinate
}Long Press Events (onLongpress, onLongpressend)
{
x: number; // X coordinate
y: number; // Y coordinate
}Pan Events
onPanstart:
{
x: number; // Starting X coordinate
y: number; // Starting Y coordinate
}onPanupdate:
{
x: number; // Current X coordinate
y: number; // Current Y coordinate
deltaX: number; // Change in X since last update
deltaY: number; // Change in Y since last update
}onPanend:
{
velocityX: number; // Horizontal velocity
velocityY: number; // Vertical velocity
}Scale Events
onScaleupdate:
{
scale: number; // Scale factor (1.0 = original)
rotation: number; // Rotation in radians
focalPointX: number; // Center point X
focalPointY: number; // Center point Y
}Best Practices
- Handle all phases - For pan and scale gestures, handle start, update, and end events
- Use appropriate gestures - Choose gestures that match user expectations
- Provide visual feedback - Show users when gestures are active
- Test on devices - Multi-touch gestures work best on actual devices
- Avoid gesture conflicts - Don’t use conflicting gestures on nested elements
Styling Example
import { FlutterGestureDetector } from '@openwebf/react-core-ui';
function StyledGestureArea() {
const [isActive, setIsActive] = useState(false);
return (
<FlutterGestureDetector
className={`gesture-detector ${isActive ? 'active' : ''}`}
onPanstart={() => setIsActive(true)}
onPanend={() => setIsActive(false)}
onScalestart={() => setIsActive(true)}
onScaleend={() => setIsActive(false)}
>
<div className="gesture-content">
Drag or pinch to interact
</div>
</FlutterGestureDetector>
);
}.gesture-detector {
padding: 32px;
border: 2px dashed #d1d5db;
border-radius: 12px;
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
transition: all 0.2s;
cursor: pointer;
}
.gesture-detector.active {
border-color: #3b82f6;
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.gesture-content {
text-align: center;
font-size: 16px;
color: #6b7280;
user-select: none;
}
.gesture-detector.active .gesture-content {
color: #1e40af;
font-weight: 600;
}Performance Tips
- Use
React.memo()for components inside gesture detectors - Throttle rapid updates in
onPanupdateandonScaleupdateif needed - Avoid heavy computations in gesture event handlers
- Use CSS transforms for visual feedback (hardware accelerated)
Gesture Conflicts
Be aware of gesture priorities:
- Tap vs Double Tap - Double tap has a delay waiting to see if it’s a double tap
- Pan vs Scale - Scale requires two fingers, pan works with one
- Long Press vs Pan - Long press must be stationary; movement triggers pan
To handle conflicts, use only the gestures you need:
// Good: Use only what you need
<FlutterGestureDetector onTap={handleTap}>
{/* ... */}
</FlutterGestureDetector>
// Avoid: Using conflicting gestures unnecessarily
<FlutterGestureDetector
onTap={handleTap}
onDoubletap={handleDoubleTap} // May cause tap delays
onLongpress={handleLongPress} // May conflict with pan
onPanstart={handlePan}
>
{/* ... */}
</FlutterGestureDetector>Next Steps
- Hybrid UI - Combine with Flutter widgets
- WebFListView - Scrollable lists with gestures
- Event Handling - Understanding WebF events