1import {timingConfig} from '@/constants/commonConst'; 2import {fontSizeConst} from '@/constants/uiConst'; 3import useColors from '@/hooks/useColors'; 4import rpx from '@/utils/rpx'; 5import {GlobalState} from '@/utils/stateMapper'; 6import {nanoid} from 'nanoid'; 7import React, {useCallback, useEffect} from 'react'; 8import {Pressable, StyleSheet, Text, View} from 'react-native'; 9import { 10 Directions, 11 Gesture, 12 GestureDetector, 13} from 'react-native-gesture-handler'; 14import Animated, { 15 cancelAnimation, 16 runOnJS, 17 useAnimatedStyle, 18 useSharedValue, 19 withDelay, 20 withTiming, 21} from 'react-native-reanimated'; 22import Icon from '@/components/base/icon.tsx'; 23 24export interface IToastConfig { 25 /** 类型 */ 26 type: 'success' | 'warn'; 27 /** 消息内容 */ 28 message?: string; 29 /** 行动点 */ 30 actionText?: string; 31 /** 行动点按钮行为 */ 32 onActionClick?: () => void; 33 /** 展示时间 */ 34 duration?: number; 35} 36 37type IToastConfigInner = IToastConfig & { 38 id: string; 39}; 40 41const toastQueue: IToastConfigInner[] = []; 42 43const fixedTop = rpx(250); 44 45const activeToastStore = new GlobalState<IToastConfigInner | null>(null); 46 47const typeConfig = { 48 success: { 49 name: 'check-circle', 50 color: '#457236', 51 }, 52 warn: { 53 name: 'exclamation-circle', 54 color: '#de7622', 55 }, 56} as const; 57 58export function ToastBaseComponent() { 59 const activeToast = activeToastStore.useValue(); 60 const colors = useColors(); 61 62 const toastAnim = useSharedValue(0); 63 64 const setNextToast = useCallback(() => { 65 activeToastStore.setValue(toastQueue.shift() || null); 66 }, []); 67 68 useEffect(() => { 69 if (activeToast) { 70 toastAnim.value = withTiming(1, timingConfig.animationSlow, () => { 71 toastAnim.value = withDelay( 72 activeToast.duration || 1200, 73 withTiming(0, timingConfig.animationSlow, finished => { 74 if (finished) { 75 runOnJS(setNextToast)(); 76 } 77 }), 78 ); 79 }); 80 } 81 }, [activeToast]); 82 83 function removeCurrentToast() { 84 if (toastAnim.value === 1) { 85 cancelAnimation(toastAnim); 86 toastAnim.value = withTiming( 87 0, 88 timingConfig.animationSlow, 89 finished => { 90 if (finished) { 91 runOnJS(setNextToast)(); 92 } 93 }, 94 ); 95 } 96 } 97 98 const flingGesture = Gesture.Fling() 99 .direction(Directions.UP) 100 .onEnd(() => { 101 removeCurrentToast(); 102 }) 103 .runOnJS(true); 104 105 const toastAnimStyle = useAnimatedStyle(() => { 106 return { 107 transform: [ 108 { 109 translateY: (toastAnim.value - 1) * fixedTop, 110 }, 111 ], 112 opacity: toastAnim.value, 113 }; 114 }); 115 116 return activeToast ? ( 117 <GestureDetector gesture={flingGesture}> 118 <View style={styles.container}> 119 <Animated.View 120 style={[ 121 styles.contentContainer, 122 { 123 backgroundColor: colors.backdrop, 124 shadowColor: colors.shadow, 125 }, 126 toastAnimStyle, 127 ]}> 128 <Icon 129 size={fontSizeConst.appbar} 130 name={typeConfig[activeToast.type].name} 131 color={typeConfig[activeToast.type].color} 132 /> 133 <Text 134 numberOfLines={1} 135 style={[styles.text, {color: colors.text}]}> 136 {activeToast.message} 137 </Text> 138 {activeToast.actionText && activeToast.onActionClick ? ( 139 <Pressable 140 style={[ 141 styles.actionTextContainer, 142 {backgroundColor: colors.primary}, 143 ]} 144 onPress={activeToast.onActionClick}> 145 <Text style={styles.actionText} numberOfLines={1}> 146 {activeToast.actionText} 147 </Text> 148 </Pressable> 149 ) : null} 150 </Animated.View> 151 </View> 152 </GestureDetector> 153 ) : null; 154} 155 156const styles = StyleSheet.create({ 157 container: { 158 position: 'absolute', 159 top: rpx(128), 160 width: '100%', 161 alignItems: 'center', 162 height: rpx(100), 163 zIndex: 20000, 164 }, 165 contentContainer: { 166 width: rpx(688), 167 height: '100%', 168 borderRadius: rpx(12), 169 backgroundColor: 'blue', 170 flexDirection: 'row', 171 alignItems: 'center', 172 paddingHorizontal: rpx(24), 173 shadowOffset: { 174 width: 0, 175 height: 2, 176 }, 177 shadowOpacity: 0.2, 178 shadowRadius: 1.41, 179 180 elevation: 2, 181 }, 182 text: { 183 fontSize: fontSizeConst.content, 184 includeFontPadding: false, 185 flex: 1, 186 marginLeft: rpx(24), 187 }, 188 actionText: { 189 fontSize: fontSizeConst.content, 190 includeFontPadding: false, 191 color: 'white', 192 }, 193 actionTextContainer: { 194 marginLeft: rpx(24), 195 width: rpx(120), 196 paddingHorizontal: rpx(12), 197 flexDirection: 'row', 198 alignItems: 'center', 199 justifyContent: 'center', 200 borderRadius: rpx(30), 201 height: rpx(58), 202 }, 203}); 204 205export function showToast(config: IToastConfig) { 206 const id = nanoid(); 207 const _config = { 208 ...config, 209 id, 210 }; 211 const activeToast = activeToastStore.getValue(); 212 if (!activeToast) { 213 activeToastStore.setValue(_config); 214 } else { 215 toastQueue.push(_config); 216 } 217 218 return id; 219} 220