Mastering Animated React-Native: Part 2

Hiran Aloka
8 min readMar 26, 2023

--

Update: This article is part of a series. Check out the full series: Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7, and Part 8! In Part 1, we said the basic stuff of animation and parallel and sequential animations. (If you haven’t already read part 1, read it now!).

Mobile app developers are always on the lookout for ways to enhance the user experience, and one way to do this is through the use of gestures. These seemingly simple swipes and flicks are actually a complex set of actions that can be used to navigate through an app, control media playback, and more. However, many developers struggle to implement gestures effectively, leading to frustration for users and missed opportunities for engagement. That’s why we’ve put together this guide to unlocking the secrets of gestures. We’ll explore the different types of gestures, how to implement them in your app, and best practices for ensuring a smooth and intuitive user experience. Whether you’re a seasoned developer or just starting out, this guide will give you the tools to create engaging and user-friendly apps that keep your users coming back for more. So, let’s dive in and discover the power of gestures!

let’s map the learning curves of the gestures. Type of gestures:

  • Navigational gestures
    - Tap
    - Scroll and pan
    - Drag
    - Swipe
    - Pinch
  • Action gestures
    - Tap
    - Long press
    - Swipe
    - Transform gestures
  • Transform gestures
    - Double tap
    - Pinch
    - Compound gestures
    - Pick up and move

Navigational gestures

Navigational gestures are hand gestures or finger movements used to interact with touchscreens, trackpads, or other digital interfaces to navigate between screens.

Tap

This gesture involves tapping on the screen with one or more fingers to select an item or perform an action, such as users can navigate to destinations by touching elements.

Stack Navigator exposes various options to configure the transition animation when a screen is added or removed. These transition animations can be customized per screen by specifying options in the Options prop for each screen.

gestureDirection - The direction of swipe gestures:

transitionSpec - An object which specifies the animation type (timing or spring) and their options (such as duration for timing)

cardStyleInterpolator - This is a function that specifies interpolated styles for various parts of the card.

headerStyleInterpolator - This is a function that specifies interpolated styles for various parts of the header.

Try this example https://github.com/alokaLimark/animation/tree/tutorial_2/pan_gestures

const customTransition = {
gestureEnabled: true,
gestureDirection: 'horizontal',
transitionSpec: {
open: TransitionSpecs.TransitionIOSSpec,
close: TransitionSpecs.TransitionIOSSpec,
},
cardStyleInterpolator: ({current, next, layouts}) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
{
rotate: current.progress.interpolate({
inputRange: [0, 1],
outputRange: ['180deg', '0deg'],
}),
},
{
scale: next
? next.progress.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.7],
})
: 1,
},
],
},
opacity: current.opacity,
};
},
};

This is a JavaScript object that defines a custom transition animation for a navigation stack in a React Native app. Here is a breakdown of each property:

  • gestureEnabled: a boolean that determines whether the user can use gestures to navigate between screens. If set to true, the user can swipe horizontally to move between screens.
  • gestureDirection: a string that determines the direction of the gesture. In this case, it is set to 'horizontal', meaning that the user can swipe left or right to navigate.
  • transitionSpec: an object that defines the animation to be used when navigating between screens. It has two properties: open and close, each of which is an object that defines the animation when opening or closing a screen, respectively. In this case, the TransitionIOSSpec animation is used, which is a standard iOS-style animation.
  • cardStyleInterpolator: a function that defines how the card (screen) should be animated when navigating between screens. It takes an object with three properties: current, next, and layouts. current is the current screen being animated, next is the next screen that will be shown (if any), and layouts is an object that contains the screen's dimensions. The function returns an object with two properties: cardStyle and opacity. cardStyle is an object that defines the transformations to be applied to the card, such as translating, rotating, or scaling it. opacity determines the opacity of the card during the animation.

In summary, this code defines a custom transition animation that allows the user to swipe horizontally between screens in a React Native app. The animation uses a standard iOS-style animation and applies translations, rotations, and scaling to the cards being animated.

The division of the screen into vertical and horizontal sections is a simple task, as it only involves establishing the direction of the intended motion. However, creating a custom animation can be somewhat more complex. For instance, when pressing the custom animation screen, a 180-degree rotation occurs, followed by the appearance of a new screen. At that moment, both the scale and opacity of the screen undergo changes.

Scroll and pan

I will briefly introduce the topic of scrolling, and in Part 3, I will delve deeper into various aspects and techniques related to scrolling. By using the Animated library, it enables you to create animated effects that can be applied to the ScrollView component, such as smoothly scrolling to a specific position, animating the content offset or content inset of the scroll view, and triggering other animations or events based on the user's scrolling behavior.

PanResponder is a built-in API in React Native that enables gesture recognition and response to touch events. It allows you to respond to different gestures, such as dragging, swiping, pinching, and tapping, on a component or view.

When you create a PanResponder, you define callbacks for various touch events, such as onStartShouldSetPanResponder, onMoveShouldSetPanResponder, onPanResponderMove, onPanResponderRelease, and onPanResponderTerminate. These callbacks are invoked by the PanResponder when a user interacts with the component, allowing you to respond to the gesture in various ways.

For example, you can use a PanResponder to create a draggable component by tracking the movement of the user's finger and updating the position of the component accordingly. You can also use it to recognize a swipe gesture and trigger an animation or navigation action.

Here is an example of creating a simple PanResponder:

import React, {useRef} from 'react';
import {Animated, View, StyleSheet, PanResponder, Text} from 'react-native';

