React navigation transition animations with useNativeDriver only run first time - animation

I am using react-navigation Transitioner to create a custom StackNavigator. When using useNativeDriver: true in my transition configuration, the animation for the transition only runs the first time. When set to false, it works as expected.
Note: Whilst setting it to false does fix my problem, I get choppy performance on Android without it, even in production mode.
Below snippet is my navigation view
render() {
return (
<Transitioner
configureTransition={this._configureTransition}
navigation={this.props.navigation}
render={this._render}
/>
);
}
_configureTransition = () => {
return { useNativeDriver: true };
}
_render = (transitionProps) => {
return transitionProps.scenes.map(scene => this._renderScene(transitionProps, scene));
}
_renderScene = (transitionProps, scene) => {
const { layout, position } = transitionProps;
const { index } = scene;
const translateX = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [layout.initWidth, 0, 0],
});
const animationStyle = {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: '#FFF',
transform: [{ translateX }],
};
const Scene = this.props.router.getComponentForRouteName(scene.route.routeName);
return (
<Animated.View key={scene.key} style={animationStyle}>
<Scene />
</Animated.View>
);
}
Below is a screen cap of the problem. Note how the first transition is animated, whilst future ones are not (the 'back' navigation should be animated too)

Related

React native state change transitions

What is the best pattern, in react native, to animate components on state change?
For example I have a list of elements and tapping on one I want it to disappear and the ones below him to 'get up' filling the missing space
How can I make the transition smooth?
React-natives own animated API works really well.
Basically you have a value in state, which you connect with a style props, and change that value over time. (for examples follow link)
For smooth animations use usenativedriver (not always possible) and also, make sure you don't have debugger runnning in emulated/real device
EDIT: 2018-05-31
This is an example of how I've used it. Probably exist other ways of doing it
import { Animated, Text} from 'react-native';
class ShowCaseAnimation extends Component {
state = {
animations: {
height: new Animated.Value(0),
fade: new Animated.Value(0),
}
}
componentDidMount() {
const { height, fade } = this.state.animations;
if (this.props.animate) {
doneAnimation({ height, fade }).start(() => {
// Do stuff after animations
});
}
}
render() {
const { animations } = this.state;
return (
<Animated.View
style={{
height: animate? animations.height : 300,
opacity: animate? animations.fade: 1,
// other styling
}}
>
<Text> All your base are belong to us </Text>
</Animated.View>
);
}
}
*doneAnimation: *
import { Animated, Easing } from 'react-native';
export const doneAnimation = ({ height, fade }) => Animated.parallel([
Animated.timing(height, {
toValue: 300,
easing: Easing.elastic(),
duration: 500,
delay: 1500,
}),
Animated.timing(fade, {
toValue: 1,
easing: Easing.ease,
duration: 1000,
delay: 1500,
}),
]);
export default doneAnimation;
doneAnimation will change the state and perform the described animations.
This is how you can trigger an animation on state change in a functional component.
Say you have a Button that changes state with onPress:
<Button title="toggle" onPress={() => setValue(!Value)} />
then you can trigger the animation inside a useEffect with the Value
that changes in the dependency array:
const [Value, setValue] = useState(true);
useEffect(() => {
// Input your animation here
// ...
}, [Value]);

react native navigation custom animated transition

