Trigger-Based Modal Animations
GestureTrigger
supports smooth trigger-based animations that create seamless transitions from trigger elements (like thumbnails) to the full modal view. This feature enhances user experience by maintaining visual continuity between the trigger and modal content.
GestureTrigger Usage
The GestureTrigger
component wraps pressable elements and registers their position for smooth modal transitions.
import { GestureTrigger, GestureViewer } from 'react-native-gesture-image-viewer';
function Gallery() {
const [visible, setVisible] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const openModal = (index: number) => {
setSelectedIndex(index);
setVisible(true);
};
return (
<View>
{/* Gallery Grid */}
{images.map((uri, index) => (
<GestureTrigger key={uri} id="gallery" onPress={() => openModal(index)}>
<Pressable>
<Image source={{ uri }} />
</Pressable>
</GestureTrigger>
))}
{/* Modal */}
<Modal visible={visible} transparent>
<GestureViewer
id="gallery"
data={images}
initialIndex={selectedIndex}
renderItem={renderImage}
onDismiss={() => setVisible(false)}
triggerAnimation={{
duration: 300,
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0),
onAnimationComplete: () => console.log('Animation finished!')
}}
/>
</Modal>
</View>
);
}
1. Using onPress on GestureTrigger (Recommended)
<GestureTrigger onPress={() => openModal(index)}>
<Pressable>
<Image source={{ uri }} />
</Pressable>
</GestureTrigger>
2. Using onPress on Child Element
<GestureTrigger>
<Pressable onPress={() => openModal(index)}>
<Image source={{ uri }} />
</Pressable>
</GestureTrigger>
important
- Both methods are functionally equivalent
- The
GestureTrigger
will automatically inject the press handler into the child component
- The child component must be pressable (support onPress prop)
- If both
GestureTrigger
and child have onPress, both will be called (child's handler first)
Supported Child Components
You can use any pressable component as a child, such as:
Pressable
TouchableOpacity
TouchableHighlight
Button
- Any custom component that accepts
onPress
prop
Example with Different Components
// Using TouchableOpacity
<GestureTrigger onPress={handlePress}>
<TouchableOpacity>
<Text>View Image</Text>
</TouchableOpacity>
</GestureTrigger>
// Using custom component
function CustomButton({ onPress, children }) {
return (
<Pressable onPress={onPress}>
{children}
</Pressable>
);
}
<GestureTrigger>
<CustomButton onPress={handlePress}>
<Text>Custom Button</Text>
</CustomButton>
</GestureTrigger>
Animation Configuration
You can customize the trigger animation behavior using the triggerAnimation
prop:
import { ReduceMotion, Easing } from 'react-native-reanimated';
function App() {
return (
<GestureViewer
triggerAnimation={{
duration: 400, // Animation duration in ms
easing: Easing.bezier(0.25, 0.1, 0.25, 1.0), // Custom easing function
reduceMotion: ReduceMotion.System, // Respect system reduce motion
onAnimationComplete: () => { // Callback when animation finishes
console.log('Modal animation completed');
}
}}
/>
)
}
Multiple Trigger Instances
You can have multiple trigger instances by using different IDs:
// Photo gallery triggers
<GestureTrigger id="photos" onPress={() => openPhotoModal(index)}>
<Image source={photo} />
</GestureTrigger>
// Video gallery triggers
<GestureTrigger id="videos" onPress={() => openVideoModal(index)}>
<Video source={video} />
</GestureTrigger>
TIP
Make sure the id
prop matches between GestureTrigger
and GestureViewer
components for the animation to work properly. (default value: default
)
Handling Dismissal Animations
onDismissStart Callback
The onDismissStart
callback is triggered immediately when the dismiss animation begins, which is useful for hiding UI elements that should disappear before the animation completes.
function App() {
const [visible, setVisible] = useState(false);
const [showExternalUI, setShowExternalUI] = useState(false);
const handleDismissStart = () => {
setShowExternalUI(false);
};
return (
<Modal visible={visible} transparent>
<GestureViewer
onDismissStart={handleDismissStart}
{...modalProps}
/>
{showExternalUI && (
<View>
<Text>{`${currentIndex + 1} / ${totalCount}`}</Text>
</View>
)}
</Modal>
);
}
Dismissing from Custom Components
You can dismiss the viewer programmatically using the dismiss
helper from renderContainer
:
<GestureViewer
renderContainer={(children, { dismiss }) => (
<View>
{children}
<Pressable
{/*
This button will properly trigger the dismiss animation
back to the original thumbnail position
*/}
onPress={dismiss}
>
<Text>Close</Text>
</Pressable>
</View>
)}
/>
Why Use renderContainer's dismiss?
When using trigger-based animations, it's important to use the dismiss
method provided by renderContainer
instead of directly controlling the visibility with setVisible(false)
. Here's why:
// ❌ Avoid: This will close immediately without trigger animation
<Button onPress={() => setVisible(false)} title="Close" />
// ✅ Preferred: This will use the trigger animation to close
<Button onPress={dismiss} title="Close" />
How It Works:
- With Trigger Animation:
- When
dismiss
is called, the viewer will animate back to the original trigger position
onDismissStart
is called at the start of the animation
onDismiss
is called after the animation completes
- Without Trigger Animation:
- If no trigger animation is configured,
dismiss
will still work as a simple close
Complete Example with Dismiss Handling
function ImageViewer() {
const [visible, setVisible] = useState(false);
const [showUI, setShowUI] = useState(true);
return (
<Modal visible={visible} transparent>
<GestureViewer
onDismissStart={() => setShowUI(false)}
onDismiss={() => setVisible(false)}
renderContainer={(children, { dismiss }) => (
<View>
{children}
{showUI && (
<View>
<Button onPress={dismiss} title="Close" />
</View>
)}
</View>
)}
/>
</Modal>
);
}
Dismiss Behavior with Trigger Animation
When using trigger-based animations, the dismiss animation will animate back to the original trigger position. The onDismissStart
callback is called at the start of this animation, allowing you to hide any UI elements that should not be visible during the dismiss animation.
<GestureViewer
onDismissStart={() => {
console.log('Dismiss animation started');
setShowUI(false);
}}
onDismiss={() => {
console.log('Dismiss animation complete');
setVisible(false);
}}
/>
This pattern ensures a smooth user experience by:
- Immediately hiding UI elements when dismiss starts
- Allowing the dismiss animation to complete naturally
- Cleaning up any resources only after the animation is fully complete
Best Practices
- Always use the
dismiss
method from renderContainer
when you want to close the viewer with animations
- Use
onDismissStart
to hide UI elements that shouldn't be visible during the dismiss animation
- Use
onDismiss
for cleanup operations that should happen after the animation completes
Common Pitfalls
// ❌ Avoid: This will bypass the trigger animation
<Button onPress={() => setVisible(false)} title="Close" />
// ❌ Avoid: This will cause the animation to break
const handleClose = () => {
setShowUI(false);
setVisible(false); // Closes too early
};
<Button onPress={handleClose} title="Close" />
// ✅ Correct: Let the animation complete naturally
<GestureViewer
onDismissStart={() => setShowUI(false)}
onDismiss={() => setVisible(false)}
renderContainer={(children, { dismiss }) => (
<View>
{children}
{showUI && (
<View>
<Button onPress={dismiss} title="Close" />
</View>
)}
</View>
)}
/>
This pattern ensures that your trigger-based animations work consistently and provides the best user experience.
API Reference
GestureTrigger Props
interface GestureTriggerProps<T extends WithOnPress> {
id?: string; // Identifier to associate with GestureViewer (default: "default")
children: ReactElement<T>; // Single pressable child element
onPress?: (...args: unknown[]) => void; // Additional onPress handler
}
TriggerAnimation Config
interface TriggerAnimationConfig {
duration?: number; // Animation duration in milliseconds
easing?: EasingFunction; // Custom easing function
reduceMotion?: boolean; // Respect system reduce motion preference
onAnimationComplete?: () => void; // Callback fired when animation completes
}
Property | Type | Default | Description |
---|
duration | number | 300 | Animation duration in milliseconds |
easing | EasingFunction | Easing.bezier(0.25, 0.1, 0.25, 1.0) | Easing function for the animation |
reduceMotion | ReduceMotion | undefined | Whether to respect system reduce motion settings |
onAnimationComplete | () => void | undefined | Callback fired when the animation completes |
NOTE
The trigger animation works by measuring the trigger element's position when pressed and animating the modal from that position to full screen.