const App = () => {
const pan = useRef(new Animated.ValueXY()).current;

const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, {dx: pan.x, dy: pan.y}]),
onPanResponderRelease: () => {
pan.extractOffset();
},
}),
).current;

return (
<View style={styles.container}>
<Animated.View
style={{
transform: [{translateX: pan.x}, {translateY: pan.y}],
}}
{...panResponder.panHandlers}>
<View style={styles.box} />
</Animated.View>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
titleText: {
fontSize: 14,
lineHeight: 24,
fontWeight: 'bold',
},
box: {
height: 100,
width: 100,
backgroundColor: 'red',
borderRadius: 100 / 2,
},
});

export default App;

In this example, we create a draggable component that responds to touch events. The onStartShouldSetPanResponder callback returns true to allow the component to become the responder for touch events. The onPanResponderMove callback updates the position of the component based on the user's finger movement, and the onPanResponderRelease callback triggers a spring animation to reset the component's position when the user releases their finger.

Sure, let’s take this React Native application to the next level. The app currently provides the ability to zoom in and out of an image using pinch gestures and move the image around using pan gestures. However, the pan gestures can be further customized to perform multiple actions, such as panning to zoom into specific details of an image or a map, and also zooming out to its original size.

In other words, the app can be enhanced to provide more granular control over the pan gestures. For instance, when zooming into an image or a map, the user can use pan gestures to move around and explore specific details of the zoomed-in area. Conversely, when zooming out, the user can use pan gestures to move back to the original size and position of the image or map.

By enabling these advanced pan gestures, users can enjoy a more immersive and intuitive experience when interacting with images and maps and have greater control over the zoom and pan functions.

import React, {useState, useRef, createRef} from 'react';
import {View, Text, Image, Animated, Dimensions} from 'react-native';
import {
PanGestureHandler,
PinchGestureHandler,
State,
} from 'react-native-gesture-handler';

const App = () => {
const [panEnabled, setPanEnabled] = useState(false);

const scale = useRef(new Animated.Value(1)).current;
const translateX = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(0)).current;

const pinchRef = createRef();
const panRef = createRef();

const onPinchEvent = Animated.event(
[
{
nativeEvent: {scale},
},
],
{useNativeDriver: true},
);

const onPanEvent = Animated.event(
[
{
nativeEvent: {
translationX: translateX,
translationY: translateY,
},
},
],
{useNativeDriver: true},
);

const handlePinchStateChange = ({nativeEvent}) => {
// enabled pan only after pinch-zoom
if (nativeEvent.state === State.ACTIVE) {
setPanEnabled(true);
}

// when scale < 1, reset scale back to original (1)
const nScale = nativeEvent.scale;
if (nativeEvent.state === State.END) {
if (nScale < 1) {
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
}).start();
Animated.spring(translateX, {
toValue: 0,
useNativeDriver: true,
}).start();
Animated.spring(translateY, {
toValue: 0,
useNativeDriver: true,
}).start();

setPanEnabled(false);
}
}
};

return (
<View>
<PanGestureHandler
onGestureEvent={onPanEvent}
ref={panRef}
simultaneousHandlers={[pinchRef]}
enabled={panEnabled}
failOffsetX={[-1000, 1000]}
shouldCancelWhenOutside>
<Animated.View>
<PinchGestureHandler
ref={pinchRef}
onGestureEvent={onPinchEvent}
simultaneousHandlers={[panRef]}
onHandlerStateChange={handlePinchStateChange}>
<Animated.Image
source={{
uri: 'https://www.linkpicture.com/q/58bd4fc9ebfccc1f2de419529bbf1a12.jpg',
}}
style={{
width: '100%',
height: '100%',
transform: [{scale}, {translateX}, {translateY}],
}}
resizeMode="contain"
/>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
);
};

export default App;

The main component of the app is the App function, which sets up the necessary state and refs for handling pinch and pan gestures on the image. It uses useState to keep track of whether pan gestures should be enabled and useRef to create references to the scale, translationX, and translationY values that are animated when handling pinch and pan events.

The onPinchEvent and onPanEvent functions use Animated.event to update the scale, translationX, and translationY values when pinch and pan events occur. These values are then used to transform the image in the style prop of the Animated.Image component.

The handlePinchStateChange function is called when pinch gestures start, change, or end. When pinch gestures start or change, it sets panEnabled to true so that pan gestures can be enabled. When pinch gestures end, it checks whether the scale is less than 1 (i.e. the image is zoomed out), and if so, it uses Animated.spring to reset the scale, translationX, and translationY values back to their original values, and disables pan gestures by setting panEnabled to false.

Finally, the App function returns a View component that wraps a PanGestureHandler and an Animated.View that wraps a PinchGestureHandler and an Animated.Image. The PanGestureHandler is configured to handle pan events only when panEnabled is true, and the PinchGestureHandler is configured to handle pinch events and call the handlePinchStateChange function when pinch gestures start, change, or end. The Animated.Image component displays an image that can be zoomed and panned using pinch and pan gestures.

Conclusion

Hand gesture animations allow for more natural and intuitive interaction with mobile applications, which can lead to increased user engagement and satisfaction. Additionally, these animations can be used to convey important information or provide visual feedback to the user, further enhancing the usability and accessibility of the application. Overall, the use of hand gesture animations in React Native mobile applications is a powerful tool for developers to create engaging and intuitive experiences for their users. With the continued growth and development of React Native, we can expect to see even more exciting innovations in this area in the future.

--

--

Hiran Aloka

iOS & Android & RN Developer, XR Developer, Designer, Dreamer