React native Animated API not working with recompose - reactjs

I have a HOC made in recompose that isn't behaving properly - it just shows the component at it's end value without ever animating. The same component, written as a regular class, behaves fine. Can someone tell me what is causing this issue, or how I would approach this so it works correctly?
recompose HOC component:
const enhancer = compose(
withState('slideAnim', 'setSlide', new Animated.Value(width)),
withState('fadeAnim', 'setFadeAnim', new Animated.Value(0)),
lifecycle({
componentDidMount () {
Animated.parallel([
Animated.timing(this.props.slideAnim, {
toValue: 0,
duration: 500
}),
Animated.timing(this.props.fadeAnim, {
toValue: 1,
duration: 500
})
]).start()
}
})
)
const ModalScene = ({ children, slideAnim, fadeAnim }) => {
return (
<Animated.View style={[styles, { opacity: fadeAnim, left: slideAnim }]}>
{children}
</Animated.View>
)
}
regular class:
class ModalScene extends React.Component {
constructor (props) {
super(props)
this.state = {
slideAnim: new Animated.Value(width),
fadeAnim: new Animated.Value(0)
}
}
componentDidMount () {
Animated.parallel([
Animated.timing(this.state.slideAnim, {
toValue: 0,
duration: 500
}),
Animated.timing(this.state.fadeAnim, {
toValue: 1,
duration: 500
})
]).start()
}
render () {
return (
<Animated.View
style={[
styles,
{ opacity: this.state.fadeAnim, left: this.state.slideAnim }
]}
>
{this.props.children}
</Animated.View>
)
}
}

Your code is correct, you just forget to export HOC, try this:
export default enhancer(ModalScene);

I had a similar problem which seemed to be caused by the mutable nature of the Animated objects. To make sure an instance of Animated.Value was created for each instance of my components I had to use the other form of withState, the one that takes a function as initial value. Try changing these lines:
withState('slideAnim', 'setSlide', new Animated.Value(width)),
withState('fadeAnim', 'setFadeAnim', new Animated.Value(0)),
to
withState('slideAnim', 'setSlide', () => new Animated.Value(width)),
withState('fadeAnim', 'setFadeAnim', () => new Animated.Value(0)),

Related

Intersection Observer API with react

I am trying to create a navigation tracker on a page where a user scrolls with the intersection observer API and a tracker shows what section on the page in react like something shown in the website with this link https://olaolu.dev/ with the boxes that changes on scroll (scroll indicators), I already created a code sandbox. anyone who can assist me would mean a lot. been getting errors here and there don't even know what to do again, Link to code sandbox below
https://codesandbox.io/s/tender-oskar-rvbz5?file=/src/App.js
I only used react-intersection-observer with framer motion before, I added a ref to my h1 so the observer knows if it's inView or not.
controls.start will trigger if inView is true.
import { useInView } from "react-intersection-observer";
const scrollVariant = {
hidden: {
opacity: 0,
x: 0,
transition: {
duration: 1,
},
},
visible: {
opacity: 1,
x: 250,
transition: {
duration: 1,
},
},
};
export default function Home() {
const controls = useAnimation();
const { ref, inView } = useInView({
triggerOnce: false,
});
React.useEffect(() => {
if (inView) {
controls.start("visible");
}
if (!inView) {
controls.start("hidden");
}
}, [controls, inView]);
return (
<>
<motion.h1
variants={scrollVariant}
initial="hidden"
animate={controls}
ref={ref}
>
</>
);
}

Framer Motion dynamic variants don't work when modifying initial properties

