I am fairly new to Animated API in react native. I went through a lot of tutorials for using animation API and it seems in every one of them the elements are positioned as absolute, is it necessary to position the element absolute ?
Also I made a piece of animation but it looks glitchy, I think the view after the textinput donot have absolute as position which might be causing the issue. Is it possible to do the animation I'm trying to while keeping the textinput position absolute but other elements positioned with flexbox?
Here is the code
handleFocus = () => {
console.log('starting animation');
this.setState({
isFocused: true
});
Animated.timing(this.isFromViewFocused, {
toValue: 1,
duration: 300
}).start();
}
handleBlur = () => {
console.log('Blurring');
this.setState({
isFocused: false
});
Animated.timing(this.isFromViewFocused, {
toValue: 0,
duration: 300
}).start();
}
render() {
const labelStyle = {
position: this.state.isFocused === true ? 'absolute' : 'relative',
alignItems: 'center',
width: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [DEVICE_WIDTH * 0.45, DEVICE_WIDTH]
}),
left: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [DEVICE_WIDTH * 0.03, 0]
}),
marginBottom: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [0, 80]
}),
top: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [10, 0]
}),
borderWidth: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [0, 5]
}),
borderColor: 'black',
paddingTop: this.state.isFocused === true ? 20 : 0
};
return (
<View style={styles.container}>
<ScrollView style={{ flex: 1 }} keyboardDismissMode='on-drag'>
<Animated.View
style={labelStyle}
>
<TextInput
onFocus={this.handleFocus}
onBlur={this.handleBlur}
style={{
borderColor: 'black',
borderWidth: 1,
width: '90%'
}}
>
<Text>Hey Text</Text>
</TextInput>
</Animated.View>
<Animated.View
style={[styles.LocationContainer,
{ marginTop: this.isFromViewFocused.interpolate({
inputRange: [0, 1],
outputRange: [20, 80]
})
}
]}>
Using absolute positioning combined with left, top, bottom, right is bad for performances. That's why your animations looks "glitchy".
You'd be better using transforms so your component stays relative and native optimizations can be made (same as CSS3 transforms).
Also, when using not native-optimizable properties (such as the one you use) you cannot set useNativeDriver to true. Which makes performances even worse.
To add with that, you cannot (or shouldn't) interpolate based on a boolean. AnimatedJS provides you with an Animated.Value class that aims to make it easier to interpolate stuff.
Here is a simpler example to Animated:
export class MyAnimatedComponent extends React.Component {
state = {
animatedValue: new Animated.Value(0);
}
focus = () => {
const { animatedValue } = this.state;
Animated.timing(animatedValue, {
duration: 280,
toValue: 1,
// This will make your animation glitch-free/performant.
useNativeDriver: true,
}).start();
}
blur = () => {
Animated.timing(animatedValue, {
duration: 140,
toValue: 0,
// This will make your animation glitch-free/performant.
useNativeDriver: true,
}).start();
}
render () {
const { animatedValue } = this.state;
const animatedStyles = {
transform: [
{
// Move the div by 120px to the left when value === 1
translateX: animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -120],
// Tells Animated to never go outside of the outputRange
extrapolate: 'clamp',
})
},
{
translateY: animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -50],
extrapolate: 'clamp',
})
}
]
}
return (
<View style={styles.wrapper}>
<Animated.View style={animatedStyles} onFocus={onFocus} onBlur={onBlur}>
I'm some content into an Animated div.
</Animated.View>
</View>
)
}
}
Related
I want to animate a View with dynamic height like an accordion menu.
class Accordeon extends Component {
constructor(props) {
super(props);
this.state = {
ViewScale: new Animated.Value(0),
chevronDeg: new Animated.Value(0),
expanded: false,
height: 0,
};
}
render() {
const ViewDegValue = this.state.ViewScale.interpolate({
inputRange: [0, 50, 100],
outputRange: ['0deg', '90deg', '180deg']
});
return(
<View>
<TouchableOpacity
style={[accordionStyles.header]}
onPress={() => {
this.setState({expanded:!this.state.expanded});
Animated.parallel ([
Animated.timing(
this.state.chevronDeg,
{
toValue: !this.state.expanded ? 0 : 100,
duration: 200,
}
),
Animated.timing(
this.state.ViewScale,
{
toValue: this.state.expanded ? 0 : 100,
duration: 200,
}
)
]).start();
}}
>
<Text
style={[accordionStyles.header__text, styles.headline__h2]}
>
{this.props.headerLabel}
</Text>
<Animated.View
style={{
transform: [
{rotateZ: ViewDegValue}
]
}}>
<FontAwesome
iconStyle={[accordionStyles.header__icon,]}
name={"chevron-down"}
size={30}
>
</FontAwesome>
</Animated.View>
</TouchableOpacity>
<Animated.View
onLayout={(event) => {
var {x, y, width, height} = event.nativeEvent.layout;
this.setState({height:height})
}}
style={[
this.state.expanded ? accordionStyles.content__hidden : accordionStyles.content__show,
accordionStyles.content,
{
**** this code below ****
// height: this.state.ViewScale.interpolate({
// inputRange: [0, 25, 50, 75, 100],
// outputRange: [0, 75, 150, 225, this.state.height]
// }),
transform: [
{translateY: - this.state.height / 2},
{scaleY: this.state.ViewScale.interpolate({
inputRange: [0, 25, 50, 75, 100],
outputRange: [0, .5, 0.75, 0.9, 1]
})},
{translateY: this.state.height / 2}
]
}
]}
>
{this.props.children}
</Animated.View>
</View>
);
}
}
and my problem is if activate this code the accordeon is not in full size just 224.*px because on layout cant gets the full size and if I set the height to auto. just scale the view then there is if the accordeon closed a giant gap between them because there is no 0 height set. I won't put an absolute and opacity element of every accordeon anywhere that's no solution.
I found my self the solution.
i add onLayout to the header and to the content but don't animate the height of the content of the whole component so the content has its original size but i have to set the initial headerHeight bigger then it is and on first load it sets it self to its original size. Thats no problem because the headerHeight is not dynamic only the contentHeight.
<Animated.View
style={[
accordionStyles.accordeon,
{height: this.state.ViewScale.interpolate({
inputRange: [0, 25, 50, 75, 100],
outputRange: [this.state.headerHeight, 75, 150, 225, this.state.headerHeight
+ this.state.contentHeight]
}),}
]}
>
I'm trying to animate a View to be sticky to a certain position when scrolling. The scrolling component I use is FlatList.
Here's what I've come so far:
ProfileScreen
Basically, this is the structure of my ProfileScreen.js:
<View>
<Animated.View /> --> The animated cover photo (includes the animated avatar as well)
<Animated.FlatList /> --> This renders the list of images you see in the video. And a ListHeaderComponent renders the content from the name to the View of follow numbers
</View
Now what I want to achieve is:
when the user scrolls the whole FlatList and it reachs the position where the View containing the follow numbers reaches the bottom border of the header, that follow View will sticks there. (the follow View must scrolls along with th FlatList)
Just like a sticky tab view.
You can imagine my "goal" screen is like the profile screen of Twitter.
Here's the full code:
//...
const ProfileScreen = (props) => {
const [index, setIndex] = useState(0);
const [isFollowed, setIsFollowed] = useState(false);
const [firstFollow, setFirstFollow] = useState(false);
const [enableScrollView, setEnableScrollView] = useState(false);
const trips = useSelector(state => state.trips.availableTrips);
const scrollY = useRef(new Animated.Value(0)).current;
const headerHeight = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT], // [0, 50]
outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT], // [120,70]
extrapolate: 'clamp'
});
const profileImageHeight = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT-10],
outputRange: [PROFILE_IMAGE_MAX_HEIGHT,PROFILE_IMAGE_MIN_HEIGHT],
extrapolate: 'clamp'
});
const profileImageMarginTop = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
outputRange: [
HEADER_MAX_HEIGHT - PROFILE_IMAGE_MAX_HEIGHT / 2,
HEADER_MIN_HEIGHT /2 - PROFILE_IMAGE_MIN_HEIGHT/2
],
extrapolate: 'clamp'
});
const profileImageMarginLeft = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
outputRange: [
WIDTH/2 - PROFILE_IMAGE_MAX_HEIGHT/2,
10
],
extrapolate: 'clamp'
})
const headerZindex = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT, 120],
outputRange: [0, 0, 1000],
extrapolate: 'clamp'
});
const headerTitleColor = scrollY.interpolate({
inputRange: [0, 50, 70, 80, 90, 100],
outputRange: ['transparent', 'transparent', 'transparent', 'transparent', 'transparent', 'white'],
extrapolate: 'extend'
});
const tabBarPosition = scrollY.interpolate({
inputRange: [0,200],
outputRange: [(HEADER_MAX_HEIGHT + PROFILE_IMAGE_MIN_HEIGHT)*2, HEADER_MIN_HEIGHT],
extrapolate: 'clamp'
})
const _render_Sitcky_Info_View = () => {
return(
<View style={{marginTop: HEADER_MAX_HEIGHT+ PROFILE_IMAGE_MAX_HEIGHT/2}}>
<InfoDisplay
isFollowed={isFollowed}
onFollow={onFollow}
userName={profile.userName}
tripsNumber={trips.length}
navigateToTripsListScreen={() => props.navigation.navigate('TripsListScreen')}
/>
</View>
)
}
return(
<SafeAreaView style={styles.container}>
<Animated.View style={[styles.backgroundImage, {
height: headerHeight,
zIndex: headerZindex,
elevation: headerZindex,
justifyContent: 'center'
}]}>
<Animated.Image
source={require('../../../assets/images/beach.jpg')}
style={{
flex: 1,
}}
/>
<View style={{
position: 'absolute',
left: 60,
}}>
<Animated.Text style={{color: headerTitleColor, fontSize: 20, fontWeight: 'bold'}}>{profile.userName}</Animated.Text>
</View>
<Animated.View style={[styles.profileImgContainer,{
height: profileImageHeight,
width: profileImageHeight,
borderRadius: PROFILE_IMAGE_MAX_HEIGHT/2,
position: 'absolute',
top: profileImageMarginTop,
left: profileImageMarginLeft,
alignSelf: 'center'
}]}>
<Image source={{uri: profile.userAvatar}} style={styles.profileImg} />
</Animated.View>
</Animated.View>
<Animated.FlatList
style={{flex: 1, backgroundColor: 'transparent'}}
contentContainerStyle={{justifyContent: 'center', alignItems: 'center'}}
scrollEventThrottle={16}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: scrollY}}}],
)}
showsVerticalScrollIndicator={false}
bounces={true}
data={imgData}
numColumns={NUM_COLUMNS}
keyExtractor={item => item.id}
renderItem={(itemData) => {
return(
<TouchableOpacity style={{
marginHorizontal: findMidOfThree(imgData, itemData.index) ? 0 : 5 ,
marginVertical: 3,
shadowColor: 'black',
shadowOpacity: 0.26,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 8,
elevation: 7,
}}>
<Image source={itemData.item.source} style={{
height: WIDTH/3.5,
width: WIDTH/3.5,
borderColor: 'white',
borderWidth: 2,
borderRadius: 10,
}} />
</TouchableOpacity>
)
}}
ListHeaderComponent={_render_Sitcky_Info_View}
{...props}
>
</Animated.FlatList>
</SafeAreaView>
);
};
export default ProfileScreen;
const styles = StyleSheet.create({
//...
})
The InfoDisplay contains the follow numbers View. I know I will have to separate it and bring it to the ProfileScreen. I also tried to animate it with the tabBarPosition, but it didn't work like I thought it'd
You can imagine my "goal" screen is like Twitter profile screen, where my follow numbers View acts like the Tab View in Twitter
https://drive.google.com/file/d/1ERt_7gnPgiwXPg-WODXSnrOZ10kQgPQl/view?usp=drivesdk
Please HELP me. I'd be very grateful!
Calculate the distance need to move by the follow View as shown in the image (let it be x).
Here I am taking 500 as an offset. you can choose any number greater than x. (or use screen height)
const range = [x, x+500];
const translateY = scrollY.interpolate({inputRange:range, outputRange:range, extrapolateLeft: 'clamp'});
ie, if x = 200;
const translateY = scrollY.interpolate({inputRange:[200, 700], outputRange:[200, 700], extrapolateLeft: 'clamp'});
The logic is that, when the follow View reaches top while scrolling (ie, moved distance x). it will start translating down with the same speed of scrolling.
Provide this translateY to the follow View.
<Animated.View style={{...styles, transform: [{translateY}]}} /> // your follow View
I hope this will helps you!
I've been messing around with React Native and I've tried to create a simple card that flips around on press (back and forth):
I had it working as a class component but now I've tried to refactor to a functional component and its only flipping one way but not back on second press.
Can someone advise me what I've missed? :/
I tried using useState and useEffect for the value and animated value etc, but no dice...
import React from 'react';
import { View, StyleSheet, Animated, TouchableWithoutFeedback, Text } from 'react-native';
import TabBarIcon from '../components/TabBarIcon';
const App = () => {
let animatedValue = new Animated.Value(0);
let value = 0;
animatedValue.addListener(({ value }) => {
this.value = value;
})
let frontInterpolate = animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['0deg', '180deg'],
})
let backInterpolate = animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['180deg', '360deg']
})
let frontOpacity = animatedValue.interpolate({
inputRange: [89, 90],
outputRange: [1, 0]
});
let backOpacity = animatedValue.interpolate({
inputRange: [89, 90],
outputRange: [0, 1]
});
let elevationFront = animatedValue.interpolate({
inputRange: [0, 25],
outputRange: [10, 0]
})
let elevationBack = animatedValue.interpolate({
inputRange: [155, 180],
outputRange: [0, 10]
})
const flipCard = () => {
if (value >= 90) {
Animated.spring(animatedValue,{
toValue: 0,
friction: 8,
tension: 10
}).start();
} else {
Animated.spring(animatedValue,{
toValue: 180,
friction: 8,
tension: 10
}).start();
}
}
const frontAnimatedStyle = {
transform: [{ rotateY: frontInterpolate}]
}
const backAnimatedStyle = {
transform: [{ rotateY: backInterpolate }]
}
return (
<TouchableWithoutFeedback onPress={() => flipCard()} >
<View>
<Animated.View style={[frontAnimatedStyle, styles.paperFront,{elevation: elevationFront}, {opacity: frontOpacity}]}>
<Text style={{fontSize: 20,paddingTop: 8, paddingLeft: 8, color: 'black',lineHeight: 20}}>
Title Front {value} - <Text style={{fontSize: 8}}>KPI</Text>
</Text>
<View style={{position: "absolute", paddingTop: 3, right: 8}}>
<TabBarIcon style={{color: "black"}} name={"md-more"} />
</View>
</Animated.View>
<Animated.View style={[backAnimatedStyle, styles.paperBack, {elevation: elevationBack}, {opacity: backOpacity}]}>
<Text>Back title {value}</Text>
</Animated.View>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
paperFront : {
marginHorizontal: 15,
backgroundColor: "white",
minHeight: 200,
borderRadius: 5,
marginBottom: 15,
},
paperBack : {
top: -215,
marginHorizontal: 15,
backgroundColor: "white",
minHeight: 200,
borderRadius: 5,
marginBottom: 15,
}
});
export default App
You need to call your "value" not "this.value", but since you already got "value" parameter I suggest you change the variable name:
let val = 0;
animatedValue.addListener(({ value}) => {
val = value;
});
P.S: don't forget to change your calls to "value" variable into "val"
fix it
if (this.value >= 90) {
in expo
I want to make a shaking image animation in React Native when i press a TouchableOpacity.
so far i have make an animation image with this code bellow:
const backgroundImage = require('./components/images/baby-sleep.jpg')
class App extends Component {
constructor(props) {
super(props)
this.animatedValue = new Animated.Value(0)
}
handleAnimation = () => {
Animated.timing(this.animatedValue, {
toValue: 1,
duration: 1000,
easing: Easing.ease
}).start()
}
This is the code where i call handleAnimation() in TouchableOpacity:
<TouchableOpacity onPress={this.handleAnimation}>
<Text style={{fontSize: 24, fontWeight: 'bold'}}>Play</Text>
</TouchableOpacity>
and this the code where i make animated image:
<Animated.Image
source={backgroundImage}
resizeMode='contain'
style={{
transform: [
{
translateX: this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 120]
})
},
{
translateY: this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 230]
})
},
{
scaleX: this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 15]
})
},
{
scaleY: this.animatedValue.interpolate({
inputRange: [0, 9],
outputRange: [1, 150.5]
})
}
]
}}
/>
that code has make an animation when i press the TouchableOpacity, but now i'm really not sure how to make the image be shaking animation when i press the TouchableOpacity
You are actually pretty close. I will provide a full example below for a single wiggle rotation and then you can just add additional animations according your requirements:
const backgroundImage = require('./components/images/baby-sleep.jpg')
class App extends Component {
constructor(props) {
super(props)
this.animatedValue = new Animated.Value(0)
}
handleAnimation = () => {
// A loop is needed for continuous animation
Animated.loop(
// Animation consists of a sequence of steps
Animated.sequence([
// start rotation in one direction (only half the time is needed)
Animated.timing(this.animatedValue, {toValue: 1.0, duration: 150, easing: Easing.linear, useNativeDriver: true}),
// rotate in other direction, to minimum value (= twice the duration of above)
Animated.timing(this.animatedValue, {toValue: -1.0, duration: 300, easing: Easing.linear, useNativeDriver: true}),
// return to begin position
Animated.timing(this.animatedValue, {toValue: 0.0, duration: 150, easing: Easing.linear, useNativeDriver: true})
])
).start();
}
}
And then to add this rotation to the Image component:
<Animated.Image
source={backgroundImage}
resizeMode='contain'
style={{
transform: [{
rotate: this.animatedValue.interpolate({
inputRange: [-1, 1],
outputRange: ['-0.1rad', '0.1rad']
})
}]
}}
/>
Let's say I'm running an animation loop between green and blue (well there are more colors along the interpolation but I start off cycling between the first three):
this.state = {
colorValue: new Animated.Value(0)
};
this.interpolations = {
background: this.state.colorValue.interpolate({
inputRange: [0, 25, 50, 75, 100],
outputRange: ["#64e5a5", "#216a7a", "#64e5a5", "#ffb637", "#ffd338"]
})
};
this.backgroundAnimation = Animated.loop(
Animated.timing(this.state.colorValue, {
toValue: 50,
duration: 10000
})
);
this.backgroundAnimation.start();
Note that I set up extra colors along that interpolation hoping I could just stop the animation loop and start a new one cycling between 75 and 100 for smooth transition between the two color cycles. When I modify this code for the desired effect, the animation seems to always start from 0. I tried setting the animated value to 50 but that didn't fix anything. Can someone explain the right way to do this?
Note: The above code doesn't include an attempt to actually switch to the next color cycle because I really don't have any clue how to do it.
Edit: I'm getting close, but not quite there, the second loop doesnt work:
this.backgroundAnimation.stop();
this.interpolations.background = this.state.colorValue.interpolate({
inputRange: [0, 100],
outputRange: [JSON.stringify(this.interpolations.background), "#ffd338"]
});
Animated.timing(this.state.colorValue, {
toValue: 100,
duration: 3000
}).start(() => {
console.log("Done");
this.setState({
...this.state,
colorValue : new Animated.Value(0) //just spit-balling, idk
});
this.interpolations.background = this.state.colorValue.interpolate({
inputRange: [0, 50, 100],
outputRange: ["#ffd338", "#fff39b", "#ffd338"]
});
console.log(this.interpolations.background);
this.backgroundAnimation = Animated.loop(
Animated.timing(this.state.colorValue, {
toValue: 100,
duration: 3000
})
);
this.backgroundAnimation.start();
});
Figured it out :)
this.backgroundAnimation.stop();
this.setState({
...this.state,
interpolations: {
...this.state.interpolations,
background: this.state.colorValue.interpolate({
inputRange: [0, 100],
outputRange: [
JSON.stringify(this.state.interpolations.background),
"#ffd338"
]
})
}
});
Animated.timing(this.state.colorValue, {
toValue: 100,
duration: 2000
}).start(() => {
this.setState({
...this.state,
interpolations: {
...this.state.interpolations,
background: this.state.colorValue.interpolate({
inputRange: [0, 50, 100],
outputRange: ["#ffd338", "#ffffff", "#ffd338"]
})
}
});
this.backgroundAnimation = Animated.loop(
Animated.timing(this.state.colorValue, {
toValue: 100,
duration: 2000
})
);
this.backgroundAnimation.start();
});
});