I have a Stack Navigator that uses either a modal or slide transition depending on the screen.
i.e. a transition sequence might look like:
HomeScreen -Slide> Screen1 -Modal From Bottom> Screen2
This used to not be a problem, but the updated modal transitions cause there to be noticeable "glitches" due to the varying card interpolation styles. I know I can fix this by adjusting the cardStyle interpolation values based on the current/previous screen.
Is there any way to access the navigation object for the Stack Navigator when creating the cardStyleInterpolator?
EDIT:
cardStyleInterpolator (incomplete):
export const cardStyleInterpolator = ({
index,
current,
next,
inverted,
layouts: { screen },
insets,
}) => {
const isLandscape = screen.width > screen.height;
const topOffset = isLandscape ? 0 : 10;
const statusBarHeight = insets.top;
const aspectRatio = screen.height / screen.width;
const progress = add(current.progress, next ? next.progress : 0);
const translateY = multiply(
progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [
screen.height,
index === 0 || index === 1 ? 0 : topOffset,
(index === 0 || index === 1 ? statusBarHeight : 0) - topOffset * aspectRatio,
],
}),
inverted
);
const overlayOpacity = progress.interpolate({
inputRange: [0, 1, 1.0001, 2],
outputRange: [0, 0.3, 1, 1],
});
const scale = isLandscape
? 1
: progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [
1,
1,
screen.width ? 1 - (topOffset * 2) / screen.width : 1,
],
});
const borderRadius = isLandscape
? 0
: index === 0 || index === 1
? progress.interpolate({
inputRange: [0, 1, 2],
outputRange: [0, 0, 10],
})
: 10;
return {
cardStyle: {
overflow: 'hidden',
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
marginTop: index === 0 || index === 1 ? 0 : statusBarHeight,
transform: [{ translateY }, { scale }],
},
overlayStyle: { opacity: overlayOpacity },
};
};
This cardStyleInterpolator is a modified version of the forModalPresentationIOS. The modification of adding index === 1 to the conditionals helps the transition from Screen2 -> Screen1. Since we pushed from HomeScreen to Screen1, when we nav back to Screen1 we want it to animate as if it was the initial route and not another modal.
The current forModalPresentationIOS assumes that index 0 is always the index where we use the modal transition from, but when overriding the cardStyleInterpolator of index 1 to use the slide transition causes this assumption to be invalid.
Related
I have made a bit of re-usable animation code. It works great but I an unsure if it is possible to get the animations back to their original values after a button is pressed easily.
The only way I can think of it to use useShared Values to store all the before and after values and set them as required but this would involve alot of values but because the animations have already ran, there must be a way to just take them back to their original start?
The code I am using for the animations is : -
EntryAnimation.js
import React, { useEffect } from 'react';
import Animated, {
useAnimatedStyle,
useSharedValue,
useDerivedValue,
interpolate,
withDelay,
withTiming,
withSpring,
Easing,
} from 'react-native-reanimated';
export const EntryAnimation = ({
children,
index,
rotateV,
scaleV,
offsetXV,
offsetYX,
}) => {
const play = useSharedValue(play);
const progress = useDerivedValue(() => {
return play.value
? withDelay(50 * (index ?? 0), withSpring(1, { duration: 350 }))
: 0;
});
useEffect(() => {
play.value = play;
}, []);
const animatedStyle = useAnimatedStyle(() => {
// const opacity = interpolate(progress.value, [0, 1], [0, 1]);
const translateY = interpolate(progress.value, [0, 1], [0, offsetYX]);
const translateX = interpolate(progress.value, [0, 1], [0, offsetXV]);
const rotate = interpolate(progress.value, [0, 1], [0, rotateV]);
const scale = interpolate(progress.value, [0, 1], [1, scaleV]);
return {
transform: [{ translateY }, { translateX }, { rotate }, { scale }],
};
});
return <Animated.View style={animatedStyle}>{children}</Animated.View>;
};
And to use on an element in my main code, I use :-
<EntryAnimation
index={1}
rotateV={0}
scaleV={0.8}
offsetXV={0}
offsetYX={-270}>
<Animated.Image
source={{ uri: item.poster }}
style={[styles.posterImage, { zIndex: 6 }]}
/>
</EntryAnimation>
I have tried using the code below but because its in a ternary statement I am getting errors?
{animStarted ? (
<EntryAnimation
index={1}
rotateV={0}
scaleV={0.8}
offsetXV={0}
offsetYX={-270}
>
) : (
<EntryAnimation
index={1}
rotateV={0}
scaleV={1}
offsetXV={0}
offsetYX={0}
>
)}
Any ideas?
I am converting some of my default Animated stuff to react-native-reanimated v2 and can't seem to figure out how to interpolate a value to a string percentage. I also can't find anything related to this in the docs.
Using Animated from react-native I could just do:
width: widthAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
})
But the interpolate() function from reanimated only takes number params, so how can I animate the width then?
interpolate(widthAnim.value, [0, 1], ['0%', '100%'])
// err: Type 'string' is not assignable to type 'number'.
You can simply use template literal
width: `${widthAnim.value * 100}%`
Check out more official example
A full example to complete the previous response:
const searchViewAnimatedWidth = useSharedValue(0);
const searchBoxAnimatedStyle = useAnimatedStyle(() => {
const interpolatedWidth = interpolate(
searchViewAnimatedWidth.value,
[0, 1],
[40, 100],
{ extrapolateRight: Extrapolation.CLAMP }
);
return {
width: withTiming(`${interpolatedWidth}%`, {
duration: 500,
useNativeDriver: false,
}),
};
});
<Animated.View
style={{...otherStyles,...searchBoxAnimatedStyle}}>
...
</Animated.View>
And in your trigger
onPress={() => {
searchViewAnimatedWidth.value = searchViewAnimatedWidth.value == 0 ? 1 : 0;
}}
I'm currently working on a ReactNative app that has 2 screens. The goal is to animate the screens such that they push each other up or down depending on which screen you're coming from. The GIF below is the transition I am trying to achieve.
Currently I'm using createAnimatedSwitchNavigator to create the effect, the problem is that my current transition only pushes the screens up. Is there a way to detect which screen I'm currently on within the animated switch navigator so that I can change the direction of the transition?
const MySwitch = createAnimatedSwitchNavigator(
{
Home: {
screen: Home
},
Page2: {
screen: Page2
},
},
{
transition: (
<Transition.Together>
<Transition.Out
type="slide-top"
durationMs={400}
interpolation="easeIn"
/>
<Transition.In type="slide-bottom" durationMs={500} />
</Transition.Together>
)
}
);
Kindly guide me for this.
Thanks
Did some researching and was able to get this working with the following code:
const AppNavigator = createStackNavigator({
Home: {
screen: Home
},
Page2: {
screen: Page2
}
},
{
initialRouteName: "Home",
headerMode: "none",
defaultNavigationOptions: {
gesturesEnabled: false
},
transitionConfig: () => ({
transitionSpec: {
duration: 1400,
easing: Easing.out(Easing.poly(4)),
timing: Animated.timing,
},
screenInterpolator: sceneProps => {
const { layout, position, scene } = sceneProps;
const { index } = scene;
const height = layout.initHeight;
const translateY = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [height, 0, -height],
});
const opacity = position.interpolate({
inputRange: [index - 1, index - 0.99, index],
outputRange: [0, 1, 1],
});
return { opacity, transform: [{ translateY }] };
},
}),
});
The hardest part was understanding interpolation as there seems to be a lot of seemingly arbitrary values. I didn't find the documentation the greatest, however was able to come up with the following understanding:
Take the code below for example. Here the first thing is setting the type of translation, which in this case is translateY
const translateY = position.interpolate({
inputRange: [index - 1, index, index + 1],
outputRange: [height, 0, -height],
});
The next confusing part is the input and output ranges, what do these numbers mean? Well the input range actually maps to the output range. Looking something like this:
inputRange: [newScreen, currentScreen, previousScreen]
In inputRange we specify which screens we want to animate, then in output range we specify what we want to do with those screens. Modifying outputRange[0] would modify the newScreen, etc.
Since we already set the translation type to translateY we know the screen is either moving up or down.
outputRange: [height, 0, -height]
This is now telling the new screen to move up to the top of the screen and the old screen to also move up, beyond the top of the screen (hence the -height, which would be the same as -100vh in CSS).
Here is a guide for react-navigation-stack 2.0 alpha/react-navigation 5
https://callstack.com/blog/custom-screen-transitions-in-react-navigation/
I wanted to create a zoom out effect when a button is pressed for my next screen, a custom transition like when apps are pressed from the homescreen and splash screen comes out of it.
I have tried transitionConfig in stackNavigator and used a custom transition with following code
let sharedElementTransition = (index, position,touchPosition, height) => {
const inputRange = [index - 1, index, index + 1];
const outputRange = [0.1, 1, 1];
const opacity = position.interpolate({
inputRange,
outputRange: [0, 1, 1],
});
const scale = position.interpolate({
inputRange:[0, 0.01, 0.99, 1],
outputRange:[0.3, 0.3, 1, 1],
});
const translateX = position.interpolate({
inputRange,
outputRange: [touchPosition.x, 0, 0]
})
const translateY = position.interpolate({
inputRange,
outputRange: [touchPosition.y, 0, 0]
})
return {
opacity,
transform: [
{scale},
{ translateX },
{ translateY },
]
};
};
The problem I am facing is scaling starts from middle of the screen , so if i pass the position as (237,16) top right corner of the screen , I can't get translateX and translateY to animate properly as the scale of the next screen is smaller the screen size (we are animating the next screen from the button pressed in the last screen) so its co-ordinate system doesn't have a point (237,16).
This is not a shared element animation problem, I also tried https://github.com/fram-x/FluidTransitions recommended in the react navigation docs, but under this one (from its example, there is no extensive documentation) my screens which are under FluidNavigator never seems to have a header.
I am using react-native-maps and want to gradually animate scale of active and inactive markers depending on the ScrollView x offset. (the below image describes it better)
My approach is to create new Animated.Value for each marker and Interpolate ScrollView offset for each marker:
//Container
constructor(props) {
super(props);
this.state = {
...
scale: [],
opacity: []
}
}
componentWillReceiveProps(props) {
if (props.places.length) {
const scale = [];
const opacity = [];
props.places.forEach((xx, ii) => {
this.state[`scale${ii}`] = new Animated.Value(0.5);
this.state[`scale${ii}`] = this._scrollX.interpolate({
inputRange: [vw * (ii - 1), vw * ii, vw * (ii + 1)],
outputRange: [0.5, 1, 0.5],
extrapolate: 'clamp'
});
scale.push(this.state[`scale${ii}`]);
this.state[`opacity${ii}`] = new Animated.Value(0);
this.state[`opacity${ii}`] = this._scrollX.interpolate({
inputRange: [vw * (ii - 1), vw * ii, vw * (ii + 1)],
outputRange: [0, 1, 0],
extrapolate: 'clamp'
});
opacity.push(this.state[`opacity${ii}`]);
});
this.setState({
scale,
opacity,
});
}
render() {
return (
<PlacesMap scale={this.state.scale} opacity={this.state.opacity} />
<ScrollView onScroll={this._onScroll} ... />
)
}
_onScroll = Animated.event(
[{ nativeEvent: { contentOffset: { x: this._scrollX }}}],
{ useNativeDriver: true }
);
//PlacesMap
....
render {
return (
<MapView.Animated>
{ places &&
places.map((place, ii) => {
return (
<PlaceMarker
key={place.id.toString()}
place={place}
scale={scale[ii]}
opacity={opacity[ii]}
/>
)
})
}
</MapView.Animated>
)}
Maybe there is a more elegant (performant) approach that I don't know, where I don't need to create new Animated.Value and Interpolators for each marker and with which I can get the same result?