Hi
Hey, you can call me Doho. I am a front-end developer and currently in charge of an App developed by RN in our company.
I’m excited to share some of the lessons I learned using react-native Reanimated and making react-native reanimated carousel, Because there are few Chinese materials and videos about react-native Reanimated, MOST of my learning channels come from official documents and videos of the bald man on YouTube.
Most of the official documents are in English, so there are some obstacles for those who are not very good at English.
Most of the videos on YouTube are good, but some of the explanations are a bit jumpy so there are some areas that are hard to follow.
Therefore, I plan to try to realize the videos I have learned again and produce them in the way of Chinese articles. In fact, I want to exercise my summarizing ability and writing skills. The follow-up will be in the way of article or video series push! ~
My GitHub homepage
My Carousel open source library
Snack demo
Reanimated 2
Why Reanimated? Because in React Native, by default, all updates are delayed by at least one frame, because communication between the UI and the JS thread is asynchronous, and the UI thread never waits for the JS thread to process all events.
And besides JS doing Diff, updates, executing application business logic, handling network requests… In addition, events are often not handled immediately, resulting in more severe delays.
Reanimated’s approach is to move the logic that handles animations and events from the JSt thread to the UI thread.
More details are not the focus of this article, you can Google ~
The cause of
Business needs to add a rotation map to support cyclic scrolling on the home page, so I searched github for some useful components in the community. Excluding some libraries that were updated only four or five years ago, there is still a component library called React-Native Snap-Carousel.
However, I found a problem in using it. When I quickly swiped through the loop, I got stuck, which looked like I had to wait for elements to append to the head or end of the loop.
Because of the time problem, so did not go deep to see the implementation of the source code, the middle of the community we mentioned a variety of ways to try is also unable to solve, mostly by increasing the number of pre-rendering before and after, but in fact, or the way to treat the symptoms.
The library also has a low maintenance frequency and a lot of unresolved issues, although the README says there will be a completely react-native Gesture-handler +react-native Reanimated implementation in the future, and it will be very useful. But it’s been a long time since they announced it and they haven’t released the official version yet, so let’s just do it ourselves, but accidentally use the same library they planned, which is the gesture and animation library above.
What are we trying to solve
You don’t get stuck if you slide sideways quickly while rolling in a loop
Implementation approach
- First we have three images by default and have slid to the second image in the middle
- We drag the image and slide it to the right
- After half of the first image is in the wheel view, we move the last image to the front
- So we’ve done a circular scroll to one side and the other way around. It works by sliding and appending, and relies on Reanimated, so the whole processing logic is still done in the UI thread, and moving images doesn’t cause animation to freeze.
Pre –
Since there may be partners who do not use either library, this section will cover some basic API usage, and refer to the respective documentation for details.
The react – native – gesture – handler, the react – native – reanimated
- PanGestureHandler
After wrapping the view container, you can call back to get parameters for different gesture movements.
- useAnimatedGestureHandler
A simple hook, use on PanGestureHandler onHandlerStateChangeprops, onStart, onActive, can be set in the hook onEnd… Response events for various events.
- useSharedValue
The animation value generated by Reanimated, whose changes affect the behavior of the animation.
- useAnimatedStyle
Reanimated needs to use useAnimatedStyle to generate the style, because it controls the generated style when the SharedValue changes, and also allows the generated style to be associated with Reanimated.View.
- The View element in Reanimated
Using the basic condition of the Reanimated value, after placing the SharedValue in useAnimatedStyle, the returned style can be passed to the Styles property to animate the element.
- useDerivedValue
Responds to a change in a SharedValue value and produces a read-only value.
const number_a = useSharedValue<number> (1);
const number_b = useDerivedValue(() = >{
return number_a.value*10}, [])// number_a = 1
// number_b = 10
Copy the code
- interpolate
Make SharedValue create a map. This is very useful when modifying an animation effect. For example, if we have an avatar, it is 100 in width and enlarged to 200 after login.
Types: interpolate(SharedValue,inputRange,outputRange,? ExtrapolateParameter)
SharedValue: animation value
InputRange: indicates the inputRange
OutputRange: indicates the outputRange
ExtrapolateParameter? : After the input range overflows, whether to change according to the output range (optional)
/ / pseudo code
const loginStatusAnim = useSharedValue<number> (0);
const style = useAnimatedStyle(() = >{
return {
transform: [{scale:interpolate(
loginStatusAnim.value,
// Here our loginStatusAnim will only change between 0 and 1
[0.1].// But we want the output value to map to 100-200, of course we can directly change 0 and 1 to 100 and 200, so here is just a demonstration
[100.200]}]}},[])return <Reanimated.View style={style}></Reanimated.View>
Copy the code
On the whole
The code here is not unpastable pseudo-code, you can paste step by step into the editor, you can see the effect.
- First of all, we can use Expo to initiate a project. Using Expo here can avoid some loose ends and focus more on this attempt.
So we have an initial project that we can use that will smooth out any dependency differences that we might introduce later on.
expo init my-project
cd ./my-project
Copy the code
- Install the library we need to use, in this case to avoid late library upgrades, the API may change, so the version is specified.
Yarn add [email protected] [email protected]Copy the code
- First we need a container to handle the gesture logic, and the container will arrange the elements horizontally. We will use gestures to create a container similar to ScrollView, with more flexible control of sliding left and right. The main logic is in
animatedListScrollHandler
In the.
Then to get the element moving, we need two more steps to generate the element with the offset X and the element with the offset X.
Carousel.tsx
import React from 'react';
import { Dimensions, Text, View } from 'react-native';
import Animated, {
useAnimatedGestureHandler,
useSharedValue,
useDerivedValue
} from 'react-native-reanimated';
import { PanGestureHandler, PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import { useComputedAnim } from './useComputedAnim';
import { Layouts } from './Layouts';
const data = [1.2.3];
const { width } = Dimensions.get('window');
const height = 300;
const Carousel: React.FC = () = > {
// 1. Get the 'base value' to use in the calculation. (Just a wrapper logic)
const computedAnimResult = useComputedAnim(width, data.length);
const animatedListScrollHandler = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onStart:... .onActive:... ,} [])return <PanGestureHandler onHandlerStateChange={animatedListScrollHandler}>{/* // 2. The gesture container specifies the layout elements that need to be nested within Reanimated */}<Animated.View
style={{
// 3.To specify the width and height of the caroute container, most of our calculations need to rely on the knownwidth.width.height.flexDirection: 'row',
position: 'relative'}} >{data.map((_, I) => {return (// 3<Layouts width={width} index={i} key={i} offsetX={offsetX} computedAnimResult={computedAnimResult}>
<View style={{ flex: 1.backgroundColor: "red", justifyContent: "center", alignItems: "center", borderWidth: 1.borderColor: "black}} ">
<Text style={{ fontSize: 100}} >{i}</Text>
</View>
</Layouts>
);
})}
</Animated.View>
</PanGestureHandler>
}
export default Carousel;
Copy the code
useComputedAnim.ts
export interface IComputedAnimResult {
MAX: number;
MIN: number;
WL: number;
LENGTH: number;
}
export function useComputedAnim(
width: number,
LENGTH: number
) :IComputedAnimResult {
/* * 1. After removing the width of the header and tail elements, the distance between them can be slid * because the position of the header and tail elements should be moved to the other side */
const MAX = (LENGTH - 2) * width;
// 2
const MIN = -MAX;
// 3. The total length of the elements
const WL = width * LENGTH;
return {
MAX,
MIN,
WL,
LENGTH,
};
}
Copy the code
- Now we’re going to perfect
animatedListScrollHandler
The logic that allows the gesture to slide can make the container internal offsetX
Change occurs.
Here we are done generating the offset X.
Carousel.tsx
const Carousel:React.FC = () = > {
/ /...
// 1. Offset of position
const handlerOffsetX = useSharedValue<number> (0);
// 2. The offset value needs to be converted to return to 0 after a cycle, which is the actual value we use
const offsetX = useDerivedValue(() = > {
const x = handlerOffsetX.value % computedAnimResult.WL;
return isNaN(x) ? 0 : x;
}, [computedAnimResult]);
// This Hook calls the set method when the gesture occurs and returns parameters that inform the gesture
const animatedListScrollHandler = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
/** * 3. CTX is the temporary context provided by method execution, we can record some temporary variables ** here we drag the current offset to the context */
onStart: (_, ctx: any) = > {
ctx.startContentOffsetX = handlerOffsetX.value;
},
/** * 4. We take the initial position from the context and add it to the X offset of the slide returned by onActive to move the element left and right */
onActive: (e, ctx: any) = >{ handlerOffsetX.value = ctx.startContentOffsetX + e.translationX; }}, [])/ /...
}
Copy the code
- So we have the most important value
offsetX
Because each of them is going to be moved independentlyAt the end of
orThe head
Of the elements to the other side, so to accurately control their position, you need to apply this value to eachCarouselItem elements
.
Layouts.tsx
import React from 'react';
import { FlexStyle, View } from 'react-native';
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
import { IComputedAnimResult } from './useComputedAnim';
import { useOffsetX } from './useOffsetX';
export const Layouts: React.FC<{
index: number;
width: number; height? : FlexStyle['height'];
offsetX: Animated.SharedValue<number>; computedAnimResult: IComputedAnimResult; } > =(props) = > {
const {
index,
width,
children,
height = '100%',
offsetX,
computedAnimResult,
} = props;
/* * 1. This is the core logic that makes the CarouselItem element move correctly, which we'll cover below */
const x = useOffsetX({
offsetX,
index,
width,
computedAnimResult,
});
/* * 2. Reanimated needs to use useAnimatedStyle to generate style * because it will control the generated style when sharedValue changes * and the generated style is also allowed to be associated with Reanimated
const offsetXStyle = useAnimatedStyle(() = > {
return {
/* * 3. Here we need to use 'index * width' to get the elements back to the origin (i.e. they are all stacked together) * and then use the exact value 'x.value' we calculated with 'useOffsetX' to control their position */
transform: [{ translateX: x.value - index * width }], }; } []);return (
// 4. Set the style
<Animated.View style={offsetXStyle}>
<View style={{ width.height}} >{children}</View>
</Animated.View>
);
}
export default Layouts;
Copy the code
Here we have the element with the offset X, and now we have a basic look for the wheel map, which can slide sideways, but we need to complete the logic in useOffsetX if we want it to loop.
- And then finally we need to let
useOffsetX
A hook produces the correct offset value for an element so that it can be converted to the other side near the end or head.
The calculation part of this method is a little tricky, but the general idea is that when the offset value X changes, we make sure that it exceeds the boundary we set, and if it exceeds the boundary, we put it on the other side. It doesn’t matter if you don’t, because maybe you can figure it out and implement your own logic
useOffsetX.ts
import Animated, {
Extrapolate,
interpolate,
useDerivedValue,
} from 'react-native-reanimated';
import type { IComputedAnimResult } from './useComputedAnim';
interface IOpts {
index: number;
width: number;
computedAnimResult: IComputedAnimResult;
offsetX: Animated.SharedValue<number>;
}
export const useOffsetX = (opts: IOpts) = > {
const { offsetX, index, width, computedAnimResult } = opts;
const { MAX, WL, MIN, LENGTH } = computedAnimResult;
const x = useDerivedValue(() = > {
// The offset from the origin of each element
const Wi = width * index;
// The starting value of each element should be rotated to the other side if the boundary is crossed
const startPos = Wi > MAX ? MAX - Wi : Wi < MIN ? MIN - Wi : Wi;
const inputRange = [
// WL is the movable area where the tail and the head are removed
-WL,
// Here is the position condition before crossing the border
-((LENGTH - 2) * width + width / 2) - startPos - 1.// Here is the position condition after crossing the boundary
-((LENGTH - 2) * width + width / 2) - startPos,
/ / the origin
0./ / in the opposite direction
(LENGTH - 2) * width + width / 2 - startPos,
/ / in the opposite direction
(LENGTH - 2) * width + width / 2 - startPos + 1./ / in the opposite direction
WL,
];
const outputRange = [
// the corresponding WL loops once, so it returns to the starting position
startPos,
1.5 * width - 1.// Turn over to the other side
-((LENGTH - 2) * width + width / 2),
// Return to the starting position
startPos,
// Turn over to the other side
(LENGTH - 2) * width + width / 2, -1.5 * width - 1),
// the corresponding WL loops once, so it returns to the starting position
startPos,
];
// Return the calculated X value, which is an absolute position relative to the origin, but our elements are arranged in order, so subtract index*width and put them at the origin
returninterpolate( offsetX.value, inputRange, outputRange, Extrapolate.CLAMP ); } []);return x;
};
Copy the code
- At this point you should have a wheel map that slides sideways without stalling, but this is a very simple version. In fact, IN my library, I also made some hack fixes for the bugs of the react-native Gesture-handler and react-native Reanimated libraries, as well as some interactive optimisations, such as the inertia effect after dragging, and the paging effect. , these are not the scope of this article, so you can go to my warehouse to have a look! react-native-reanimated-carousel
React-native – Reanimated – Carousel-example
More functions
I will improve more APIS in my react-Native Reanimated Carousel project to make this component easier to use, but it may not increase the complex UI effects of react-Native Snap-Carousel. The goal is to make this component simpler and more flexible.
Hope more partners can participate in the maintenance together, or to make more suggestions, come, come! To project
At the end of
Thank you for reading and I look forward to receiving suggestions, questions or corrections.
Star 🌟, react-native-reanimated- Carousel, thank you!
I will write more articles on react-Native reanimated V2 in the future. I hope it will be helpful for students and families! My GitHub homepage
Ps: Reprint please indicate the source