Skip to content

Commit 039fd7a

Browse files
author
chj_damon
committed
fix: 修复Modal组件的动画bug
1 parent 61a7ae9 commit 039fd7a

File tree

3 files changed

+116
-50
lines changed

3 files changed

+116
-50
lines changed

.changeset/strange-ducks-fix.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@td-design/react-native': patch
3+
---
4+
5+
修复 Modal 弹出动画的 bug

packages/react-native/src/modal/Modal/index.tsx

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { useTheme } from '@shopify/restyle';
21
import React, { FC, PropsWithChildren } from 'react';
3-
import { Animated, StyleProp, StyleSheet, TouchableWithoutFeedback, ViewStyle } from 'react-native';
2+
import { StyleProp, StyleSheet, TouchableWithoutFeedback, ViewStyle } from 'react-native';
3+
import Animated from 'react-native-reanimated';
44
import { SafeAreaView } from 'react-native-safe-area-context';
55

66
import Box from '../../box';
77
import Portal from '../../portal';
8-
import { Theme } from '../../theme';
98
import useModal from './useModal';
109

1110
const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView);
11+
1212
export type ModalProps = PropsWithChildren<{
1313
/** 是否显示弹窗 */
1414
visible: boolean;
@@ -32,31 +32,17 @@ const Modal: FC<ModalProps> = ({
3232
position = 'bottom',
3333
bodyContainerStyle,
3434
}) => {
35-
const theme = useTheme<Theme>();
36-
const { rendered, opacity, wrapContainer, edges, hideModal } = useModal({ visible, onClose, position });
35+
const { rendered, animatedStyle, wrapContainer, edges, hideModal } = useModal({
36+
visible,
37+
onClose,
38+
position,
39+
maskVisible,
40+
});
3741

3842
if (!rendered) return null;
3943
return (
4044
<Portal>
41-
<AnimatedSafeAreaView
42-
style={[
43-
{
44-
zIndex: 99,
45-
flex: 1,
46-
backgroundColor: maskVisible ? theme.colors.mask : theme.colors.transparent,
47-
flexDirection: position === 'bottom' ? 'column-reverse' : 'column',
48-
},
49-
position === 'center'
50-
? {
51-
justifyContent: 'center',
52-
}
53-
: {},
54-
{
55-
opacity: opacity.current,
56-
},
57-
]}
58-
edges={edges}
59-
>
45+
<AnimatedSafeAreaView style={animatedStyle} edges={edges}>
6046
<Box backgroundColor="background" zIndex="99" style={[wrapContainer, bodyContainerStyle]}>
6147
{children}
6248
</Box>

packages/react-native/src/modal/Modal/useModal.ts

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,111 @@
1-
import { useLatest, useMemoizedFn, useSafeState } from '@td-design/rn-hooks';
1+
import { useTheme } from '@shopify/restyle';
2+
import { useLatest, useMemoizedFn, usePrevious, useSafeState } from '@td-design/rn-hooks';
23
import { useEffect, useMemo, useRef } from 'react';
3-
import { Animated, BackHandler, Easing } from 'react-native';
4+
import { BackHandler, NativeEventSubscription } from 'react-native';
5+
import { Easing, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
46
import { Edge, useSafeAreaInsets } from 'react-native-safe-area-context';
57

68
import type { ModalProps } from '.';
9+
import { Theme } from '../../theme';
710

8-
export default function useModal({ visible, onClose, position }: Pick<ModalProps, 'visible' | 'onClose' | 'position'>) {
9-
const insets = useSafeAreaInsets();
10-
const opacity = useRef(new Animated.Value(0));
11-
const [rendered, setRendered] = useSafeState(false);
11+
export default function useModal({
12+
visible,
13+
onClose,
14+
position,
15+
maskVisible,
16+
}: Pick<ModalProps, 'visible' | 'onClose' | 'position' | 'maskVisible'>) {
17+
const theme = useTheme<Theme>();
1218
const onCloseRef = useLatest(onClose);
19+
const insets = useSafeAreaInsets();
20+
const opacity = useSharedValue(0);
21+
22+
const [rendered, setRendered] = useSafeState(visible);
23+
const latestVisible = useLatest(visible);
24+
const previousVisible = usePrevious(visible);
25+
26+
useEffect(() => {
27+
if (visible && !rendered) {
28+
setRendered(true);
29+
}
30+
}, [visible, rendered]);
31+
32+
useEffect(() => {
33+
if (previousVisible !== latestVisible.current) {
34+
if (visible) {
35+
showModal();
36+
} else {
37+
hideModal();
38+
}
39+
}
40+
}, [visible]);
41+
42+
useEffect(() => {
43+
return removeListeners;
44+
}, []);
45+
46+
/**
47+
* 处理安卓返回事件
48+
*/
49+
const handleBack = useMemoizedFn(() => {
50+
hideModal();
51+
return true;
52+
});
53+
54+
const subscription = useRef<NativeEventSubscription | undefined>(undefined);
55+
56+
const removeListeners = () => {
57+
if (subscription.current?.remove) {
58+
subscription.current?.remove();
59+
} else {
60+
BackHandler.removeEventListener('hardwareBackPress', handleBack);
61+
}
62+
};
1363

1464
/**
1565
* 打开弹窗
1666
*/
1767
const showModal = useMemoizedFn(() => {
18-
Animated.timing(opacity.current, {
19-
toValue: 1,
68+
subscription.current?.remove();
69+
subscription.current = BackHandler.addEventListener('hardwareBackPress', handleBack);
70+
71+
opacity.value = withTiming(1, {
2072
duration: 400,
21-
easing: Easing.out(Easing.cubic),
22-
useNativeDriver: true,
23-
}).start();
73+
easing: Easing.in(Easing.cubic),
74+
});
2475
});
2576

2677
/**
2778
* 关闭弹窗
2879
*/
2980
const hideModal = useMemoizedFn(() => {
30-
Animated.timing(opacity.current, {
31-
toValue: 0,
32-
duration: 400,
33-
easing: Easing.out(Easing.cubic),
34-
useNativeDriver: true,
35-
}).start(finished => {
36-
if (!finished) return;
81+
removeListeners();
3782

38-
if (visible) {
39-
onCloseRef.current?.();
40-
showModal();
41-
} else {
42-
setRendered(false);
83+
opacity.value = withTiming(
84+
0,
85+
{
86+
duration: 400,
87+
easing: Easing.out(Easing.cubic),
88+
},
89+
finished => {
90+
runOnJS(finishCallback)(finished);
4391
}
44-
});
92+
);
4593
});
4694

95+
function finishCallback(finished?: boolean) {
96+
if (!finished) return;
97+
98+
if (visible && onCloseRef) {
99+
onCloseRef.current?.();
100+
}
101+
102+
if (latestVisible.current) {
103+
showModal();
104+
} else {
105+
setRendered(false);
106+
}
107+
}
108+
47109
useEffect(() => {
48110
if (visible && !rendered) {
49111
setRendered(true);
@@ -53,7 +115,6 @@ export default function useModal({ visible, onClose, position }: Pick<ModalProps
53115
} else if (rendered) {
54116
hideModal();
55117
}
56-
// eslint-disable-next-line react-hooks/exhaustive-deps
57118
}, [visible, rendered]);
58119

59120
useEffect(() => {
@@ -93,9 +154,23 @@ export default function useModal({ visible, onClose, position }: Pick<ModalProps
93154
}
94155
}, [insets.bottom, insets.top, position]);
95156

157+
const animatedStyle = useAnimatedStyle(() => {
158+
const style: any = {
159+
zIndex: 99,
160+
flex: 1,
161+
backgroundColor: maskVisible ? theme.colors.mask : theme.colors.transparent,
162+
flexDirection: position === 'bottom' ? 'column-reverse' : 'column',
163+
opacity: opacity.value,
164+
};
165+
if (position === 'center') {
166+
style.justifyContent = 'center';
167+
}
168+
return style;
169+
});
170+
96171
return {
97172
rendered,
98-
opacity,
173+
animatedStyle,
99174
wrapContainer,
100175
edges,
101176
hideModal,

0 commit comments

Comments
 (0)