I'm using react native v0.49 and I'm trying to implement custom transition when navigate to other page.
what I'm trying to do is to make transition only for one scene from scene 2 to scene3. but not for all the app.
this example I found it's for all web so I want to make just for one screen and for all the app because if I do that way it will effect for all the app and it's not what I'm looking for. any idea?
class SceneOne extends Component {
render() {
return (
<View>
<Text>{'Scene One'}</Text>
</View>
)
}
}
class SceneTwo extends Component {
render() {
return (
<View>
<Text>{'Scene Two'}</Text>
</View>
)
}
}
let AppScenes = {
SceneOne: {
screen: SceneOne
},
SceneTwo: {
screen: SceneTwo
},
SceneThree: {
screen: SceneTwo
},
}
let MyTransition = (index, position) => {
const inputRange = [index - 1, index, index + 1];
const opacity = position.interpolate({
inputRange,
outputRange: [.8, 1, 1],
});
const scaleY = position.interpolate({
inputRange,
outputRange: ([0.8, 1, 1]),
});
return {
opacity,
transform: [
{scaleY}
]
};
};
let TransitionConfiguration = () => {
return {
// Define scene interpolation, eq. custom transition
screenInterpolator: (sceneProps) => {
const {position, scene} = sceneProps;
const {index} = scene;
return MyTransition(index, position);
}
}
};
class App extends Component {
return (
<View>
<AppNavigator />
</View>
)
}
Here's an example of how we do it, you can add your own transitions to make it your own. Our goal was simply to expose the baked-in transition configurations to have more control over the animations. Our transition configuration: https://gist.github.com/jasongaare/db0c928673aec0fba7b4c8d1c456efb6
Then, in your StackNavigator, add that config like so:
StackNavigator(
{
LoginScreen: { screen: LoginScreen },
HomeScreen: { screen: HomeScreen },
},
{
stateName: 'MainStack',
initialRouteName: 'HomeScreen',
initialRouteParams: { transition: 'fade' },
transitionConfig: TransitionConfig,
}
);
Finally, when you navigate, just add your params when you navigate:
this.props.navigation.navigate('HomeScreen', { transition: 'vertical' })

Draggable view within parent boundaries

I'm facing a task where I want to place a draggable marker on a background image and afterwards get the coordinates of the marker within the background image.
I've followed this neat tutorial to make a draggable marker using Animated.Image, PanResponder and Animated.ValueXY. The problem is that I cannot figure out how to limit the draggable view to only move around within the boundaries of its parent (the background image).
Any help is very much appreciated :)
Best regards
Jens
Here is one way to do it using the react-native-gesture-responder.
import React, { Component } from 'react'
import {
StyleSheet,
Animated,
View,
} from 'react-native'
import { createResponder } from 'react-native-gesture-responder'
const styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
},
draggable: {
height: 50,
width: 50,
},
})
export default class WorldMap extends Component {
constructor(props) {
super(props)
this.state = {
x: new Animated.Value(0),
y: new Animated.Value(0),
}
}
componentWillMount() {
this.Responder = createResponder({
onStartShouldSetResponder: () => true,
onStartShouldSetResponderCapture: () => true,
onMoveShouldSetResponder: () => true,
onMoveShouldSetResponderCapture: () => true,
onResponderMove: (evt, gestureState) => {
this.pan(gestureState)
},
onPanResponderTerminationRequest: () => true,
})
}
pan = (gestureState) => {
const { x, y } = this.state
const maxX = 250
const minX = 0
const maxY = 250
const minY = 0
const xDiff = gestureState.moveX - gestureState.previousMoveX
const yDiff = gestureState.moveY - gestureState.previousMoveY
let newX = x._value + xDiff
let newY = y._value + yDiff
if (newX < minX) {
newX = minX
} else if (newX > maxX) {
newX = maxX
}
if (newY < minY) {
newY = minY
} else if (newY > maxY) {
newY = maxY
}
x.setValue(newX)
y.setValue(newY)
}
render() {
const {
x, y,
} = this.state
const imageStyle = { left: x, top: y }
return (
<View
style={styles.container}
>
<Animated.Image
source={require('./img.png')}
{...this.Responder}
resizeMode={'contain'}
style={[styles.draggable, imageStyle]}
/>
</View>
)
}
}
I accomplished this another way using only the react-native PanResponder and Animated libraries. It took a number of steps to accomplish and was difficult to figure out based on the docs, however, it is working well on both platforms and seems decently performant.
The first step was to find the height, width, x, and y of the parent element (which in my case was a View). View takes an onLayout prop. onLayout={this.onLayoutContainer}
Here is the function where I get the size of the parent and then setState to the values, so I have it available in the next function.
` onLayoutContainer = async (e) => {
await this.setState({
width: e.nativeEvent.layout.width,
height: e.nativeEvent.layout.height,
x: e.nativeEvent.layout.x,
y: e.nativeEvent.layout.y
})
this.initiateAnimator()
}`
At this point, I had the parent size and position on the screen, so I did some math and initiated a new Animated.ValueXY. I set the x and y of the initial position I wanted my image offset by, and used the known values to center my image in the element.
I continued setting up my panResponder with the appropriate values, however ultimately found that I had to interpolate the x and y values to provide boundaries it could operate in, plus 'clamp' the animation to not go outside of those boundaries. The entire function looks like this:
` initiateAnimator = () => {
this.animatedValue = new Animated.ValueXY({x: this.state.width/2 - 50, y: ((this.state.height + this.state.y ) / 2) - 75 })
this.value = {x: this.state.width/2 - 50, y: ((this.state.height + this.state.y ) / 2) - 75 }
this.animatedValue.addListener((value) => this.value = value)
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: ( event, gestureState ) => true,
onMoveShouldSetPanResponder: (event, gestureState) => true,
onPanResponderGrant: ( event, gestureState) => {
this.animatedValue.setOffset({
x: this.value.x,
y: this.value.y
})
},
onPanResponderMove: Animated.event([ null, { dx: this.animatedValue.x, dy: this.animatedValue.y}]),
})
boundX = this.animatedValue.x.interpolate({
inputRange: [-10, deviceWidth - 100],
outputRange: [-10, deviceWidth - 100],
extrapolate: 'clamp'
})
boundY = this.animatedValue.y.interpolate({
inputRange: [-10, this.state.height - 90],
outputRange: [-10, this.state.height - 90],
extrapolate: 'clamp'
})
}`
The important variables here are boundX and boundY, as they are the interpolated values that will not go outside of the desired area. I then set up my Animated.Image with these values, which looks like this:
` <Animated.Image
{...this.panResponder.panHandlers}
style={{
transform: [{translateX: boundX}, {translateY: boundY}],
height: 100,
width: 100,
}}
resizeMode='contain'
source={eventEditing.resource.img_path}
/>`
The last thing I had to make sure, was that all of the values were available to the animation before it tried to render, so I put a conditional in my render method to check first for this.state.width, otherwise, render a dummy view in the meantime. All of this together allowed me to accomplish the desired result, but like I said it seems overly verbose/involved to accomplish something that seems so simple - 'stay within my parent.'

