Iām trying to increase the size of an image on user press and decrease it when he presses again with animated API using the following:
const [viewState, setViewState]= useState(true);
const scaleAnim = (new Animated.Value(.9))
const scaleOut = () => {
if(viewState){
Animated.timing(scaleAnim, {
toValue: 2.2,
duration: 2000,
useNativeDriver:true,
}).start(()=>{setViewState(false)});
}
else{
Animated.timing(scaleAnim, {
toValue: .9,
duration: 700,
useNativeDriver:true,
}).start(setViewState(true));
}
};
<Animated.View style={{transform:[{scale:scaleAnim}]}} >
<Image style={styles.image} source={require('../path..')} />
</Animated.View>
const styles = StyleSheet.create({
image: {
width:70,
resizeMode:"contain",
height: 45,
alignSelf: "center",
},
But the issue is, whenever the duration is over, the size is going back to default. I want to to stay permanently and do the opposite when the user presses again(decrease size)
Any suggestions?
Created a Component hope this is how you wanted....
snack: https://snack.expo.io/neEtc2ihJ
export default function App() {
const [viewState, setViewState] = React.useState(true);
const scale = React.useRef(new Animated.Value(1)).current;
const [init, setInit] = React.useState(true);
React.useEffect(() => {
if (init) {
setInit(false);
} else {
if (viewState) {
Animated.timing(scale, {
toValue: 2,
duration: 1000,
useNativeDriver: true,
}).start();
} else {
Animated.timing(scale, {
toValue: 0.5,
duration: 700,
useNativeDriver: true,
}).start();
}
}
}, [viewState]);
const scaleOut = () => {
setViewState(!viewState);
};
return (
<View style={styles.container}>
<Animated.View style={{ transform: [{ scale }] }}>
<Image
style={styles.image}
source={require('./assets/snack-icon.png')}
/>
</Animated.View>
<Button title="animate" onPress={scaleOut} />
</View>
);
}
Firstly you want your animated value to either useState or useRef. The react-native example uses useRef, so I'd suggest you to do the same. I'd also suggest tha you use an interpolation to scale so that you can tie more animations to that one animated value. The result would be something like this:
const animatedValue = useRef(new Animated.Value(0)).current;
const [ toggle, setToggle ] = useState(false)
const scaleOut = () => {
let animation
if(!toggle){
animation = Animated.timing(animatedValue, {
toValue: 1,
duration: 700,
useNativeDriver:true,
});
}
else{
animation = Animated.timing(animatedValue, {
toValue: 0,
duration: 2000,
useNativeDriver:true,
});
}
animation.start(()=>{
setToggle(!toggle)
})
};
let scaleAnim = animatedValue.interpolate({
inputRange:[0,1],
outputRange:[0.9,2.2]
})
return (
<Animated.View style={{transform:[{scale:scaleAnim}]}} >
<TouchableOpacity onPress={scaleOut}>
<Image style={styles.image} source={require('../path..')} />
</TouchableOpacity>
</Animated.View>
);
By doing this, you can scale multiple images at whatever size you want by just adding another interpolation. But if you have no desire to do that:
const scaleOut = () => {
let animation
if(!toggle){
animation = Animated.timing(animatedValue, {
toValue: 2.2,
duration: 2000,
useNativeDriver:true,
});
}
else{
animation = Animated.timing(animatedValue, {
toValue: 0.9,
duration: 700,
useNativeDriver:true,
});
}
animation.start(()=>{
setToggle(!toggle)
})
};
return (
<Animated.View style={{transform:[{scale:animatedValue}]}} >
<TouchableOpacity onPress={scaleOut} />
<Image style={styles.image} source={require('../path..')} />
</TouchableOpacity>
</Animated.View>
);
If you want to go a step further, swap out the TouchableOpacity for a Pressable, put the animations in a Animated.loop and start that in onPressIn, and on pressOut stop the animations and bring the set the animatedValue back to initial value:
const onPressIn= ()=>{
Animated.loop([
Animated.timing(animatedValue, {
toValue: 2.2,
duration: 2000,
useNativeDriver:true,
}),
Animated.timing(animatedValue, {
toValue: 0.9,
duration: 700,
useNativeDriver:true,
});
],{useNativeDriver:true}).start()
}
const onPressOut= ()=>{
animatedValue.stop()
Animated.timing(animatedValue,{
toValue: 0.9,
duration: 700,
useNativeDriver:true,
})
}
return(
<Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
<Animated.View style={{transform:[{scale:animatedValue}]}} >
<Image style={styles.image} source={require('../path..')} />
</Animated.View>
</Pressable>
);
Related
I am using react-spring lib. in the react-native application. I have archived most of animation but I can not figure out how to use rotate init.
Here is my current code =>
<Spring
from={ { width: '100%', resizeMode: "contain", rotate: 0 } }
to={ { rotate: 360 } }
loop
config={ { duration: 1500 } }
>
{({ rotate, ...props }) => (
<AnimatedImage source={img} style={ {...props, transform: [{ rotate: `${rotate}deg` }]} } />
)}
</Spring>
Then get a error Like this =>
TypeError: undefined is not a function (near '...transform.forEach...')
If someone can explain how to achieve the rotate animation with react-spring it would be really helpful.
you can find a simple example here
You can achieve the rotation animation in #react-spring/native by using the interpolations methods.
const AnimatedImage = animated(Image)
const App = () => {
const { rotate } = useSpring({
from: { rotate: 0 },
to: { rotate: 1 },
loop: { reverse: true },
config: { duration: 1500 }
})
return (
<AnimatedImage source={img} style={{ transform: [ { rotate: rotate.to([0, 1], ['0deg', '360deg']) } ] }} />
);
};
It is strange that we can not apply rotation directly, but i haven't found any other method other than this one in #react-spring/native
react-spring/native it's web based library maybe it's work but there's no clear way for it, but on the other hand you can archive this animation using native Animated like this:
const App: () => Node = () => {
const spinValue = new Animated.Value(0);
const spin = () => {
spinValue.setValue(0);
Animated.timing(spinValue, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
}).start(() => spin());
};
useEffect(() => {
spin();
}, []);
const rotate = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
return (
<SafeAreaView>
<View style={styles.container}>
<Animated.View style={{transform: [{rotate}]}}>
<AntDesign name={'loading1'} color={'blue'} size={50} />
</Animated.View>
</View>
</SafeAreaView>
);
};
you can check this working example from here.
and here's a repo on github with a working react native project also from here.
Hope it helps you :D
Rotation is a style transform and in RN, you can rotate things like this
render() {
return (
<View style={{transform:[{rotate: '10 deg'}]}}>
<Image source={require('./logo.png')} />
</View>
);
}
However, to animate things in RN, you have to use numbers, not strings. Can you still animate transforms in RN or do I have to come up with some kind of sprite sheet and change the Image src at some fps?
You can actually animate strings using the interpolate method. interpolate takes a range of values, typically 0 to 1 works well for most things, and interpolates them into a range of values (these could be strings, numbers, even functions that return a value).
What you would do is take an existing Animated value and pass it through the interpolate function like this:
spinValue = new Animated.Value(0);
// First set up animation
Animated.timing(
this.spinValue,
{
toValue: 1,
duration: 3000,
easing: Easing.linear, // Easing is an additional import from react-native
useNativeDriver: true // To make use of native driver for performance
}
).start()
// Next, interpolate beginning and end values (in this case 0 and 1)
const spin = this.spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})
Then use it in your component like this:
<Animated.Image
style={{transform: [{rotate: spin}] }}
source={{uri: 'somesource.png'}} />
In case if you want to do the rotation in loop, then add the Animated.timing in the Animated.loop
Animated.loop(
Animated.timing(
this.spinValue,
{
toValue: 1,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
)
).start();
Don't forget to add property useNativeDriver to ensure that you get the best performance out of this animation:
// First set up animation
Animated.timing(
this.state.spinValue,
{
toValue: 1,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
).start();
A note for the newbies like me:
For animating something else you need to wrap it in <Animated.SOMETHING> for this to work. Or else the compiler will panic on that transform property:
import {Animated} from 'react-native';
...
//animation code above
...
<Animated.View style={{transform: [{rotate: spinValue}] }} >
<YourComponent />
</Animated.View>
BUT for an image (Animated.Image), the example above is 100% goodness and correct.
Since most of the answers are functions & hooks based, herewith a complete example of class based Animation of Image.
import React from 'react';
import {
SafeAreaView,
View,
Animated,
Easing,
TouchableHighlight,
Text,
} from 'react-native';
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
rotateValueHolder: new Animated.Value(0)
};
}
componentDidMount = () => {
this.startImageRotateFunction();
}
startImageRotateFunction = () => {
Animated.loop(Animated.timing(this.state.rotateValueHolder, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
useNativeDriver: false,
})).start();
};
render(){
return(
<SafeAreaView>
<View>
<Animated.Image
style={{
width: 200,
height: 200,
alignSelf:"center",
transform:
[
{
rotate: this.state.rotateValueHolder.interpolate(
{
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}
)
}
],
}}
source={{uri:'https://raw.githubusercontent.com/AboutReact/sampleresource/master/old_logo.png',}}
/>
<TouchableHighlight
onPress={() => this.startImageRotateFunction()}>
<Text style={{textAlign:"center"}}>
CLICK HERE
</Text>
</TouchableHighlight>
</View>
</SafeAreaView>
);
}
}
Just gonna drop the solution I solved by stitching together parts from the answers here.
import { Feather } from '#expo/vector-icons'
import * as React from 'react'
import { TextStyle, Animated, Easing } from 'react-native'
import { Colors, FontSize } from '~/constants/Theme'
export const LoadingSpinner = React.memo(
({ color = Colors['sand'], size = FontSize['md'] - 1, fadeInDelay = 1000, ...props }: Props) => {
const fadeInValue = new Animated.Value(0)
const spinValue = new Animated.Value(0)
Animated.sequence([
Animated.delay(fadeInDelay),
Animated.timing(fadeInValue, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
}),
]).start()
Animated.loop(
Animated.timing(spinValue, {
toValue: 360,
duration: 300000,
easing: Easing.linear,
useNativeDriver: true,
})
).start()
return (
<Animated.View
style={{
opacity: fadeInValue,
transform: [{ rotate: spinValue }],
}}
>
<Feather
name="loader"
size={size}
style={{
color,
alignSelf: 'center',
}}
{...props.featherProps}
/>
</Animated.View>
)
}
)
type Props = {
color?: TextStyle['color']
size?: number
featherProps?: Partial<Omit<React.ComponentProps<typeof Feather>, 'style'>>
fadeInDelay?: number
}
Hope it helps š
I have a ScrollView and I want to have a View with background color white, then if scroll position is more than 0 change it to transparent, and back to white if scroll goes back to 0.
I tried to get started but react native animations seem crazy complicated coming back from a vue.js background.
Here is my code:
const [animation, setAnimation] = useState(new Animated.Value(0))
const handleScroll = (event) => {
console.log(event.nativeEvent.contentOffset.y);
var scroll = event.nativeEvent.contentOffset.y;
if(scroll > 0)
{
handleAnimation();
}
};
const boxInterpolation = animation.interpolate({
inputRange: [0, 1],
outputRange:["rgb(255,255,255)" , "rgba(255,255,255,0)"]
})
const animatedStyle = {
backgroundColor: boxInterpolation
}
const handleAnimation = () => {
Animated.timing(animation, {
toValue:1,
duration: 1000,
useNativeDriver: true
}).start();
};
And my views
<ScrollView showsVerticalScrollIndicator={false} scrollEventThrottle={16} onScroll={handleScroll} style={{flex:1,backgroundColor:'white',position:'relative'}}>
<View style={{ width:'100%', height:505,position:'relative' ,backgroundColor:'red}}>
</View>
</ScrollView>
<Animated.View style={{width:'100%',height:100,...animatedStyle, flexDirection:'row',justifyContent:'space-around',alignItems:'center',position:'absolute',bottom:0,left:0}}>
</Animated.View>
The recommend way of getting the ScrollView x and y position is this below
const scrolling = React.useRef(new Animated.Value(0)).current;
<Animated.ScrollView
onScroll={Animated.event(
[{
nativeEvent: {
contentOffset: {
y: scrolling,
},
},
}],
{ useNativeDriver: false },
)}
</Animated.View>
Now everything works heres a full example (https://snack.expo.dev/#heytony01/ca5000) must be run a phone not web. And below is the code.
import React from 'react';
import { Button,View,Animated,ScrollView } from 'react-native';
export default function App() {
const scrolling = React.useRef(new Animated.Value(0)).current;
const boxInterpolation = scrolling.interpolate({
inputRange: [0, 100],
outputRange:["rgb(255,255,255)" , "rgba(255,255,255,0)"],
})
const animatedStyle = {
backgroundColor: boxInterpolation
}
return (
<View style={{flex:1}}>
<Animated.ScrollView showsVerticalScrollIndicator={false} scrollEventThrottle={16} style={{flex:1,backgroundColor:'black',position:'relative'}}
onScroll={Animated.event(
[{
nativeEvent: {
contentOffset: {
//
y: scrolling,
},
},
}],
{ useNativeDriver: false },
)}
>
<View style={{ width:'100%', height:505,position:'relative', backgroundColor:'red'}}>
</View>
</Animated.ScrollView>
<Animated.View style={{width:'100%',height:100,...animatedStyle, flexDirection:'row',justifyContent:'space-around',alignItems:'center',position:'absolute',bottom:0,left:0,zIndex:1}}>
</Animated.View>
</View>
);
}
Also if you want to print the animated values when debugging you can do this
React.useEffect(()=>{
scrolling.addListener(dic=>console.log(dic.value))
return ()=> scrolling.removeAllListeners()
},[])
I made a custom TextInput that I'm animating into view on my Registration screen component. The issue is that for every keystroke onChangeText, the animation plays again and this is not the expected behavior. If I take out the animation, the TextInput works fine.
I've tried changing my dependency on my useState, also tried to wrap the component with a useCallback and useMemo and none has gotten it to work.
Also worthy of note is that I'm handling my state management with useReducer. Code below
const RegistrationScreen = ({ navigation }: any) => {
const textPosition = new Animated.Value(width);
const inputPosition = new Animated.Value(width);
const inputPosition2 = new Animated.Value(width);
const inputPosition3 = new Animated.Value(width);
const inputPosition4 = new Animated.Value(width);
const [state, dispatch] = useReducer(reducer, initialState);
const { name, email_address, phone_number, password } = state;
const animate = Animated.sequence([
Animated.parallel([
Animated.timing(textPosition, {
toValue: 0,
delay: 50,
duration: 1000,
useNativeDriver: true,
easing: Easing.elastic(3),
}),
Animated.timing(inputPosition, {
toValue: 0,
delay: 100,
duration: 1000,
useNativeDriver: true,
easing: Easing.elastic(3),
}),
Animated.timing(inputPosition2, {
toValue: 0,
delay: 200,
duration: 1000,
useNativeDriver: true,
easing: Easing.elastic(3),
}),
Animated.timing(inputPosition3, {
toValue: 0,
delay: 300,
duration: 1000,
useNativeDriver: true,
easing: Easing.elastic(3),
}),
Animated.timing(inputPosition4, {
toValue: 0,
delay: 400,
duration: 1000,
useNativeDriver: true,
easing: Easing.elastic(3),
}),
]),
]);
const _onCreateAccountHandler = () => dispatch(getPhoneVerificationCode(phone_number));
const _onChangeHandler = (field: any, value: any) => dispatch({ type: 'FIELD', field, value });
useEffect(() => {
animate.start();
}, [animate]);
return (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAvoidingView
style={{ flex: 1 }}
keyboardVerticalOffset={100}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<View style={styles.container}>
<View style={{ flex: 1 }}>
<Header
backButtonEnabled
backButtonColor={colors.darkGray}
onBackButtonPress={() => navigation.goBack(null)}
/>
</View>
<View style={styles.innerContainer}>
<Animated.View
style={[
styles.createAccount,
{
transform: [
{
translateX: textPosition,
},
],
},
]}>
<Text style={styles.creaetAccountText}>{strings.create_an_account}</Text>
</Animated.View>
<View style={styles.textAreaContainer}>
<Animated.View
style={[
styles.textInputContainer,
{
transform: [
{
translateX: inputPosition,
},
],
},
]}>
<TextInput
placeHolder={strings.name}
value={name}
onChangeText={(text: any) => _onChangeHandler('name', text)}
onCancelPressed={() => {}}
placeHolderStyle={{
backgroundColor: colors.lightWhite,
}}
autoCorrect={false}
/>
</Animated.View>
You could try wrapping animate with React.useCallback()
So From further research, it turns out that since I'm animating the component with a value that is always constant (width), onTextChange will always rerender the screen and any value that is different will be accounted for, hence, the animation plays again since the current value is different from the initial value.
My Solution:
I had to use useState to change the initial value as soon as the first animation completes. I also had to call that in a setTimeout that runs after the animation that way - the initial width will update to become the current width and when the component re-renders, it won't render again. (or in this case, there's no difference in the translate X value, so animation won't play again)
I have an animation that uses Animated.timing() which slides a component in to view when the condition mapIsCentered = true. When the condition isn't met the component just disappears ungracefully. I'd like for it to slide in and out as the condition changes.
One thing to note the mapIsCentered state is updated on a different screen and passed as a prop to the component I am working in. I have logged the state and it updates when the map is moved.
** the slide in works as expected
Thanks to #Ashwith for the first answer
const values = useRef(new Animated.ValueXY({ x: 0, y: 120 })).current;
useEffect(() => {
Animated.timing(values, {
toValue: mapIsCentered ? { x: 0, y: 0 } : { x: 0, y: 120 },
duration: 500,
useNativeDriver: false,
}).start();
}, [mapIsCentered]);
{!walkInProgress && !hasOnGoingWalks && (
<Animated.View
style={{
transform: [{ translateY: values.y }],
}}
>
<WeatherToast
translations={translations}
loading={loading}
weather={weather}
/>
</Animated.View>
Thanks in advance!
I have changed the structure hope it works for you...
snack: https://snack.expo.io/#ashwith00/excited-orange
App.js
const walkInProgress = false , hasOnGoingWalks = false;
export default function App() {
const { width } = useWindowDimensions();
const [mapCentered, setMapCentered] = React.useState(false)
const toggle = () => {
setMapCentered((ct) => !ct);
};
return (
<View style={styles.container}>
<WeatherToast mapCentered={mapCentered && !walkInProgress && !hasOnGoingWalks} />
<Button title="shift" onPress={toggle} />
</View>
);
}
WeatherTost.js
export default ({ mapCentered }) => {
const [visible, setVisible] = useState(mapCentered);
const { width } = useWindowDimensions();
const values = React.useRef(new Animated.ValueXY({ x: 0, y: 120 })).current;
React.useEffect(() => {
if (mapCentered) {
setVisible(true);
Animated.timing(values, {
toValue: { x: 0, y: 0 },
duration: 300,
}).start();
} else {
Animated.timing(values, {
toValue: { x: width, y: 0 },
duration: 300,
}).start(({ finished }) => {
if (finished) {
setVisible(false);
}
});
}
}, [mapCentered]);
const styles = [];
return visible ? (
<Animated.View
style={{
width: 200,
height: 200,
position: 'absolute',
backgroundColor: 'red',
transform: [
{
translateX: values.x,
},
{
translateY: values.y,
},
],
}}
/>
) : (
<View />
);
};