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]);
Related
We simply want to make an image movable around the screen and then snap back to the middle, we have our code below and that's working fine currently styled with a blue box that you can move around, but we're unable to figure out how we can amend the blue box into being our image that we have required in.
import React, { useRef } from "react";
import { Animated, PanResponder, StyleSheet, View, Image } from "react-native";
const Users = [{id:"1", uri: require('./assets/present.jpeg'), keyword: 'present1'}]
const DraggableView = () => {
const position = useRef(new Animated.ValueXY()).current;
console.log(pan)
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
pan, // Auto-multiplexed
{ toValue: { x: 0, y: 0 } } // Back to zero
).start();
},
});
return (
<View style={styles.container}>
<Animated.View
{...panResponder.panHandlers}
style={[pan.getLayout(), <Image source={require('./assets/present.jpeg')}/>}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
box: {
backgroundColor: "#61dafb",
width: 80,
height: 80,
borderRadius: 4,
},
});
export default DraggableView;
The <Image /> component should be a child of the <Animated.View /> component. Currently, you have it defined in the style prop which doesn't work.
const DraggableView = () => {
const position = useRef(new Animated.ValueXY()).current;
console.log(pan)
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{
dx: pan.x, // x,y are Animated.Value
dy: pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
pan, // Auto-multiplexed
{ toValue: { x: 0, y: 0 } } // Back to zero
).start();
},
});
return (
<View style={styles.container}>
<Animated.View
{...panResponder.panHandlers}
style={pan.getLayout()}
>
<Image source={require('./assets/present.jpeg')}/>
</Animated.View>
</View>
);
};
I have this simple react native app that when click on a button add a message to list and display the message for 2 second then the message get deleted from the list
import React, {
useEffect,
useRef,
useState,
} from 'react';
import {
Animated,
Button,
Text,
View,
} from 'react-native';
const getRandomMessage = () => {
const number = Math.trunc(Math.random() * 10000);
return 'Random message ' + number;
};
const Message = (props) => {
const opacity = useRef(new Animated.Value(0))
.current;
useEffect(() => {
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.delay(2000),
Animated.timing(opacity, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
]).start(() => {
props.onHide();
});
}, []);
return (
<Animated.View
style={{
opacity,
transform: [
{
translateY: opacity.interpolate({
inputRange: [0, 1],
outputRange: [-20, 0],
}),
},
],
margin: 10,
marginBottom: 5,
backgroundColor: 'white',
padding: 10,
borderRadius: 4,
shadowColor: 'black',
shadowOffset: {
width: 0,
height: 3,
},
shadowOpacity: 0.15,
shadowRadius: 5,
elevation: 6,
}}
>
<Text>{props.message}</Text>
</Animated.View>
);
};
export default () => {
const [messages, setMessages] = useState([]);
return (
<>
<View
style={{
position: 'absolute',
top: 45,
left: 0,
right: 0,
}}
>
{messages.map((message) => (
<Message
key={message}
message={message}
onHide={() => {
setMessages((messages) =>
messages.filter(
(currentMessage) =>
currentMessage !== message
)
);
}}
/>
))}
</View>
<Button
title="Add message"
onPress={() => {
const message = getRandomMessage();
setMessages([...messages, message]);
}}
/>
</>
);
};
if i change the code that delete the message from the list
from
setMessages((messages) =>
messages.filter(
(currentMessage) =>
currentMessage !== message
)
to this
const temp = messages.filter(currentMessage => currentMessage !== message);
setMessages(temp);
the app behave differently and the whole screen re-render
is there any explanation for this because it took me hours to figure out what is the problem with my code but i do not understand why it did work when I changed the state update function to use the previous state
I'm trying to create an auto scroll flatlist carousel that can also allow the user to scroll manually. The problem is that I get this error flatListRef.scrollToIndex is not a function. I tried searching how other people create an auto scroll flatlist but their solution is using class.
const flatListRef = useRef(null)
useEffect (() => {
const totalIndex = data.length - 1;
setInterval (() => {
if(flatListRef.current.index < totalIndex) {
flatListRef.scrollToIndex({animated: true, index: flatListRef.current.index + 1})
} else {
flatListRef.scrollToIndex({animated: true, index: 0})
}
}, 3000)
}, []);
const renderItem = ({item}) => {
return (
<View style={styles.cardView}>
<Image style={styles.image} source={item.image} resizeMode="contain"/>
</View>
)
}
return (
<View style={{paddingHorizontal: 10}} >
<FlatList
ref={flatListRef}
data={data}
keyExtractor={data => data.id}
horizontal
pagingEnabled
scrollEnabled
snapToAlignment="center"
scrollEventThrottle={16}
decelerationRate={"fast"}
showsHorizontalScrollIndicator={true}
persistentScrollbar={true}
renderItem={renderItem}
/>
</View>
);
styles:
cardView: {
flex: 1,
width: width - 20,
height: height * 0.21,
backgroundColor: Colors.empty,
alignItems: 'center',
justifyContent: 'center',
},
image: {
backgroundColor: Colors.empty,
width: width - 20,
height: height * 0.21,
},
useRef returns a mutable object whose .current property is initialized to initial value. So basically you need to access scrollIndex like this
flatListRef.current.scrollToIndex({animated: true, index: flatListRef.current.index + 1})
Edit: To answer your comment, you should've asked directly for auto-scrolling, however, The code below should work!
const flatListRef = useRef(null)
let index=0;
const totalIndex = datas.length - 1;
useEffect (() => {
setInterval (() => {
index++;
if(index < totalIndex) {
flatListRef.current.scrollToIndex({animated: true, index: index})
} else {
flatListRef.current.scrollToIndex({animated: true, index: 0})
}
}, 1000)
}, []);
as per the guide on React Native onScrollToIndexFailed
you have to override
onScroll={(e) => {
this.setState({ currentIndex: Math.floor(e.nativeEvent.contentOffset.x / (dimensions.wp(this.props.widthPercent || 100) - 1)) });
}}
onScrollToIndexFailed={info => {
const wait = new Promise(resolve => setTimeout(resolve, 500));
wait.then(() => {
flatListRef?.current?.scrollToIndex({ index: 0, animated: true });
});
}}
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!
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,
},
});