Looping animations in "React Native Reanimated 2" sync after a while - javascript

I was using the reanimated v2 for creating animations for an app. I was creating a splash (loading) screen with three dots that jump up and down. The 3 dots are supposed to have certain constant delay (interval) between them. Like the animation when the other person is typing in facebook's messenger.
The animation looks fine in the beginning but after a while the 2 dots or even 3 dots depending on the delays and duration sync up and I am left with 2 or 3 dots absolutely in sync with each other. Here is the video of the problem animation video
I am very new to react-native and reanimated. So I am assuming the problem is in my code. I have no idea if this is the correct way to do this. The code examples I see in reanimated v1 have "startClock" and custom "runTiming" functions but I couldn't find them in the docs for v2. reanimated docs
import React, { useEffect } from "react";
import { View, StyleSheet } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
withDelay,
} from "react-native-reanimated";
import { themeColor } from "../../assets/ThemeColor";
const Loading = () => {
const y1 = useSharedValue(0);
const y2 = useSharedValue(0);
const y3 = useSharedValue(0);
const animatedStyles1 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
0,
withRepeat(withTiming(y1.value, { duration: 200 }), -1, true)
),
},
],
};
});
const animatedStyles2 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
100,
withRepeat(withTiming(y2.value, { duration: 200 }), -1, true)
),
},
],
};
});
const animatedStyles3 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
200,
withRepeat(withTiming(y3.value, { duration: 200 }), -1, true)
),
},
],
};
});
/**
*
*
*
*
*/
useEffect(() => {
y1.value = -10;
y2.value = -10;
y3.value = -10;
}, []);
/**
*
*
*
*
*
*/
return (
<View style={styles.loadingContainer}>
<Animated.View
style={[styles.ballStyle, animatedStyles1]}
></Animated.View>
<Animated.View
style={[styles.ballStyle, animatedStyles2]}
></Animated.View>
<Animated.View
style={[styles.ballStyle, animatedStyles3]}
></Animated.View>
</View>
);
};
const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
ballStyle: {
width: 13,
height: 13,
backgroundColor: themeColor,
borderRadius: 13,
margin: 10,
},
});
export default Loading;
Can someone please tell me why the animations sync up eventually and what is the correct way to animate three elements with the same animation but some constant delay. Thank you.

Related

How to create a compass that points to specific coordinates (React-Native)

Here is what I have for now:
import {
Alert,
Animated,
Easing,
Linking,
StyleSheet,
Text,
View,
} from "react-native";
import React, { useEffect, useState } from "react";
import * as Location from "expo-location";
import * as geolib from "geolib";
import { COLORS } from "../../assets/Colors/Colors";
export default function DateFinder() {
const [hasForegroundPermissions, setHasForegroundPermissions] =
useState(null);
const [userLocation, setUserLocation] = useState(null);
const [userHeading, setUserHeading] = useState(null);
const [angle, setAngle] = useState(0);
useEffect(() => {
const AccessLocation = async () => {
function appSettings() {
console.warn("Open settigs pressed");
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else RNAndroidOpenSettings.appDetailsSettings();
}
const appSettingsALert = () => {
Alert.alert(
"Allow Wassupp to Use your Location",
"Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
[
{
text: "Cancel",
onPress: () => console.warn("Cancel pressed"),
},
{ text: "Open settings", onPress: appSettings },
]
);
};
const foregroundPermissions =
await Location.requestForegroundPermissionsAsync();
if (
foregroundPermissions.canAskAgain == false ||
foregroundPermissions.status == "denied"
) {
appSettingsALert();
}
setHasForegroundPermissions(foregroundPermissions.status === "granted");
if (foregroundPermissions.status == "granted") {
const location = await Location.watchPositionAsync(
{
accuracy: Location.Accuracy.BestForNavigation,
activityType: Location.ActivityType.Fitness,
distanceInterval: 0,
},
(location) => {
setUserLocation(location);
}
);
const heading = await Location.watchHeadingAsync((heading) => {
setUserHeading(heading.trueHeading);
});
}
};
AccessLocation().catch(console.error);
}, []);
useEffect(() => {
if (userLocation != null) {
setAngle(getBearing() - userHeading);
rotateImage(angle);
}
}, [userLocation]);
const textPosition = JSON.stringify(userLocation);
const getBearing = () => {
const bearing = geolib.getGreatCircleBearing(
{
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
},
{
latitude: 45.47200370608976,
longitude: -73.86246549592089,
}
);
return bearing;
};
const rotation = new Animated.Value(0);
console.warn(angle);
const rotateImage = (angle) => {
Animated.timing(rotation, {
toValue: angle,
duration: 1000,
easing: Easing.bounce,
useNativeDriver: true,
}).start();
};
//console.warn(rotation);
return (
<View style={styles.background}>
<Text>{textPosition}</Text>
<Animated.Image
source={require("../../assets/Compass/Arrow_up.png")}
style={[styles.image, { transform: [{ rotate: `${angle}deg` }] }]}
/>
</View>
);
}
const styles = StyleSheet.create({
background: {
backgroundColor: COLORS.background_Pale,
flex: 1,
// justifyContent: "flex-start",
//alignItems: "center",
},
image: {
flex: 1,
// height: null,
// width: null,
//alignItems: "center",
},
scrollView: {
backgroundColor: COLORS.background_Pale,
},
});
I think that the math I'm doing must be wrong because the arrow is pointing random directions spinning like crazy and not going to the coordinate I gave it. Also, I can't seem to use the rotateImage function in a way that rotation would be animated and i'd be able to use it to animate the image/compass. If anyone could help me out i'd really appreciate it I've been stuck on this for literally weeks.