According to the docs I can make variant properties dynamic: https://www.framer.com/docs/animation/##dynamic-variants.
But this doesn't work when I try to make the initial properties dynamic.
For example:
import React, { useState, useEffect } from "react";
import { motion, useAnimation } from "framer-motion";
//make div appear from either bottom or right, depending on "origin" custom prop
const variant = {
hidden: (origin) =>
origin === "bottom"
? { x: 0, y: 200, opacity: 0 }
: { x: 200, y: 0, opacity: 0 },
visible: { x: 0, y: 0, opacity: 1, transition: { duration: 1 } },
};
function App() {
const [origin, setOrigin] = useState("bottom");
const controls = useAnimation();
//after 2 secs make origin "right"
useEffect(() => {
setTimeout(() => {
setOrigin("right");
}, 2000);
}, []);
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
controls.start("visible");
}, 4000);
}, [controls]);
return (
<motion.div
style={{ width: 100, height: 50, background: "red" }}
variants={variant}
initial="hidden"
animate={controls}
custom={origin}
/>
);
}
export default App;
Here I made a dynamic variant to make a div appear from either the right or bottom, which I can control from a custom prop. Initially this custom prop is set to "bottom". After 2 secs, this is changed to "right". When I start the animation after 4 secs, I expect the div to appear from the right but it still appears from the bottom:
This is because the component is already rendered and is still the same component even if the origin prop being passed to the component has changed.
You can do two things:
Use a isVisible state variable where the render method will observe for changes and render the component when it becomes true.
function App() {
const [isVisible, setIsVisible] = useState(false);
...
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
controls.start("visible");
}, 4000);
}, [controls]);
return (
isVisible && (
<motion.div
...
/>
)
);
}
DEMO
Add a key prop to the component with the origin value so that when the value changes, React will re-render the component.
function App() {
...
return (
<motion.div
key={origin}
...
/>
);
}
DEMO
2nd option may be your preferred choice if you need to toggle between the origin.

Animation from sub-component is not displayed

Problem :
Animation is not played from the parent component.
Context :
I have two components.
The first one (Parent component):
export default function LevelSecondItem (props) {
const { Data, moveTo, Identifiant, Score, GAME } = props;
return (
<View style={styles.main_container}>
<ProgressBarLight
percentage={40}
total={3}
items={1}
label={Data.description}
/>
<View style={styles.second_container}>
<ButtonCustom text={"Commencer"} onPress={() => moveTo(Data.puzzle,Identifiant+"-"+Data.id,GAME) }/>
</View>
</View>
)
}
The second component (sub-component - child) :
const ProgressBarLight = (props) => {
const fadeAnim = useRef(new Animated.Value(0)).current // Initial value for opacity: 0
useEffect(() => {
Animated.timing(
fadeAnim,
{
toValue: 100,
duration: 10000,
useNativeDriver:false
}
).start();
}, [fadeAnim])
return (<Animated.View><Animated.Text>{fadeAnim}</Animated.Text></Animated.View>)
}
Comment :
When saving the file, react is updating the app. In this way, I'm able to see the animated value. The value is changing. But the main component is not showing the animation. I don't know why.
You need to reset the animated value to zero by using animation.setValue(0) . I was not able to find this function...
I try to use setAnimation(0) and this is thowing an error.
import React, { useState, useEffect } from 'react'
import { Text, View, StyleSheet, Animated } from 'react-native'
import {GREY80_VALUE,GREEN_VALUE} from '../GlobalConstant'
export default function CardIndicator(props) {
const { currentTotalIndex, currentTotalObject } = props;
const [animation, setAnimation] = useState(new Animated.Value(0));
const anim = ()=>{
Animated.timing(
animation,
{
toValue: 1,
duration: 1000,
useNativeDriver: false,
}
).start(()=>{
Animated.timing(
animation,
{
toValue: 0,
duration: 1000,
useNativeDriver: false,
}
)
});
}
useEffect(()=>{
animation.setValue(0)
anim()
},[currentTotalIndex])
const boxInterpolation = animation.interpolate({
inputRange: [0, 1],
outputRange:[GREEN_VALUE , GREY80_VALUE]
})
const animatedStyle = {
backgroundColor: boxInterpolation
}
return (<Animated.View style={{padding:10,borderRadius:5,...animatedStyle}}><Text style={{fontFamily:"RobotoMono-Bold",fontSize: 16}}>{currentTotalIndex+" / "+currentTotalObject}</Text></Animated.View>)
}
you can use react native animatable library it's very easy to use
just like this
import * as Animatable from 'react-native-animatable';
const FadeInView = (props) => {
return (<View><Animatable.Text animation="fadeIn">{"your text here"}</Animatable.Text></View>)
}
you can find more props of this library and use it

