How to do series/parallel animation in react-native-reanimated 2? - react-native

I'm looking for the equivalent of Animated.sequence and Animated.parallel from react-native. So far from the docs for v2, I could only see the withSequence function that changes the value of only on value and therefore the style of only one component in series.
What I was looking for was to trigger animations in two different components, either in series or in parallel.
For parallel, it seems changing values in statements one after another worked. Correct me if I'm wrong.
// these will run in parallel
val1Shared.value = withTiming(50);
val2Shared.value = withTiming(100);
But for series, I need to have each animation inside a useTiming callback. Which leads to kind of callback hell.
val1Shared.value = withTiming(50, undefined, () => {
val2Shared.value = withTiming(100);
});
Please help with the best practices in achieving this with reanimated 2.
Thanks.

I think there is no way of doing it like in Animated.
This library requires you to think a different way. If you want you run two animations (each one for a separate view) using interpolation, you can use a single shared value and a sufficient interpolation config.
Let's assume (according to your example) that you have two views you want to animate. The first animation is from 0 to 50, the second one is from 0 to 100.
Let's also assume you're transforming an X axis (I don't know what's your case, but it's going to be very similar) and we count the animation progress from 0 to 1, and both views will animate for the same period of time.
Initialize the shared value:
const animation = useSharedValue(0);
And do sth like this (of your choice):
animated.value = withTiming(1, { duration, easing });
For the first view, your transformation will look like this:
const animatedStyles1 = useAnimatedStyle(
() => ({
transform: [
{
translateX: interpolate(animation.value, [0, 0.5, 1], [0, 50, 50]),
},
],
}),
[mode, rectSize]);
and for the second one, like this:
const animatedStyles2 = useAnimatedStyle(
() => ({
transform: [
{
translateX: interpolate(animation.value, [0, 0.5, 1], [0, 0, 100]),
},
],
}),
[mode, rectSize]);
Now the explanation.
For the first view, we only work for the half of progress from the whole animation, so we from 0 to 0.5 we'll animate from 0 to 50. then, for the reset of the progress we'll stay in the same place (this is called a dead zone), to from 0.5 to 1 value of the progress does not change (50 to 50).
Now, for the second view, we wait for the progress to be in half, so from 0 to 0.5 we don't do anything (0 to 0). Then, when the progress is in half, we animate the view from 0 to 150.
In general, that way you can orchestrate the whole thing using just a single shared value.
Here you can find more info about the interpolation. The docs is from the react-native's Animated, but the idea is the same.
Good luck!

Related

Is it possible to control which layers are in front on deck.gl?

On Leaflet, when I create a layer from a GeoJSON, I have a way of controlling which layer is shown in front of the map by looping the layer features and then using a function like feature.bringToFront(). The result is like the following:
On deck.gl however, after creating my layers from a GeoJSON, it's hard to tell which layer is in front of the map... The following image is the same example made with deck.gl instead of Leaflet.
So, is there any way of controlling the feature that I want to show in front with deck.gl? I know I could change the elevation when the view of the map is from above, but in this case, it's not a good solution when I'm navigating through the 3D map. Is it possible to do on deck.gl? Can I force some specific feature to appear in front of others? Is there any parameter or function that controls that?
Have been working on this for a few hours and from a leafletJs background.
The closest I've gotten to a feature.bringToFront equivalent is that you can order the layers and pass that to the DeckGl component, a description from the deck.gl documentation is here:
https://deck.gl/docs/developer-guide/using-layers#rendering-layers
The below works for me, however, it seems to work only for layers of the same type. In the example below, if I add a LineLayer type at index 0 of the layers array below, it will render in front of the two SolidPolygonLayer's below.
const layers = [
new SolidPolygonLayer({
id: 'at array index 0 and appears in the background',
data: helper.getPolygonData(),
getPolygon: d => d.coordinates,
getFillColor: d => [255, 0, 0],
filled: true,
}),
new SolidPolygonLayer({
id: 'at array index 1 and appears in the foreground',
data: helper.getPolygonData(),
getPolygon: d => d.coordinates,
getFillColor: d => d.color,
filled: true,
}),
];
return (
<DeckGL
layers={layers}
viewState={getViewState()}
onViewStateChange={e => setViewState(e.viewState)}
initialViewState={INITIAL_VIEW_STATE}
controller={true}
getTooltip={getTooltip}
>
...
</DeckGL>
);

how can I stop the wheel of fortune to particular point using Animated API in React Native?

I'm developing wheel of fortune where I want the wheel to iterate four times while spinning, and stop to a particularly given value. All I want is to spin the wheel four times and stop to a particular slice. I'm thinking below code can work my idea.
Animated.loop(
Animated.timing(this._angle, {
toValue: 920,
duration: 5000,
}),//.start();
{
iterations: 1
}
).start();
I'm using angle as value. My formula to get the 'toValue' is "targetAngle * 4". This works but I want to decelerate the spinning in its last iterate just like Animated.decay() does. Is there any better idea to resolve this issue?
you can do
const onPress = () => {
Animated.timing(RotateAnimeted, {
toValue: 6,
duration: 1000,
}).start();
};
const Rotate = RotateAnimeted.interpolate({
inputRange: [0, 1, 2, 3, 4, 5, 6],
outputRange: [
"0deg",
"360deg",
"720deg",
"840deg",
"940deg",
"1020deg",
"1080deg",
],
});
You can change the values to get the result you want
I did an example at the expo
Attaches a gif image

Prepending data to a FlatList always shows the first child

this is our FlatList, say hello:
<FlatList
data={this.state.dates}
...
/>
we feed it with the following dates:
this.state = {
dates: [
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
};
then when the visible date changes (onViewableItemsChanged), if we end up to the first item (21/06/2019), we prepend data, so that the new state becomes:
dates: [
'18/06/2019',
'19/06/2019',
'20/06/2019',
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
The Problem:
right after we prepend the data, instead of STILL seeing 21/06/2019 (which was the date when the prepend took place) we now see 19/06/2019.
That's because below the hood, 21/06/2019 used to be index 0, but after the prepend, index 0 corresponds to 19/06/2019.
What we want:
I'm trying to make it so that the day remains the same after prepending data.
Please don't tell me to use scrollToPosition because that's a hack really, not the solution.
Is there any good solution to that problem?
Thank you
There is an undocumented prop maintainVisibleContentPosition on ScrollView that do what you want, but unfortunately it's not working on android
I found another workaround by keep latest y offset with onScroll and also save content height before and after adding new items with onContentSizeChange and calculate the difference of content height, and set new y offset to the latest y offset + content height difference!
I've opened an issue on github also, but there is not any complete solution yet
Thanks to sgtpepper43 for undocumented iOS solution
that maintainVisibleContentPosition does not work if you prepend data while you are at a scroll position of 0 (all the way to the top). It does work if you are prepending while not a the top. Even scroll Y of 1 would be enough.
check this
Unfortunately this still an issue even in React Native 0.63.3 and has not been solved.
Leaving this here, since it took me a while to get a working solution/ workaround to this problem that doesn't leave a bad taste in my mouth: prepopulate the array with empty data, and then use the scrollToIndex method.
this.state = {
dates: [
'',
'',
'',
'21/06/2019',
'22/06/2019',
'23/06/2019',
]
};
And then:
<FlatList
data={this.state.dates}
ref={flatListRef}
getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}
...
/>
inside your componentDidMount:
const startingIndex = 4
flatListRef.current.scrollToIndex({index: startingIndex, animated: false, viewPosition: 0})
There's a library: https://github.com/GetStream/react-native-bidirectional-infinite-scroll that allows you to both preprend or append data and preserve scroll position
It's basically an extension over the FlatList and supports all the props available for a plain FlatList
From their example usage tutorial: https://dev.to/vishalnarkhede/react-native-how-to-build-bidirectional-infinite-scroll-32ph#%F0%9F%96%A5-tutorial-chat-ui-with-bidirectional-infinite-scroll
It looks like it would be enough to just prepend that data to the top exactly like you're trying to do

Creating a parallax image in React Native

I'm trying to create an animated "parallax" image in React-Native. I have a static background image and two individual images as an overlay. The goal is to create something similar to the image found on this website http://www.digitalhands.net/ or https://www.galaxia.co/. Where do I even start with this? Initially I'd be happy with it moving just on itself from left to right and etc. Afterwards I want to make it so that it would use the gyroscope to get the x and y values for animating the image.
A parallax effect consists of images moving in different speeds and in the same direction, such that the 'closer' an object is, the faster it moves, which creates the illusion of three dimensions.
To achieve this effect in react-native, you can use the Animated library to interpolate the position of one image as a fraction of the position of another.
For the sake of an example, let's assume you want the parallax effect in the vertical direction, and that all images are positioned at 0 vertically.
First, you would need an animated value in your component's state:
this.state = {
...
imagePos: new Animated.Value(0)
}
Then, for each Image style you can add a transform on its y axis:
<Animated.Image src={...} style={[firstImageStyle, {
transform: [{translateY: this.state.imagePos.interpolate({
inputRange: [0, 1],
outputRange: [0, 100]
})}]
}]}
<Animated.Image src={...} style={[secondImageStyle, {
transform: [{translateY: this.state.imagePos.interpolate({
inputRange: [0, 1],
outputRange: [0, 50]
})}]
}]}
Note the use of Animated.Image instead of Image to make the image animatable.
This would cause the first image to move horizontally between 0-100 dp when imagePos has values between 0 and 1, and the second image to move between 0-50 dp.
To change the value of the animated value you can use any of the functions in the Animated library (timing, spring, decay, etc.) or you can attach it to some native event.
The animated library documentation has much more detail.
As for the use of the gyroscope, I haven't gone into the details, but you can probably use react-native-sensors or react-native-motion-manager to get the values you need, and attach them to the animation.
By using a package like https://github.com/react-native-sensors/react-native-sensors I managed to get the x, y, z values of the phones position and animate an image with it.
constructor() {
super();
this.state = {
x: 0,
y: 0,
z: 0,
};
}
componentWillMount() {
// Normal RxJS functions
accelerationObservable
.subscribe(({x, y, z}) => this.setState({
x: x,
y: y,
z: z,
}));
}
render() { return (
<Image style={{left: this.state.x + this.state.y, top: this.state.z}}
source={require('image.png')}/>
)
}

How to make draggable remember it's location in React Native

I'm learning about the animation and panresponder apis in react native. Following Animated Drag and Drop with React Native and the source
I want to extend the example so that you can drag the circle without having to reset it. Currently the code animates the circle back to the center of the screen and relies on the panresponders dX/dY values.
my best guess is I have to change something in onPanResponderMove
onPanResponderMove: Animated.event([null, {
dx: this.state.pan.x,
dy: this.state.pan.y,
}]),
What do I need to change in the source so if I comment out the onPanResponderRelease logic the circle properly drags around the screen?
getTranslateTransform()?
The logic in onPanResponderRelease is making it so that if the draggable is not in the dropzone when released, it is reset back to 0,0 (these are the lines causing it).
Removing that logic alone isn't all you should do though. You should set the offset and reset the value of this.state.pan in onPanResponderRelease. To do this you need to track the value.
In componentDidMount, add this:
this.currentPanValue = {x: 0, y: 0};
this.panListener = this.state.pan.addListener((value) => this.currentPanValue = value);
Then, in componentWillUnmount:
this.state.pan.removeListener(this.panListener);
Now that you have the current value, you just add this to the PanResponder:
onPanResponderRelease: (e, gestureState) => {
this.state.pan.setOffset({x: this.currentPanValue.x, y: this.currentPanValue.y});
this.state.pan.setValue({x: 0, y: 0});
},
It seems more complicated than it actually is. Basically you are just setting the offset from 0/0, then setting the value to 0/0 so that when you start moving it again after having released it before, it doesn't jump back to 0/0 before jumping back to where your finger is. It also makes sure you have its current position...which you will probably need at some point anyways.
Another way would be to track current offset and keep adding it to the pan. So declare this in the constructor currentPanValue : {x: 0, y: 0},
And edit the onPanResponderRelease to the following :
onPanResponderRelease : (e, gesture) => {
this.state.currentPanValue.x += this.state.pan.x._value;
this.state.currentPanValue.y += this.state.pan.y._value;
this.state.pan.setOffset({x: this.state.currentPanValue.x, y: this.state.currentPanValue.y});
this.state.pan.setValue({x: 0, y: 0});
}
Incase if you want the final coordinates, you should get it with this.state.currentPanValue
Actually the only thing you need to change is the onPanResponderRelease function. What you are currently doing is applying the delta of the gesture movement to the initial position of the component. What you want to do is applying it to the position at the end of the last gesture. So you somehow need to save the offset. AnimatedValueXY.setOffset to the rescue!
onPanResponderRelease : (e, gesture) => {
if(this.isDropZone(gesture)){
this.setState({
showDraggable : false
});
}else{
this.state.pan.setOffset({
x: this.state.pan.x._offset + e.dx,
y: this.state.pan.y._offset + e.dy,
})
this.state.pan.setValue({x: 0, y: 0})
}
I'm using the internal _offset variable here because I could find a way to access it via the API. You can off course also just keep track of the offset manually. I didn't test this code yet, but you get the idea.