How to animate fontWeight in React Native

I'm trying to animate fontWeight of an Animated.Text but can't get it to work.
1. Interpolating
const titleFontWeight = this.positionTitle1.x.interpolate({
inputRange: [-0.3 * SCREEN_WIDTH, 0],
outputRange: ['400', '600']
});
render() {
return (
<Animated.Text style={{ fontWeight: titleFontWeight }}>
Title
</Animated.Text>
);
}
With this solution the effect of the change won't happen until the whole animation (i.e. the animation of this.positionTitle1) is done and I get a
Warning: Failed prop type: Invalid prop 'fontWeight' of value '377.333' supplied to text [...].
2. Animated.timing
constructor() {
super();
this.fontWeight = new Animated.Value(400);
}
animateFontWeight() {
Animated.timing(this.fontWeight, {
toValue: 600,
duration: 500
}).start();
}
render() {
return (
<Animated.Text style={{ fontWeight: `${this.fontWeight._value}` }}>
Title
</Animated.Text>
);
}
For this solution the effect also doesn't show until the animation is done.
Is there any solution to this?
Try this
Setup Text
<Animated.Text
style={{fontWeight: this.fontWeightAnimation.interpolate({
inputRange: [300, 900],
outputRange: ['300', '900'],
easing: value => {
const thousandRounded = value * 1000;
if (thousandRounded < 300) {
return 0;
}
if (thousandRounded < 600) {
return 0.5;
}
return 1;
}
})}}> Sample Text </Animated.Text>
Init Animation : this.fontWeightAnimation = new Animated.Value(300);
Start Animation :
Animated.timing(this.fontWeightAnimation, {
toValue: 900,
duration: 3000
}).start();
As it has been mentioned above, fontWeight only accept fixed string value, ex.: '400', '800' or 'normal', 'bold'. To achieve your goal, you can simply using setState at the same time as your animation start.
startAnimation() {
Animated.parallel([
Animated.timing(this.state.translate, {
toValue: -27,
duration: 200,
}),
Animated.timing(this.state.color, {
toValue: 1,
duration: 300,
}),
]).start();
this.setState({
fontWeight: '800'
});};
stopAnimation() {
this.state.translate.setValue(0);
this.state.color.setValue(0);
this.setState({
fontWeight: '400'
});};

