I only encountered this issue once I incorporated the useEffect() hook as suggested by React native - "this.setState is not a function" trying to animate background color?
With the following, I get
Rendered more hooks than during the previous render
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
if (!fontsLoaded) {
return <AppLoading />;
} else {
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(this.state.backgroundColor, {
toValue: 100,
duration: 5000
}).start();
}, [backgroundColor]);
var color = this.state.colorValue.interpolate({
inputRange: [0, 300],
outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
});
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color
},
textWrapper: {
height: hp('70%'), // 70% of height device screen
width: wp('80%'), // 80% of width device screen
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
myText: {
fontSize: hp('2%'), // End result looks like the provided UI mockup
fontFamily: 'SequelSans-BoldDisp'
}
});
return (
<Animated.View style={styles.container}>
<View style={styles.textWrapper}>
<Text style={styles.myText}>Login</Text>
</View>
</Animated.View>
);
}
};
Im just trying to animate fade the background color of a view. I tried deleting the first useEffect in case it was causing some redundancy, but that did nothing. Im new to ReactNative - what is wrong here?
EDIT:
export default props => {
let [fontsLoaded] = useFonts({
'Inter-SemiBoldItalic': 'https://rsms.me/inter/font-files/Inter-SemiBoldItalic.otf?v=3.12',
'SequelSans-RomanDisp' : require('./assets/fonts/SequelSans-RomanDisp.ttf'),
'SequelSans-BoldDisp' : require('./assets/fonts/SequelSans-BoldDisp.ttf'),
'SequelSans-BlackDisp' : require('./assets/fonts/SequelSans-BlackDisp.ttf'),
});
//Set states
const [backgroundColor, setBackgroundColor] = useState(new Animated.Value(0));
useEffect(() => {
setBackgroundColor(new Animated.Value(0));
}, []); // this will be only called on initial mounting of component,
// so you can change this as your requirement maybe move this in a function which will be called,
// you can't directly call setState/useState in render otherwise it will go in a infinite loop.
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
}, [backgroundColor]);
// var color = this.state.colorValue.interpolate({
// inputRange: [0, 300],
// outputRange: ['rgba(255, 0, 0, 1)', 'rgba(0, 255, 0, 1)']
// });
//------------------------------------------------------------------->
if (!fontsLoaded) {
return <AppLoading />;
} else {
const styles = StyleSheet.create({
container: { flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: backgroundColor
New errors:
invalid prop 'color' supplied to 'Stylesheet'
Animated useNativeDriver was not specified
On your first render (I'm guessing) only the useFonts hook will be called as you return <AppLoading /> since !fontsLoaded. The rest of your hooks are in the else block, meaning you won't have the same number of hooks on every render.
Check out https://reactjs.org/docs/hooks-rules.html for more explanation, especially https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
The useNativeDriver error exists because you didn't specify it:
Your code:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000
}).start();
Fix:
useEffect(() => {
Animated.timing(useState(backgroundColor), {
toValue: 100,
duration: 7000,
useNativeDrive: true
}).start();
Hope this helps!
Related
I have a progress bar screen inside navigation that I need to be reset every time user clicks on that specific route. Now it only plays animation when I go to that route for the first time. My question is: how to reset barWidth so that animation would play every time user clicks on specific route?
What have I tried?
I thought that the problem is that the component is not re-rendering and thats why value is not resetting, but it looks like the problem is that the width of the bar doesn't reset when user clicks on the screen after animation plays.
At first I've tried using useRef hook and later changed to simply setting the animated value to 0, but in both cases the bar's width didn't reset.
Code:
const { width } = Dimensions.get("screen");
const SettingsScreen = ({navigation}) => {
const isFocused = useIsFocused();
return (
<FlatList
contentContainerStyle={style.barContainer}
data={[1, 2, 3, 4, 5]}
keyExtractor={(_, index) => index.toString()}
renderItem={() => (
<ProgressBar isFocused={isFocused} navigation={navigation} />
)}
></FlatList>
);
};
const ProgressBar = ({ navigation, isFocused }) => {
//const barWidth = React.useRef(new Animated.Value(0)).current;
const barWidth = new Animated.Value(0);
console.log(barWidth);
const finalWidth = width / 2;
React.useEffect(() => {
const listener = navigation.addListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
return listener;
}, [navigation]);
return (
<View style={style.contentContainer}>
<Animated.View style={[style.progressBar, { width: barWidth }]} />
</View>
);
};
const style = StyleSheet.create({
contentContainer: {
flex: 1,
justifyContent: "center",
padding: 30,
},
barContainer: {
padding: 30,
},
progressBar: {
backgroundColor: "green",
width: width / 2,
height: 15,
borderRadius: 15,
},
});
The addListener function is Deprecated. try to use addEventListener instead.
also, why is it inside a const listener with the return listener?
as i see it you can write the useEffect like that:
React.useEffect(() => {
navigation.addEventListener("focus", () => {
Animated.spring(barWidth, {
toValue: finalWidth,
bounciness: 10,
speed: 2,
useNativeDriver: false,
}).start();
});
}, [navigation]);
I am using React Native, and one of the components we need is a button with a gradient background. OnPress the colors should smoothly animate from their base value to their active value, and when you finish they should smoothly go back to their base value. I'm using a TouchableHighlight component to get access to the onShowUnderlay and onHideUnderlay functions to trigger the gradient change.
I was successfully able to get it to change abruptly on state change, but I'm having a harder time getting it to smoothly animate. When I used the following code, the emulator gives me this error: JSON value '<null>' of type NSNull cannot be converted to a UI Color. Did you forget to call processColor() on the JS side?, which I think is related to LinearGradient not being able to read the interpolated values as an RGBA value.
Am I not using the Animated API or the LinearGradient correctly? Or is it just not possible to do it this way?
import React, { PureComponent } from 'react';
import { Animated, View, TouchableHighlight, Text } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import styles from './styles/FooterButton.styles';
const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);
export default class FooterGradientButton extends PureComponent {
midColor = new Animated.Value(0);
lastColor = new Animated.Value(0);
showUnderlay = () => {
this.midColor.setValue(0);
this.lastColor.setValue(0);
Animated.parallel([
Animated.timing(this.midColor, {
duration: 500,
toValue: 1,
}),
Animated.timing(this.lastColor, {
duration: 500,
toValue: 1,
}),
]).start();
};
hideUnderlay = () => {
this.midColor.setValue(1);
this.lastColor.setValue(1);
Animated.parallel([
Animated.timing(this.midColor, {
duration: 500,
toValue: 0,
}),
Animated.timing(this.lastColor, {
duration: 500,
toValue: 0,
}),
]).start();
};
render() {
const firstColor = 'rgba(52, 85, 219, 1)';
const midColor = this.midColor.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(19, 144, 255, 1)', 'rgba(52,85,219, 1)'],
});
const lastColor = this.lastColor.interpolate({
inputRange: [0, 1],
outputRange: ['rgba(2,194,211, 1)', 'rgba(30,144,255, 1)'],
});
return (
<View style={[styles.margin, styles.shadow]}>
<AnimatedGradient start={{ x: 0.0, y: 0.5 }} end={{ x: 1, y: 0.5 }} style={{ flex: 1 }} colors={[firstColor, midColor, lastColor]}>
<TouchableHighlight
activeOpacity={1}
underlayColor="#ffffff00"
onShowUnderlay={() => this.showUnderlay()}
onHideUnderlay={() => this.hideUnderlay()}
style={[styles.gradientButton, styles.androidButton]}
onPress={() => (!this.props.inactive ? this.props.onPress() : null)}
>
<Text style={[styles.buttonText, { color: 'white' }]}>NEXT</Text>
</TouchableHighlight>
</AnimatedGradient>
</View>
);
}
}
Take a look at react native placeholder. You should be able to pass a linear gradient and animate it.
Let's say I have a view that is positioned absolute at the bottom of the screen. This view contains a text input. When the text input is focused, I want the bottom of the view to touch the top of the keyboard.
I've been messing around with KeyboardAvoidingView, but the keyboard keeps going over my view. Is it not possible to make this work with position absolute?
What other method can I try? Thanks!
Few days ago I have the same problem (although I have a complex view with TextInput as a child) and wanted not only the TextInput to be focused but the whole view to be "attached" to the keyboard. What's finally is working for me is the following code:
constructor(props) {
super(props);
this.paddingInput = new Animated.Value(0);
}
componentWillMount() {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 60,
}).start();
};
keyboardWillHide = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: 0,
}).start();
};
render() {
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
[...]
<Animated.View style={{ marginBottom: this.paddingInput }}>
<TextTranslateInput />
</Animated.View>
</KeyboardAvoidingView>
);
}
where [..] you have other views.
Custom hook:
import { useRef, useEffect } from 'react';
import { Animated, Keyboard, KeyboardEvent } from 'react-native';
export const useKeyboardHeight = () => {
const keyboardHeight = useRef(new Animated.Value(0)).current;
useEffect(() => {
const keyboardWillShow = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: e.endCoordinates.height,
useNativeDriver: true,
}).start();
};
const keyboardWillHide = (e: KeyboardEvent) => {
Animated.timing(keyboardHeight, {
duration: e.duration,
toValue: 0,
useNativeDriver: true,
}).start();
};
const keyboardWillShowSub = Keyboard.addListener(
'keyboardWillShow',
keyboardWillShow
);
const keyboardWillHideSub = Keyboard.addListener(
'keyboardWillHide',
keyboardWillHide
);
return () => {
keyboardWillHideSub.remove();
keyboardWillShowSub.remove();
};
}, [keyboardHeight]);
return keyboardHeight;
};
#jazzdle example works great! Thank you for that!
Just one addition - in keyboardWillShow method, one can add event.endCoordinates.height so paddingBottom is exact height as keyboard.
keyboardWillShow = (event) => {
Animated.timing(this.paddingInput, {
duration: event.duration,
toValue: event.endCoordinates.height,
}).start();
}
Using Functional Component. This works for both iOS and Android
useEffect(() => {
const keyboardVisibleListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
handleKeyboardVisible
);
const keyboardHiddenListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
handleKeyboardHidden
);
return () => {
keyboardHiddenListener.remove();
keyboardVisibleListener.remove();
};}, []);
const handleKeyboardVisible = (event) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 60,
useNativeDriver: false,
});};
const handleKeyboardHidden = (event: any) => {
Animated.timing(paddingInput, {
duration: event.duration,
toValue: 0,
useNativeDriver: false,
});};
React Native now supports an InputAccessoryView which can be used for exactly this purpose - even for anchored TextInputs.
Here's a specific example: https://github.com/facebook/react-native/blob/main/packages/rn-tester/js/examples/InputAccessoryView/InputAccessoryViewExample.js
You can use flexbox to bottom position the element. Here's simple example -
render() {
return (
<View style={styles.container}>
<View style={styles.top}/>
<View style={styles.bottom}>
<View style={styles.input}>
<TextInput/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
top: {
flex: .8,
},
bottom: {
flex: .2,
},
input: {
width: 200,
},
});
Every time I press a button the progress bar will go up %20 but the problem is for instance if its at %80 and I reload the app it will show it as %60 instead, I was wondering if anyone could help me fix this issue to have the progress bars percentage stay the same after reloading or closing the app.
'use strict';
var React = require('react-native');
var ProgressBar = require('react-native-progress-bar');
var {
AppRegistry,
AsyncStorage,
StyleSheet,
Text,
View,
TouchableHighlight
} = React;
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFF',
},
button: {
alignSelf: 'center',
marginTop: 50,
width: 100,
height: 50,
backgroundColor: '#0059FF',
borderRadius: 8,
borderWidth: 2,
borderColor: '#0059FF'
},
buttonClear: {
alignSelf: 'center',
marginTop: 10,
width: 100,
height: 50,
backgroundColor: '#3B3A3A',
borderRadius: 8,
borderWidth: 2,
borderColor: '#3B3A3A'
},
buttonText: {
fontSize: 18,
textAlign: 'center',
lineHeight: 33,
color: '#FFF',
}
});
var PROGRESS = 0;
class BasicStorageExample extends React.Component {
constructor(props) {
super(props);
this.state = {
progress: PROGRESS
}
}
componentDidMount() {
AsyncStorage.getItem('progressbar')
.then((value) => {
JSON.parse(value);
this.setState({
progress: value
});
console.log('Progress on load: ' + value);
})
.done();
}
onButtonPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
this.setState({
progress: PROGRESS += 0.2
});
console.log('Progress on Button Press: ' + PROGRESS);
})
.done();
}
onButtonClearPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
PROGRESS = 0;
this.setState({
progress: 0
});
})
.done();
}
render() {
return (
<View style={styles.container}>
<ProgressBar
fillStyle={{}}
backgroundStyle={{backgroundColor: '#cccccc', borderRadius: 2}}
style={{marginTop: 10, width: 300}}
progress={this.state.progress} />
<TouchableHighlight
ref="button"
style={styles.button}
underlayColor='#002C7F'
onPress={this.onButtonPress.bind(this)}>
<Text style={styles.buttonText}>Done</Text>
</TouchableHighlight>
<TouchableHighlight
style={styles.buttonClear}
underlayColor='#002C7F'
onPress={this.onButtonClearPress.bind(this)}>
<Text style={styles.buttonText}>Clear</Text>
</TouchableHighlight>
</View>
);
}
};
AppRegistry.registerComponent('BasicStorageExample', () => BasicStorageExample);
The problem is that you are getting the old value of PROGRESS (before you increment by 0.2) and setting this value to the item progressbar.
So, every time you reload, React runs the componentDidMount function, and since you set progressbar to the old value of PROGRESS, it will always show it one increment behind what you see in the view.
Try changing your onButtonPress() to this:
onButtonPress() {
PROGRESS += 0.2;
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
... // continues normally
I am using react native to build an app and the only problem i am having is that i have a progress bar that keeps track of the users progress but when I close the app completely and open it back up everything resets to its original data.So I turned to AsyncStorage to hold my data in but I am having trouble trying to figure out how to use it in my code, if anyone could help that would be great.
*UPDATE:
I tried to implement asyncstorage and the data seems to be sticking when I fully close the app but I have it so every time I press a button the progress bar will go up %20 and for instance if its at %80 and I reload the app it will show it as %60 instead, I was wondering if anyone could help me fix this issue to have the progress bars percentage stay the same after reloading or closing the app.
'use strict';
var React = require('react-native');
var ProgressBar = require('react-native-progress-bar');
var {
AppRegistry,
AsyncStorage,
StyleSheet,
Text,
View,
TouchableHighlight
} = React;
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FFF',
},
button: {
alignSelf: 'center',
marginTop: 50,
width: 100,
height: 50,
backgroundColor: '#0059FF',
borderRadius: 8,
borderWidth: 2,
borderColor: '#0059FF'
},
buttonClear: {
alignSelf: 'center',
marginTop: 10,
width: 100,
height: 50,
backgroundColor: '#3B3A3A',
borderRadius: 8,
borderWidth: 2,
borderColor: '#3B3A3A'
},
buttonText: {
fontSize: 18,
textAlign: 'center',
lineHeight: 33,
color: '#FFF',
}
});
var PROGRESS = 0;
class BasicStorageExample extends React.Component {
constructor(props) {
super(props);
this.state = {
progress: PROGRESS
}
}
componentDidMount() {
AsyncStorage.getItem('progressbar')
.then((value) => {
JSON.parse(value);
this.setState({
progress: value
});
console.log('Progress on load: ' + value);
})
.done();
}
onButtonPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
this.setState({
progress: PROGRESS += 0.2
});
console.log('Progress on Button Press: ' + PROGRESS);
})
.done();
}
onButtonClearPress() {
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
.then(() => {
JSON.parse(PROGRESS);
PROGRESS = 0;
this.setState({
progress: 0
});
})
.done();
}
render() {
return (
<View style={styles.container}>
<ProgressBar
fillStyle={{}}
backgroundStyle={{backgroundColor: '#cccccc', borderRadius: 2}}
style={{marginTop: 10, width: 300}}
progress={this.state.progress} />
<TouchableHighlight
ref="button"
style={styles.button}
underlayColor='#002C7F'
onPress={this.onButtonPress.bind(this)}>
<Text style={styles.buttonText}>Done</Text>
</TouchableHighlight>
<TouchableHighlight
style={styles.buttonClear}
underlayColor='#002C7F'
onPress={this.onButtonClearPress.bind(this)}>
<Text style={styles.buttonText}>Clear</Text>
</TouchableHighlight>
</View>
);
}
};
AppRegistry.registerComponent('BasicStorageExample', () => BasicStorageExample);
Just call AsyncStorage.setItem('some-id', someVar) to set it then AsyncStorage.getItem('some-id') to retrieve. It's similar to localStorage. There's a full API and example in the documentation:
https://facebook.github.io/react-native/docs/asyncstorage.html
For store data
AsyncStorage.setItem('progressbar', JSON.stringify(PROGRESS))
For getting Data call asynchronously.
async componentWillMount() {
const result = await AsyncStorage.getItem('progressbar');
// you will get value. check your conditions here
}