Animate back and forth rotate on logo click with react native

I'm trying to animate a menu logo to rotate when clicked. I'm successfully getting the rotation when it rotates up, but it just goes directly to 0 on rotate down instead of going through the rotate animation.
This is my component:
import React from 'react';
import { TouchableOpacity, Animated } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
const TabIcon = ({
route,
renderIcon,
onPress,
focused,
menuToggled,
activeTintColor,
inactiveTintColor,
}) => {
const isMenuLogo = route.params && route.params.navigationDisabled;
const animation = new Animated.Value(0);
Animated.timing(animation, {
toValue: menuToggled ? 1 : 0,
duration: 200,
useNativeDriver: true,
}).start();
const rotateInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg'],
});
const animatedStyles = { transform: [{ rotate: rotateInterpolate }] };
const logoStyles = [animatedStyles, styles.logoStyle];
return (
<TouchableOpacity
style={styles.tabStyle}
onPress={onPress}
activeOpacity={isMenuLogo && 1}
>
<Animated.View style={isMenuLogo ? logoStyles : null}>
{
renderIcon({
route,
focused,
tintColor: focused
? activeTintColor
: inactiveTintColor,
})
}
</Animated.View>
</TouchableOpacity>
);
};
TabIcon.propTypes = {
route: PropTypes.shape({
key: PropTypes.string,
}).isRequired,
renderIcon: PropTypes.func.isRequired,
onPress: PropTypes.func,
focused: PropTypes.bool,
menuToggled: PropTypes.bool,
activeTintColor: PropTypes.string.isRequired,
inactiveTintColor: PropTypes.string.isRequired,
};
TabIcon.defaultProps = {
onPress: () => {},
focused: false,
menuToggled: false,
};
export default TabIcon;
I'm checking first if it has been toggled before actually rotating it. This component is being called in another parent component displaying a custom bottom tab navigation.
Should I be doing a different animation for it when it rotates down or am I missing a configuration in my current animation?
Any help and suggestion would really be much appreciated. Thank you.
I think the issue is related to the fact that when you set the initial value of animation because it is always set to 0 it doesn't reflect the change when you switch the menu.
You need to change:
const animation = new Animated.Value(0);
to
const animation = new Animated.Value(menuToggled ? 0 : 1);
Though making that change will cause a different problem. Because the menuToggled affects the start and end positions of the animation the Icon will now rotate into the correct starting position from the end position. This is not ideal.
However we can fix that by setting a default value of null for menuToggled. Then wrapping the animation in an if-statement that only runs if the menuToggled is not null.
Here is an example based off of your initial code:
import React from 'react';
import { View, StyleSheet, Animated, TouchableOpacity } from 'react-native';
import { Ionicons } from '#expo/vector-icons';
const TabIcon = ({
onPress,
menuToggled
}) => {
const logoStyles = [styles.logoStyle];
if (menuToggled !== null) {
const animation = new Animated.Value(menuToggled ? 0 : 1);
Animated.timing(animation, {
toValue: menuToggled ? 1 : 0,
duration: 200,
useNativeDriver: true
}).start();
const rotateInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg']
});
const animatedStyles = { transform: [{ rotate: rotateInterpolate }] };
logoStyles.push(animatedStyles);
}
return (
<TouchableOpacity
style={styles.tabStyle}
onPress={onPress}
>
<Animated.View style={logoStyles}>
<Ionicons name="md-checkmark-circle" size={32} color="green" />
</Animated.View>
</TouchableOpacity>
);
};
export default class App extends React.Component {
state = {
menuToggled: null
}
toggleMenu = () => {
this.setState(prevState => {
return { menuToggled: !prevState.menuToggled };
});
}
render () {
return (
<View style={styles.container}>
<TabIcon
onPress={this.toggleMenu}
menuToggled={this.state.menuToggled}
/>
</View>
);
}
}
I stripped down your TabIcon component as there was a lot of things there that were not related to the animation. You should be easily able to incorporate what I have done into your own component. https://snack.expo.io/#andypandy/rotating-icon
I've tried Andrew's solution above and it works, but I have opted for turning it into a class component. It works the same way. See the component below.
import React, { PureComponent } from 'react';
import { TouchableOpacity, Animated } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles';
class TabIcon extends PureComponent {
constructor(props) {
super(props);
this.state = {
animation: new Animated.Value(0),
};
}
render() {
const { animation } = this.state;
const {
route,
renderIcon,
onPress,
focused,
menuToggled,
activeTintColor,
inactiveTintColor,
} = this.props;
const isMenuLogo = route.params && route.params.navigationDisabled;
Animated.timing(animation, {
toValue: menuToggled ? 1 : 0,
duration: 200,
useNativeDriver: true,
}).start();
const rotateInterpolate = animation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg'],
});
const animatedStyles = { transform: [{ rotate: rotateInterpolate }] };
const logoStyles = [animatedStyles, styles.logoStyle];
return (
<TouchableOpacity
style={styles.tabStyle}
onPress={onPress}
activeOpacity={isMenuLogo && 1}
>
<Animated.View style={isMenuLogo ? logoStyles : null}>
{
renderIcon({
route,
focused,
tintColor: focused
? activeTintColor
: inactiveTintColor,
})
}
</Animated.View>
</TouchableOpacity>
);
}
}
TabIcon.propTypes = {
route: PropTypes.shape({
key: PropTypes.string,
}).isRequired,
renderIcon: PropTypes.func.isRequired,
onPress: PropTypes.func,
focused: PropTypes.bool,
menuToggled: PropTypes.bool,
activeTintColor: PropTypes.string.isRequired,
inactiveTintColor: PropTypes.string.isRequired,
};
TabIcon.defaultProps = {
onPress: () => {},
focused: false,
menuToggled: false,
};
export default TabIcon;