React Native Animating List of texts

I have an array of texts that I want to flash on a blank screen, one after the other with animations. Something like:
state = {
meditations: ["Take a deep breath", "embrace this feeling", "breath
deeply", ...]
}
I want to show only one string at a time, and animate their opacity. So a string fades in and fades out, then the next string, and so on.
I am new to react native and quite confused about how to go about this. Please, how may I approach this, I have read the docs but still not clear how to.
Below is what I have tried, I modified this from the docs but it shows everything at once. I'm still trying to see how I can make them animate one after the other, showing only one at a time. Thanks for your help in advance.
import React from 'react';
import { Animated, Text, View } from 'react-native';
class FadeInView extends React.Component {
state = {
fadeAnim: new Animated.Value(0), // Initial value for opacity: 0
}
renderMeditations() {
let { fadeAnim } = this.state;
return this.props.meditations.map((meditation, index) => {
Animated.timing( // Animate over time
this.state.fadeAnim, // The animated value to drive
{
toValue: 2, // Animate to opacity: 1 (opaque)
duration: 10000, // Make it take a while
}
).start(() => {
this.setState({ fadeAnim: new Animated.Value(0) })
}); // Starts the animation
return (
<Animated.Text // Special animatable View
key={index}
style={{
...this.props.style,
opacity: fadeAnim, // Bind opacity to animated value
}}
>
{meditation}
</Animated.Text>
)
})
}
render() {
return (
<View style={{flex: 1}}>
{this.renderMeditations()}
</View>
);
}
}
export default class App extends React.Component {
state = {
meditations: ["Take a deep breath", "Calm down", "Relax", "Tell yourself all will be fine"]
}
render() {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<FadeInView meditations={this.state.meditations} style={{fontSize: 28, textAlign: 'center', margin: 10}} />
</View>
)
}
}
After much toil with this, I was able to solve it with react-native-animatable like so:
import React from "react";
import {
View,
Text,
Animated
} from "react-native";
import * as Animatable from 'react-native-animatable';
class VideoScreen extends React.Component {
state = {
meditations: ["Take a deep breath", "embrace this feeling", "breath
deeply"],
index: 0
};
render() {
const { meditations, index } = this.state;
return (
<View style={{flex: 1}}>
<Animatable.Text
key={index}
animation={'fadeIn'}
iterationCount={2}
direction="alternate"
duration={2000}
onAnimationEnd={() => {
if (this.state.index < this.state.meditations.length - 1) {
this.setState({ index: this.state.index + 1});
}
}}
style={{
position: "absolute",
left: 0, right: 0,
bottom: 40
}}>
{meditations[index]}
</Animatable.Text>
</View>
);
}
}
export default VideoScreen;
The map function executes all at once so basically you are rendering/returning all 3 items at the same time. I understand that your issue is that the animation is working tho.
If what you want is to show one text, then the other and so on I suggest iterating the index of your text array instead of using the map function.
Something like:
Execute Animation
Increase Index
Index = 0 if you are at the end of the array.
In a loop. Check setInterval, it might help you.
For the function components:-
we can use the above-metioned solutions. I am writing a function hopefully it will help you display a looping text with the animation
We will use this package for the animation https://github.com/oblador/react-native-animatable.
import {StyleSheet} from 'react-native';
import React, {useState} from 'react';
import * as Animatable from 'react-native-animatable';
const UserMessage = () => {
const [index, setIndex] = useState(0);
const meditations = [
'Take a deep breath',
'embrace this feeling',
'breath deeply',
];
return (
<Animatable.Text
key={index}
animation={'fadeIn'}
iterationCount={2}
direction="alternate"
duration={2000}
onAnimationEnd={() => {
if (index < meditations.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
}}
style={styles.messageStyle}>
{meditations[index]}
</Animatable.Text>
);
};
export default UserMessage;
const styles = StyleSheet.create({
messageStyle: {
textAlign: 'center',
fontSize: 18,
fontWeight: '500',
width: '80%',
color: '#1C1C1C',
marginBottom: 20,
minHeight: 50,
alignSelf: 'center',
},
});

Resources