How to make different durations with useSpring?

I want my component to have an animation that if my mouse enter,it will be fully displayed in 0.3s,and if my mouse leave,it will disappear in 0.1s.But useSpring can just define one duration just like the code below,which cause that the component will be displayed and disappear all in 0.3s.How can I define different duration for from->to and to->from?Thanks for anyone who can help.
const animationStyle = useSpring({
bottom: show ? 0 : -71,
from: {
bottom: -71
},
config: { duration: 300 }
})
Like this
import import React, { useState } from 'react';
import { animated, useSpring } from '#react-spring/web';
const SuspendedComponent: React.FC = ({ children }) => {
const [isMouseEnter, setMouseEnter] = useState<boolean>(false);
const _style = useSpring({
bottom: isMouseEnter ? 0 : -71,
from: {
bottom: -71,
},
config: { duration: 300 },
});
function onMouseHandler(isEnter: boolean) {
setMouseEnter(isEnter);
}
return (
<div
style={{ display: 'block', width: '100px', height: '100px' }}
onMouseEnter={() => onMouseHandler(true)}
onMouseLeave={() => onMouseHandler(false)}
>
{isMouseEnter && <animated.div style={_style}>{children}</animated.div>}
</div>
);
};
export default SuspendedComponent;
You can control animated.div element display by onMouseEnter and onMouseLeave event.

React-Native: Get X and Y coordinates of draggable Object using Reanimated 2 API

I am developing an app, which provides the design of different shapes like Square , I have used the following Reanimated 2 API . I want to get its coordinates (pageX and PageY) every time I move the object.
my research
So i read this article about measure and i decided to try with that.
Then i created ref to my animated view like that const aref = useAnimatedRef();.
After that i read again in Reanimated docs that i should use useDerivedValue combined with measure. But unfortunately I didn't get what I needed in the end. My app crashed with following error -
Tried to synchronously call usederivedvalue from a different thread
My code so far i tried to use runOnJs - no luck again! And my question is -
import React from "react";
import { StyleSheet, View } from "react-native";
import {
GestureHandlerRootView,
PanGestureHandler,
} from "react-native-gesture-handler";
import Animated, {
measure,
runOnJS,
useAnimatedGestureHandler,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from "react-native-reanimated";
const SIZE = 100.0;
const CIRCLE_RADIUS = SIZE * 2;
export default function App() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const aref = useAnimatedRef();
const panGestureEvent = useAnimatedGestureHandler({
onStart: (event, context) => {
context.translateX = translateX.value;
context.translateY = translateY.value;
},
onActive: (event, context) => {
translateX.value = event.translationX + context.translateX;
translateY.value = event.translationY + context.translateY;
const wrap = () => {
try {
const pageX = measure(aref).pageX;
console.log({ pageX });
} catch {}
};
useDerivedValue(() => {
runOnJS(wrap)(true);
});
},
onEnd: () => {
const distance = Math.sqrt(translateX.value ** 2 + translateY.value ** 2);
if (distance < CIRCLE_RADIUS + SIZE / 2) {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
}
},
});
const rStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translateX.value,
},
{
translateY: translateY.value,
},
],
};
});
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<View style={styles.container}>
<View style={styles.circle}>
<PanGestureHandler onGestureEvent={panGestureEvent}>
<Animated.View style={[styles.square, rStyle]} ref={aref} />
</PanGestureHandler>
</View>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
square: {
width: SIZE,
height: SIZE,
backgroundColor: "rgba(0, 0, 256, 0.5)",
borderRadius: 20,
},
circle: {
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
alignItems: "center",
justifyContent: "center",
borderRadius: CIRCLE_RADIUS,
borderWidth: 5,
borderColor: "rgba(0, 0, 256, 0.5)",
},
});
My error - every time when i move my square my app crash with error above.
How can i get coordinates (pageX and PageY) every time I move the object.
What is causing the problem?
Of course this is the use of
const wrap = () => {
try {
const pageX = measure(aref).pageX;
console.log({ pageX });
} catch {}
};
useDerivedValue(() => {
runOnJS(wrap)(true);
});
in onActive event but i don't know how to fix that ?
Sorry for my bad English.