How do I access the variable in componentWillMount()

I am trying to set a global style variable from the componentWillMount() but I am not able to do that. When I am logging the variable it says undefined.
export default class Post extends React.Component {
constructor(props) {
super(props)
this.state = {
postImageUri: {
uri: this.props.postImageUri
}
}
}
componentWillMount() {
Image.getSize(this.props.postImageUri, (width, height) => {
...
styles.postImage.height = height
styles.postImage.width = Dimensions.get('window').width
console.log(styles.postImage.height) //Undefined
console.log(styles.postImage.width) //Undefined
...
});
}
render() {
return (
<View>
<Image source={this.state.postImageUri} style={styles.postImage}/>
</View>
}
}
val styles = StyleSheet.create({
postImage: {
height: 0,
width: 0
}
})
Instead of mutating the style, you should store the height in state:
state = {
height: null,
};
componentDidMount() {
Image.getSize(this.props.postImageUri, (width, height) => {
this.setState({ height });
});
}
Then use style composition to apply the height from the state:
<Image
source={this.state.postImageUri}
style={[styles.postImage, { height: this.state.height } ]}
/>
Your width computation is synchronous so you can set it directly in style:
const styles = StyleSheet.create({
postImage: {
width: Dimensions.get('window').width,
},
});
Store the style in state instead of storing in a global variable. This way the component will itself figure out whenever the state is changed and component will re-render.
You can change the state using this.setState({stateName: someValue});.

Resources