Skip to content

Commit 63ffbce

Browse files
authored
feat: Improved animation fidelity
- Use two different animations for entering/exiting - Removed useless styles - Fix janky Android elevation animation by setting `needsOffscreenAlphaCompositing` while animating (see facebook/react-native#23090)
2 parents bae5bb4 + a0186e6 commit 63ffbce

File tree

2 files changed

+103
-90
lines changed

2 files changed

+103
-90
lines changed

src/Container.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,7 @@ DialogContainer.defaultProps = {
9999
};
100100

101101
const styles = StyleSheet.create({
102-
modal: {
103-
flex: 1,
104-
marginLeft: 0,
105-
marginRight: 0,
106-
marginTop: 0,
107-
marginBottom: 0,
108-
},
109102
centeredView: {
110-
justifyContent: "center",
111-
alignItems: "center",
112103
marginTop: 22,
113104
},
114105
blur: {

src/Modal.js

Lines changed: 103 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,49 @@ import {
1212
} from "react-native";
1313

1414
const MODAL_ANIM_DURATION = 300;
15-
const MODAL_BACKDROP_OPACITY = 0.4;
16-
17-
const IOS_CONTENT_ANIMATION = {
18-
from: { opacity: 0, scale: 1.2 },
19-
0.5: { opacity: 1, scale: 1.1 },
20-
to: { opacity: 1, scale: 1 },
21-
};
22-
23-
const ANDROID_CONTENT_ANIMATION = {
24-
from: { opacity: 0, scale: 0.3 },
25-
0.5: { opacity: 1, scale: 0.7 },
26-
to: { opacity: 1, scale: 1 },
27-
};
28-
29-
const OTHER_OS_CONTENT_ANIMATION = {
30-
from: { opacity: 0, scale: 0.3 },
31-
0.5: { opacity: 1, scale: 0.7 },
32-
to: { opacity: 1, scale: 1 },
33-
};
15+
const MODAL_BACKDROP_OPACITY = 0.3;
16+
17+
const CONTENT_ANIMATION_IN = Platform.select({
18+
ios: {
19+
opacity: {
20+
inputRange: [0, 1],
21+
outputRange: [0, 1],
22+
},
23+
scale: {
24+
inputRange: [0, 0.5, 1],
25+
outputRange: [1.2, 1.1, 1],
26+
},
27+
},
28+
android: {
29+
opacity: {
30+
inputRange: [0, 0.5, 1],
31+
outputRange: [0, 1, 1],
32+
},
33+
scale: {
34+
inputRange: [0, 1],
35+
outputRange: [0.3, 1],
36+
},
37+
},
38+
default: {
39+
opacity: {
40+
inputRange: [0, 0.5, 1],
41+
outputRange: [0, 1, 1],
42+
},
43+
scale: {
44+
inputRange: [0, 1],
45+
outputRange: [0.3, 1],
46+
},
47+
},
48+
});
49+
50+
const CONTENT_ANIMATION_OUT = Platform.select({
51+
default: {
52+
opacity: {
53+
inputRange: [0, 1],
54+
outputRange: [0, 1],
55+
},
56+
},
57+
});
3458

3559
export class Modal extends Component {
3660
static propTypes = {
@@ -48,6 +72,7 @@ export class Modal extends Component {
4872

4973
state = {
5074
visible: this.props.visible,
75+
currentAnimation: "none",
5176
deviceWidth: Dimensions.get("window").width,
5277
deviceHeight: Dimensions.get("window").height,
5378
};
@@ -94,27 +119,33 @@ export class Modal extends Component {
94119
};
95120

96121
show = () => {
97-
this.setState({ visible: true });
98-
Animated.timing(this.animVal, {
99-
easing: Easing.inOut(Easing.quad),
100-
// Using native driver in the modal makes the content flash
101-
useNativeDriver: false,
102-
duration: MODAL_ANIM_DURATION,
103-
toValue: 1,
104-
}).start();
122+
this.setState({ visible: true, currentAnimation: "in" }, () => {
123+
Animated.timing(this.animVal, {
124+
easing: Easing.inOut(Easing.quad),
125+
// Using native driver in the modal makes the content flash
126+
useNativeDriver: false,
127+
duration: MODAL_ANIM_DURATION,
128+
toValue: 1,
129+
}).start(() => {
130+
this.setState({ currentAnimation: "none" });
131+
});
132+
});
105133
};
106134

107135
hide = () => {
108-
Animated.timing(this.animVal, {
109-
easing: Easing.inOut(Easing.quad),
110-
// Using native driver in the modal makes the content flash
111-
useNativeDriver: false,
112-
duration: MODAL_ANIM_DURATION,
113-
toValue: 0,
114-
}).start(() => {
115-
if (this._isMounted) {
116-
this.setState({ visible: false }, this.props.onHide);
117-
}
136+
this.setState({ animationDirection: "out" }, () => {
137+
Animated.timing(this.animVal, {
138+
easing: Easing.inOut(Easing.quad),
139+
// Using native driver in the modal makes the content flash
140+
useNativeDriver: false,
141+
duration: MODAL_ANIM_DURATION,
142+
toValue: 0,
143+
}).start(() => {
144+
if (this._isMounted) {
145+
this.setState({ currentAnimation: "none" });
146+
this.setState({ visible: false }, this.props.onHide);
147+
}
148+
});
118149
});
119150
};
120151

@@ -125,7 +156,7 @@ export class Modal extends Component {
125156
contentStyle,
126157
...otherProps
127158
} = this.props;
128-
const { deviceHeight, deviceWidth, visible } = this.state;
159+
const { currentAnimation, deviceHeight, deviceWidth, visible } = this.state;
129160

130161
const backdropAnimatedStyle = {
131162
opacity: this.animVal.interpolate({
@@ -134,46 +165,38 @@ export class Modal extends Component {
134165
}),
135166
};
136167

137-
const contentAnimationSteps = Platform.select({
138-
ios: [
139-
IOS_CONTENT_ANIMATION.from,
140-
IOS_CONTENT_ANIMATION["0.5"],
141-
IOS_CONTENT_ANIMATION.to,
142-
],
143-
android: [
144-
ANDROID_CONTENT_ANIMATION.from,
145-
ANDROID_CONTENT_ANIMATION["0.5"],
146-
ANDROID_CONTENT_ANIMATION.to,
147-
],
148-
default: [
149-
OTHER_OS_CONTENT_ANIMATION.from,
150-
OTHER_OS_CONTENT_ANIMATION["0.5"],
151-
OTHER_OS_CONTENT_ANIMATION.to,
152-
],
153-
});
168+
const contentAnimatedStyle =
169+
currentAnimation === "in"
170+
? {
171+
opacity: this.animVal.interpolate({
172+
inputRange: CONTENT_ANIMATION_IN.opacity.inputRange,
173+
outputRange: CONTENT_ANIMATION_IN.opacity.outputRange,
174+
extrapolate: "clamp",
175+
}),
176+
transform: [
177+
{
178+
scale: this.animVal.interpolate({
179+
inputRange: CONTENT_ANIMATION_IN.scale.inputRange,
180+
outputRange: CONTENT_ANIMATION_IN.scale.outputRange,
181+
extrapolate: "clamp",
182+
}),
183+
},
184+
],
185+
}
186+
: {
187+
opacity: this.animVal.interpolate({
188+
inputRange: CONTENT_ANIMATION_OUT.opacity.inputRange,
189+
outputRange: CONTENT_ANIMATION_OUT.opacity.outputRange,
190+
extrapolate: "clamp",
191+
}),
192+
};
154193

155-
const contentAnimatedStyle = {
156-
opacity: this.animVal.interpolate({
157-
inputRange: [0, 0.5, 1],
158-
outputRange: contentAnimationSteps.map((x) => x.opacity),
159-
extrapolate: "clamp",
160-
}),
161-
transform: [
162-
{
163-
scale: this.animVal.interpolate({
164-
inputRange: [0, 0.5, 1],
165-
outputRange: contentAnimationSteps.map((x) => x.scale),
166-
extrapolate: "clamp",
167-
}),
168-
},
169-
],
170-
};
171194
return (
172195
<ReactNativeModal
173196
transparent
174197
animationType="none"
175-
visible={visible}
176198
{...otherProps}
199+
visible={visible}
177200
>
178201
<TouchableWithoutFeedback onPress={onBackdropPress}>
179202
<Animated.View
@@ -186,8 +209,15 @@ export class Modal extends Component {
186209
</TouchableWithoutFeedback>
187210
{visible && (
188211
<Animated.View
189-
style={[styles.content, contentAnimatedStyle, contentStyle]}
212+
style={[styles.content, contentAnimatedStyle]}
190213
pointerEvents="box-none"
214+
// Setting "needsOffscreenAlphaCompositing" solves a janky elevation
215+
// animation on android. We should set it only while animation
216+
// to avoid using more memory than needed.
217+
// See: https://github.com/facebook/react-native/issues/23090
218+
needsOffscreenAlphaCompositing={["in", "out"].includes(
219+
currentAnimation
220+
)}
191221
>
192222
{children}
193223
</Animated.View>
@@ -198,14 +228,6 @@ export class Modal extends Component {
198228
}
199229

200230
const styles = StyleSheet.create({
201-
container: {
202-
flex: 1,
203-
position: "absolute",
204-
top: 0,
205-
left: 0,
206-
right: 0,
207-
bottom: 0,
208-
},
209231
backdrop: {
210232
position: "absolute",
211233
top: 0,

0 commit comments

Comments
 (0)