How to animate mapped elements one at a time in react-native?

I mapped an object array to create a tag element with the details being mapped onto the element. And then I created an animation so on render, the tags zoom in to full scale. However, I was wanting to take it to the next step and wanted to animate each tag individually, so that each tag is animated in order one after the other. To me, this seems like a common use of animations, so how could I do it from my example? Is there any common way to do this that I am missing?
import {LeftIconsRightText} from '#atoms/LeftIconsRightText';
import {LeftTextRightCircle} from '#atoms/LeftTextRightCircle';
import {Text, TextTypes} from '#atoms/Text';
import VectorIcon, {vectorIconTypes} from '#atoms/VectorIcon';
import styled from '#styled-components';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {Animated, ScrollView} from 'react-native';
export interface ICustomerFeedbackCard {
title: string;
titleIconName: string[];
tagInfo?: {feedback: string; rating: number}[];
}
export const CustomerFeedbackCard: React.FC<ICustomerFeedbackCard> = ({
title,
titleIconName,
tagInfo,
...props
}) => {
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
const zoomAnim = useRef(new Animated.Value(START_ZOOM_SCALE)).current;
/**
* Creates an animation with a
* set duration and scales the
* size by a set factor to create
* a small zoom effect
*/
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: FINAL_ZOOM_SCALE,
duration: FAST_ZOOM,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
/**
* Sorts all tags from highest
* to lowest rating numbers
* #returns void
*/
const sortTags = () => {
tagInfo?.sort((a, b) => b.rating - a.rating);
};
/**
* Displays the all the created tags with
* the feedback text and rating number
* #returns JSX.Element
*/
const displayTags = () =>
tagInfo?.map((tag) => (
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
return (
<CardContainer {...props}>
<HeaderContainer>
<LeftIconsRightText icons={titleIconName} textDescription={title} />
<Icon name="chevron-right" type={vectorIconTypes.SMALL} />
</HeaderContainer>
<ScrollOutline>
<ScrollContainer>
{sortTags()}
{displayTags()}
</ScrollContainer>
</ScrollOutline>
<FooterContainer>
<TextFooter>Most recent customer compliments</TextFooter>
</FooterContainer>
</CardContainer>
);
};
And here is the object array for reference:
export const FEEDBACKS = [
{feedback: 'Good Service', rating: 5},
{feedback: 'Friendly', rating: 2},
{feedback: 'Very Polite', rating: 2},
{feedback: 'Above & Beyond', rating: 1},
{feedback: 'Followed Instructions', rating: 1},
{feedback: 'Speedy Service', rating: 3},
{feedback: 'Clean', rating: 4},
{feedback: 'Accommodating', rating: 0},
{feedback: 'Enjoyable Experience', rating: 10},
{feedback: 'Great', rating: 8},
];
Edit: I solved it by replacing React-Native-Animated and using an Animated View and instead using Animatable and using an Animatable which has built in delay. Final solution:
const displayTags = () =>
tagInfo?.map((tag, index) => (
<TagContainer animation="zoomIn" duration={1000} delay={index * 1000}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
Here is a gif of the animation
This is an interesting problem. A clean way you could approach this problem is to develop a wrapper component, DelayedZoom that will render its child component with a delayed zoom. This component would take a delay prop that you can control to add a delay for when the component should begin animation.
function DelayedZoom({delay, speed, endScale, startScale, children}) {
const zoomAnim = useRef(new Animated.Value(startScale)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
delay: delay,
toValue: endScale,
duration: speed,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
return (
<Animated.View
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
{children}
</Animated.View>
);
}
After this, you can use this component as follows:
function OtherScreen() {
const tags = FEEDBACKS;
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
function renderTags() {
return tags.map((tag, idx) => {
const delay = idx * 10; // play around with this. Main thing is that you get a sense for when something should start to animate based on its index, idx.
return (
<DelayedZoom
delay={delay}
endScale={FINAL_ZOOM_SCALE}
startScale={START_ZOOM_SCALE}
speed={FAST_ZOOM}>
{/** whatever you want to render with a delayed zoom would go here. In your case it may be TagContainer */}
<TagContainer>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</DelayedZoom>
);
});
}
return <View>{renderTags()}</View>;
}
I hope this helps to point you in the right direction!
Also some helpful resources:
Animation delays: https://animationbook.codedaily.io/animated-delay/
Demo
It is a bit of work to implement this, I didn't have your components to try it out so I have created a basic implementation, I hope this will help
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View, Animated } from "react-native";
const OBJ = [{ id: 1 }, { id: 2 }, { id: 3 }];
const Item = ({ data, addValue }) => {
const zoomAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
addValue();
});
};
zoomIn();
}, [zoomAnim]);
return (
<View>
<Animated.View
ref={zoomAnim}
style={[
{
transform: [{ scale: zoomAnim }]
}
]}
>
<Text style={styles.text}>{data}</Text>
</Animated.View>
</View>
);
};
function App() {
const [state, setState] = useState([OBJ[0]]);
const addValue = () => {
const currentId = state[state.length - 1].id;
if (OBJ[currentId]) {
const temp = [...state];
temp.push(OBJ[currentId]);
setState(temp);
}
};
return (
<View style={styles.app}>
{state.map((item) => {
return <Item data={item.id} key={item.id} addValue={addValue} />;
})}
</View>
);
}
const styles = StyleSheet.create({
text: {
fontSize: 20
}
});
export default App;
Basically, I am adding an element to the state at the end of the previous animation, one thing to note is that the key is very important, and don't use the index as a key. instead of Ids you might want to add any other value that is sorted or maybe link an item by passing the id of the previous item.
ADDING A SOLUTION USING REANIMATED AND MOTI
There is this library which you can use moti(https://moti.fyi/) it will work with reanimated, so you need to add reanimated too. Before using Reanimated you must consider that your normal chrome dev tools for that particular application will stop working with reanimated 2.0 and above you can use flipper though.
coming to the solution.
import { View as MotiView } from 'moti';
...
const displayTags = () =>
tagInfo?.map((tag, index) => (
<MotiView
key = {tag.id}
from={{ translateY: 20, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
transition={{ type: 'timing' }}
duration={500}
delay={index * 150}>
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</MotiView>
));
...
That's it, make sure to use a proper key, don't use index as key.
Side Note: If you are doubtful that sould you use reanimated or not, just go through https://docs.swmansion.com/react-native-reanimated/docs/ this page. Using Moti you can have really cool animation easily also if you reanimated version 2.3.0-alpha.1 then you need not to use Moti but as it is alpha version so it is not advisable to use in production you can wait for its stable release too.

How to do a React Native count down with animated images?

I am trying to create a react native animation with some images as a count down like this:
3 --> ease out down duration 1 second each case
2 --> ease out down
1 --> ease out down
I found a way to do this with react animatable, but the results don't convince me, and if there is a better way to do it please let me know.
I think everytime I run into a new render of the element start counting down I say to react animatable to make an iteration of 3 ease out down for each number changing the image number, this is not a natural way of solving the problem I have.
For the moment is not using redux, may be later I will add it.
The properties of the state: time, current time to begin are not being used yet.
All that I need is the effect that a count down is happening with the images I show, in a well defined animation.
I think is almost getting there, but surely there is a better way to face to this problem, any suggestion is well received, even though it is as a guide. I also tried with react-native-animate-number but no luck...
Thanks in advance
import React from 'react';
import util from '../utils.js';
import * as Animatable from 'react-native-animatable';
import {
View,
Dimensions,
Image,
StyleSheet
} from 'react-native';
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const styles = StyleSheet.create({
mainOption: {
},
container: {
width: width,
height: height,
flex: 1,
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center'
}
});
const timeLapse = [
<Image source={require('../images/1.png')} key={util.uniqueId()}/>,
<Image source={require('../images/2.png')} key={util.uniqueId()}/>,
<Image source={require('../images/3.png')} key={util.uniqueId()}/>
];
export default class StartingGameCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
time: props.time,
currentTimeToBegin: props.currentTimeToBegin,
imagePosition: 0
};
}
componentWillReceiveProps(nextProps) {
const nextImage = this.state.imagePosition + 1;
this.setState({
...this.state,
letters: nextProps.time,
currentTimeToBegin: nextProps.currentTimeToBegin,
imagePosition: nextImage
});
}
render() {
return (
<View style={styles.container}>
<Animatable.View
ref='countDown'
duration={1000}
delay={0}
iterationCount={3}
animation="fadeOutDown"
style={styles.mainOption}>
{timeLapse[this.state.imagePosition]}
</Animatable.View>
</View>
);
}
}
Ok after taking a look into the documentation I found out that may be using sequence and timing could be an option:
import React from 'react';
import {
Animated,
Text,
View,
Dimensions,
Image,
Easing,
StyleSheet
} from 'react-native';
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
let counter = 0;
const util = {
uniqueId: () => {
return counter++;
}
};
const styles = StyleSheet.create({
mainOption: {
},
container: {
width: width,
height: height,
top: -100,
flex: 1,
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center'
}
});
const timeLapse = [
require('../images/1.png'),
require('../images/2.png'),
require('../images/3.png')
];
export default class StartingGameCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
time: props.time,
countDown: new Animated.Value(0),
currentTimeToBegin: props.currentTimeToBegin,
quantity: 3,
sizes: [],
duration: props.duration,
animatedValues: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]
};
}
animations() {
let result = [];
state = this.state;
for(let i = 0; i < state.quantity; i++) {
//Start
result.push(Animated.timing(
state.countDown,
{
toValue: 0,
duration: 0
}
)
);
let parallel = [];
//Animate
for(let j = 0; j < state.quantity; j++) {
parallel.push(
Animated.timing(
state.animatedValues[j],
{
easing: Easing.bezier(.07,.42,.85,.5),
toValue: i === j ? 1 : 0,
duration: i === j ? state.duration : 0
}
)
);
}
//Stop
parallel.push(Animated.timing(
state.countDown,
{
easing: Easing.bezier(.07,.42,.85,.5),
toValue: 1,
duration: state.duration
}
)
);
result = [...result, Animated.parallel(parallel)];
}
return result;
}
componentDidMount() {
Animated.sequence(this.animations()).start();
}
shouldComponentUpdate(nextProps, nextState) {
//I don't to run lifecycle in any case by mistake here
return false;
}
createImagesAnimated() {
let transitionY = this.state.countDown.interpolate({
inputRange: [0, 1],
outputRange: [0, this.props.transition.y]
});
let result = [];
//What to be Animated
for(let i = 0; i < this.state.quantity; i++) {
let image = this.state.animatedValues[i].interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 1, 0]
});
result.push(
<Animated.Image
key={util.uniqueId()}
source={timeLapse[i]}
style={{
//How to animate it
position: 'absolute',
opacity: image,
transform: [{
translateY: transitionY
}]
}}
/>
);
}
return result;
}
render() {
return (
<View style={styles.container}>
{this.createImagesAnimated()}
</View>
);
}
}
This would be the implementation of the component:
<CountDownComponent
transition={{y:200}}
duration={5000}
/>
This solves my problem I hope to find in another moment a better solution in the future.
Here is the repo:
https://github.com/jotaoncode/CountDown

Categories

Resources