I am using react native with expo cli and I have a component:
import React, {useEffect, useState} from 'react'
import {View, TextInput, Text, TouchableOpacity, Animated, Easing} from 'react-native';
import s from './Login_style'
import {connect} from "react-redux";
const LoginInner = (props) => {
const [mode, setMode] = useState(true)
const btnAnim = new Animated.Value(0)
const setModeAnim = (is) => {
if (is) {
Animated.timing(btnAnim, {
toValue: 1,
duration: 300,
easing: Easing.out(Easing.exp),
useNativeDriver: false,
}).start()
// setMode(false)
} else {
Animated.timing(btnAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.exp),
useNativeDriver: false
}).start()
// setMode(true)
}
}
const size1 = btnAnim.interpolate({
inputRange: [0, 1],
outputRange: ['30%', '60%']
})
const size2 = btnAnim.interpolate({
inputRange: [0, 1],
outputRange: ['60%', '30%']
})
return (
<View style={s.page}>
<View style={s.inputs}>
<Text style={[s.text, s.title]}>{mode ? 'Вход' : 'Регистрация'}</Text>
<View style={s.textInput}>
<View>
<Text style={[s.text, s.placeholder]}>Логин</Text>
</View>
<TextInput style={s.input}/>
</View>
<View style={s.textInput}>
<View>
<Text style={[s.text, s.placeholder]}>Пароль</Text>
</View>
<TextInput style={s.input}/>
</View>
<View style={s.actions}>
<Animated.View style={[s.btn, s.loginBtn, {width: size2}]}>
<TouchableOpacity style={s.touchableBtn} onPress={() => {
setModeAnim(false)
}}>
<Text style={s.loginBtn_text}>{mode ? 'Вход' : '<--'}</Text>
</TouchableOpacity>
</Animated.View>
<Animated.View style={[s.btn, s.regBtn, {width: size1}]}>
<TouchableOpacity style={s.touchableBtn} onPress={() => {
setModeAnim(true)
}}>
<Text style={s.loginBtn_text}>{mode ? '-->' : 'Регистрация'}</Text>
</TouchableOpacity>
</Animated.View>
</View>
</View>
</View>
)
}
const Login = connect((state) => {
return {}
}, {})(LoginInner)
export default Login
Here I am interested in this function
const setModeAnim = (is) => {
if (is) {
Animated.timing(btnAnim, {
toValue: 1,
duration: 300,
easing: Easing.out(Easing.exp),
useNativeDriver: false,
}).start()
// setMode(false)
} else {
Animated.timing(btnAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.exp),
useNativeDriver: false
}).start()
// setMode(true)
}
}
I run the animation here when the function is executed (ignore the commented out line for now). The animation really works.
VIDEO DEMONSTRATION: https://youtu.be/fiGy0gNej68
But if I change the state of the component in this function after starting the animation:
setMode(false) or setMode(true) animation does not start, even the buttons do not change their size
VIDEO DEMONSTRATION: https://youtu.be/deLZEKVnaBY
Tell me how to solve this
Thanks
To add to #LIMPIX64's answer:
using const btnAnim = React.useRef(new Animated.Value(0)).current solves the problem because since Animated.Value is an instance of a class, you need to wrap it in a useRef call so that it only gets created once when the component renders for the first time (which is what useRef accomplishes). Otherwise, the class could be reinstantiated on a state change and cause the issue you're experiencing
Please do const btnAnim = React.useRef(new Animated.Value(0)).current – Harrison
Related
I'm having this issue on React-native 0.70 and 0.65, Android specifically. I can't work out if it's an issue with my setup or a bug in React and I can't find any other posts about it anywhere even though it's a pretty noticeable issue.
When using InteractionManager.runAfterInteractions() to delay tasks until an animation has finished, everything works as expected if useNativeDriver: false but if it is set to true the callback is run immediately after the animation is started.
It should be reproducible with the following code:
import React, { useRef } from "react";
import { Animated, Text, View, StyleSheet, Button, InteractionManager } from "react-native";
const App = () => {
const fadeAnim = useRef(new Animated.Value(0)).current;
const fadeIn = () => {
console.log('Starting fade in')
Animated.timing(fadeAnim, {
toValue: 1,
duration: 10000,
useNativeDriver: true // <--- InteractionManager.runAfterInteractions only works when set to false.
}).start();
const task = InteractionManager.runAfterInteractions(() =>
{
console.log('This should run after the animation has finished fading.')
})
};
return (
<View style={styles.container}>
<Animated.View
style={[
styles.fadingContainer,
{
opacity: fadeAnim
}
]}
>
<Text style={styles.fadingText}>Fading View!</Text>
</Animated.View>
<View style={styles.buttonRow}>
<Button title="Fade In View" onPress={fadeIn} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center"
},
fadingContainer: {
padding: 20,
backgroundColor: "powderblue"
},
fadingText: {
fontSize: 28
},
buttonRow: {
flexBasis: 100,
justifyContent: "space-evenly",
marginVertical: 16
}
});
export default App;
I would appreciate any advice or if anyone could tell me if they encounter the same issue or not (indicating it's a problem with my setup). Thanks.
Here is my code for a custom functional component (./Animations/ModalViewMoveUp.js)
import React, { useRef, useEffect } from 'react';
import { Animated, Easing, Text, View } from 'react-native';
import { ExtendedExceptionData } from 'react-native/Libraries/LogBox/LogBox';
export const ModalViewMoveUp = (props) => {
const moveAnim = useRef(new Animated.Value(900)).current
const animIn = Animated.timing(
moveAnim,
{
toValue: 50,
duration: 1700,
easing: Easing.elastic(),
useNativeDriver:false
}
)
const animOut = Animated.timing(
moveAnim,
{
toValue: 1000,
duration: 1700,
easing: Easing.elastic(),
useNativeDriver:false
}
)
React.useEffect(() => {
animIn.start();
}, [moveAnim])
return (
<Animated.View
style={{
...props.style,
marginTop: moveAnim,
}}
>
{props.children}
</Animated.View>
);
}
And in ./App.js
import {ModalIconViewMoveDown} from './Animations/ModalIconViewMoveDown'
...
<ModalViewMoveUp >
{/* other elements */}
<TouchableOpacity onPress={()=>???}><Text>CANCEL</Text></TouchableOpacity>
</ModalViewMoveUp >
I basically want a way to start the exit animation animOut.start() on when the cancel button is pressed, but i cant even get a ref of the ModalViewMoveUp element in the TouchableOpacity , let alone call a function from it.
Even something like this would work for me
<ModalViewMoveUp shouldStop={this.state.shouldStop}>
{/* other elements */}
<TouchableOpacity onPress={()=>setState{{shouldStop:true}}}><Text>CANCEL</Text></TouchableOpacity>
</ModalViewMoveUp >
And then in /ModalViewMoveUp.js
React.useEffect(() => {
if(shouldStop){
animOut.start();
}
//animIn.start() runs on render()
}, [moveAnim])
But i know i cant just set a state in an exported component.
Do i have to convert my functional component to a class?
Any code examples would be appreciated!
What i ended up doing: pass a state bool when calling the component
<ModalViewMoveUp shouldOpen ={this.state.shouldOpen } style ={styles.modalViewUp}>
{/* other elements */}
<TouchableOpacity onPress={()=>setState{{shouldOpen:true }}}><Text>CANCEL</Text></TouchableOpacity>
</ModalViewMoveUp >
and in /ModalViewMoveUp.js
return(
<View>
{props.shouldOpen == true &&
<Animated.View
style={{
...props.style,
marginTop: moveAnim,
}}
>
{props.children}
</Animated.View>
}
{props.shouldOpen == false &&
<Animated.View
style={{
...props.style,
marginTop: moveAnim,
}}
>
{animOut.start()}
{props.children}
</Animated.View>
}
</View>
);
)
It works exactly as i want it to. If anyone has any contraindications feel free to share !
I was creating a Button in react native (on web). If I view the button in Edge, a very weird padding appears. I tried to debug but I can't get a fix. It works correctly on Android and Firefox.
I think there is some problem with Edge's renderer (Blink) because the code works correctly in firefox and on android (native).
Here is the code for the component:
import React, { useState } from 'react';
import { View, Animated, Easing, Image, StyleSheet, Pressable, Text } from 'react-native';
import { Hoverable } from 'react-native-web-hooks';
const Button = () => {
const [animatedButtonPressed] = useState(new Animated.Value(0));
const [animatedButtonHover] = useState(new Animated.Value(0));
const animate = (value, toValue, duration = 150, easing = Easing.linear) => {
Animated.timing(value, {
toValue: toValue,
duration: duration,
easing: easing,
// change later
useNativeDriver: false
}).start();
}
const animatedTextStyle = {
color: animatedButtonPressed.interpolate({
inputRange: [0,1],
outputRange: ["#fff" , "#2b7f3c"]
})
}
const animatedButtonStyle = {
backgroundColor: animatedButtonHover.interpolate({
inputRange: [0,1],
outputRange: ["#2b7f3c", "transparent"]
}),
}
const animatedButtonPressedStyle = {
backgroundColor: animatedButtonPressed.interpolate({
inputRange: [0,1],
outputRange: ["#fff", "red"]
}),
}
let Handlers = (props) => {
return (
<>
<Pressable onPressIn={() => animate(animatedButtonPressed, 1)} onPressOut={() => animate(animatedButtonPressed, 0)}>
<Hoverable onHoverIn={() => animate(animatedButtonHover, 1)} onHoverOut={() => animate(animatedButtonHover, 0)}>
<View {...props}/>
</Hoverable>
</Pressable>
</>
)
}
let Backgrounds = (props) => {
return (
<Animated.View style={animatedButtonPressedStyle}>
<Animated.View style={animatedButtonStyle}>
<View {...props}/>
</Animated.View>
</Animated.View>
)
}
return (
<View style={styles.container}>
<View style={styles.button}>
<Backgrounds>
<Handlers>
<View style={styles.padding}>
<Animated.Text style={[styles.text, animatedTextStyle]}>Hello</Animated.Text>
</View>
</Handlers>
</Backgrounds>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignSelf: 'flex-start',
overflow: 'hidden',
},
button: {
borderRadius: 3,
borderWidth: 2,
borderColor: 'green',
},
padding: {
paddingHorizontal: 10,
paddingVertical: 5,
},
text: {
color: 'white',
fontSize: 16
}
});
export default Button;
I think this is unfortunately a symptom of using React-native on the web things get squashed and you won't have as much control for cross browser quirks.
Is there any reason you didn't use ReactJS?
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,
},
});
In the Component ProgressBarAndroid, there are props indeterminable={Boolean} which show to a user an animation of what it's going on. I would like to do almost the same on ProgressViewIOS. So I tried to Animate it with Animated...
I saw on docs of Animated method called 'createAnimatedComponent' which they use to create Animated.View
I tried so to create another Animated (Native) Component but it doesn't work at all.
The animation should gradually raise fillValue to 20 % and continue with an original value from the media upload...
This is my Component
// ProgressBar.ios.js
// #flow
import { PropTypes } from 'react';
import Component from 'components/base/Component';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(ProgressViewIOS);
class ProgressBarIOS extends Component {
static propTypes = {
// Percentage (0 - 100)
fill: PropTypes.number.isRequired,
};
constructor(props, context: any) {
super(props, context);
this.state = {
fillValue: new Animated.Value(props.fill),
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.fill === 0) {
Animated.timing(this.state.fillValue, { toValue: 0.2, duration: 500 }).start();
} else if (nextProps.fill > 19) {
this.state.fillValue.setValue(nextProps.fill / 100);
}
}
shouldComponentUpdate(nextProps) {
return this.props.fill !== nextProps.fill;
}
render() {
return (
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={this.state.fillValue} />
);
}
}
export default ProgressBarIOS;
EDIT: AnimatedComponent is used to modify style only. Props could be passed as animated value but remember it is not a number!
Animated.createAnimatedComponent can animate a number of different properties, however only some properties are supported using the native driver, fortunately it appears progress on ProgressViewIOS is one of them.
Here is a working implementation of an animated ProgressViewIOS.
import * as React from 'react';
import { View, SafeAreaView } from 'react-native';
import { ProgressViewIOS, Animated } from 'react-native';
const AnimatedProgressViewIOS = Animated.createAnimatedComponent(
ProgressViewIOS
);
export default function App() {
const value = React.useRef(new Animated.Value(0));
React.useEffect(() => {
Animated.loop(
Animated.timing(value.current, {
duration: 2000,
toValue: 1,
useNativeDriver: true,
})
).start();
}, []);
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressViewIOS
style={{ alignSelf: 'stretch' }}
progress={value.current}
/>
</View>
</SafeAreaView>
);
}
It's worth noting that ProgressViewIOS is now deprecated, but building your own progress view is very straight forward and requires just two Views with simple styling like this (expo snack):
import * as React from 'react';
import { View, SafeAreaView, StyleSheet, Button, Text } from 'react-native';
import { Animated } from 'react-native';
export default function App() {
const [progress, setProgress] = React.useState(() => Math.random());
return (
<SafeAreaView>
<View style={{ padding: 20 }}>
<AnimatedProgressView progress={progress} />
<Text style={{padding: 20, textAlign: 'center'}}>{Math.round(progress * 100)}%</Text>
<Button title="Animate" onPress={() => setProgress(Math.random())} />
</View>
</SafeAreaView>
);
}
function AnimatedProgressView({ progress, style }) {
const value = React.useRef(new Animated.Value(0));
const [width, setWidth] = React.useState(0);
React.useEffect(() => {
Animated.spring(value.current, { toValue: progress }).start();
}, [progress]);
return (
<View
style={[styles.track, style]}
onLayout={(event) => setWidth(event.nativeEvent.layout.width)}>
<Animated.View
style={[
styles.fill,
{
transform: [
{
translateX: value.current.interpolate({
inputRange: [0, 1],
outputRange: [-width, 0],
overflow: 'clamp',
}),
},
],
},
]}
/>
</View>
);
}
const styles = StyleSheet.create({
track: {
minHeight: 4,
borderRadius: 2,
overflow: 'hidden',
backgroundColor: '#ddd',
},
fill: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'blue',
},
});