import React, { useMemo, useState, useRef, useEffect } from 'react'; import {View, Text, StyleSheet, BackHandler} from 'react-native'; import Animated, {Extrapolate, spring} from 'react-native-reanimated'; import {PanGestureHandler, TouchableOpacity, State} from 'react-native-gesture-handler'; import {onGestureEvent, snapPoint} from 'react-native-redash'; import {viewportHeight, IS_ANDROID} from './helper'; import {getParams} from './paramsManager'; import styles, {_top, _scale, _scaleBehind} from './styles'; const {lessOrEq, lessThan, greaterOrEq, neq, interpolate, Value, useCode, cond, set, eq, block, and, not, call, add, Clock, clockRunning, startClock, stopClock} = Animated; export type ModalNavigation = { params: any; setParams: (params: any) => void; close: () => void; gestureHandler: { onHandlerStateChange: (...args: any[]) => void; onGestureEvent: (...args: any[]) => void; }; onModalDidFocus: (callback: () => void) => (() => void); } export type LastModalRef = {closeLast: () => void} type Props = { name: string, title: string | ((params: any) => string); transition: Animated.Value; index: number; // the index layer of the modal component: any; isLast: boolean; closeRef: React.MutableRefObject<(name: string) => void>; forwardRef?: (ref: LastModalRef) => void; hideHeaderButton?: boolean; hideHeader?: boolean; ready: boolean; } function Modal(props: Props){ const {ready, title, transition, index, component: Component, name, isLast, closeRef, forwardRef, hideHeaderButton, hideHeader} = props; // modal params const [params, setParams] = useState(getParams(name)); // first open const firstOpen: Animated.Value<0 | 1> = useMemo(() => new Value(1), [ready]); // did focus animated value const didFocusV: Animated.Value<0 | 1> = useMemo(() => new Value(0), []); // did focus callback ref const didFocusCallbackRef = useRef<(() => void) | null>(); // on closing the modal const onClosing: Animated.Value<0 | 1> = useMemo(() => new Value(0), []); // gesture relevant const translationY = useMemo(() => new Value(0), []); const velocityY = useMemo(() => new Value(0), []); const state = useMemo(() => new Value(State.UNDETERMINED), []); const gestureHandler = useMemo(() => onGestureEvent({state, translationY, velocityY}), []); // spring state const springState: Animated.SpringState = useMemo(() => ({ finished: new Value(0), velocity: new Value(0), position: new Value(viewportHeight), time: new Value(0), }), []); // modal translate y const translateY = useMemo(() => new Value(viewportHeight), []); const scale = useMemo(() => interpolate(transition, { inputRange: [index + 1, index + 2, index + 3], outputRange: [1, _scale, _scaleBehind], extrapolate: Extrapolate.CLAMP, }), [index]); const opacity = useMemo(() => interpolate(transition, { inputRange: [index + 1, index + 2, index + 3], outputRange: [0, 0.2, 0.4], extrapolate: Extrapolate.CLAMP, }), [index]); if(forwardRef && isLast){ forwardRef({ closeLast: () => onClosing.setValue(1) }) } // modals component params const modalNavigation: ModalNavigation = useMemo(() => ({ params, setParams, close: () => { if(isLast){ onClosing.setValue(1); }else{ console.warn(`you should close the last modal first!`); } }, gestureHandler, onModalDidFocus: (callback: () => void) => { didFocusCallbackRef.current = callback; return () => { didFocusCallbackRef.current = null; } } }), [name, isLast, params, gestureHandler]); useCode(() => { if(isLast && ready){ const openClock = new Clock(); const closeClock = new Clock(); const snapClock = new Clock(); const shouldToSnap = new Value(0); const canClose = new Value(0); const snapPointV = new Value(0); const springConfig = { toValue: new Value(_top), damping: new Value(200), mass: 1, stiffness: new Value(400), overshootClamping: false, restSpeedThreshold: 0.01, restDisplacementThreshold: 0.01, } return block([ cond( and(firstOpen, neq(state, State.ACTIVE)), [ cond(not(clockRunning(openClock)), [ set(springState.position, translateY), set(springState.time, 0), set(springState.finished, 0), set(springConfig.damping, 200), set(springConfig.stiffness, 400), startClock(openClock), ]), spring(openClock, springState, springConfig), set(translateY, springState.position), cond(springState.finished, [ stopClock(openClock), set(firstOpen, 0), ]), ] ), cond( and(onClosing, neq(state, State.ACTIVE)), [ cond(clockRunning(openClock), [ stopClock(openClock), set(firstOpen, 0), ]), cond(clockRunning(snapClock), [ stopClock(snapClock), set(shouldToSnap, 0), ]), cond(not(clockRunning(closeClock)), [ set(springState.finished, 0), set(springState.velocity, 0), set(springState.time, 0), set(springState.position, translateY), set(springConfig.toValue, viewportHeight), set(springConfig.damping, 6), set(springConfig.stiffness, 80), startClock(closeClock), ]), spring(closeClock, springState, springConfig), set(translateY, springState.position), cond(springState.finished, [ stopClock(closeClock), set(onClosing, 0), ]), ] ), cond(eq(state, State.ACTIVE), [ cond(not(shouldToSnap), set(shouldToSnap, 1)), cond(clockRunning(openClock), [ stopClock(openClock), set(firstOpen, 0), ]), cond(clockRunning(closeClock), [ stopClock(closeClock), set(onClosing, 0), ]), cond(clockRunning(snapClock), [ stopClock(snapClock), ]), set( translateY, interpolate(add(springState.position, translationY), { inputRange: [0, _top, viewportHeight], outputRange: [_top - 12, _top, viewportHeight], extrapolate: Extrapolate.CLAMP, }) ), ]), cond(and(eq(state, State.END), shouldToSnap), [ cond(not(clockRunning(snapClock)), [ set(springState.finished, 0), set(springState.time, 0), set(springState.velocity, velocityY), set(springState.position, translateY), set(snapPointV, snapPoint(translateY, velocityY, [_top, viewportHeight])), set(springConfig.toValue, snapPointV), cond( eq(snapPointV, viewportHeight), [ set(springConfig.damping, 6), set(springConfig.stiffness, 80), ], [ set(springConfig.damping, 200), set(springConfig.stiffness, 400), ] ), startClock(snapClock), ]), spring(snapClock, springState, springConfig), set(translateY, springState.position), cond(springState.finished, [ stopClock(snapClock), set(shouldToSnap, 0), ]), ]), set(transition, interpolate(translateY, { inputRange: [_top, viewportHeight], outputRange: [index + 1, index], extrapolate: Extrapolate.CLAMP, })), cond(lessThan(translateY, viewportHeight), set(canClose, 1)), cond( and(greaterOrEq(translateY, viewportHeight), canClose), [ stopClock(closeClock), stopClock(snapClock), set(onClosing, 0), set(shouldToSnap, 0), set(springState.finished, 1), set(springState.position, viewportHeight), call([], () => { closeRef.current(name); }) ] ), ]) }else{ return block([ set(translateY, interpolate(transition, { inputRange: [index, index + 1, index + 2], outputRange: [viewportHeight, _top, 0], extrapolate: Extrapolate.CLAMP, })) ]) } }, [isLast, index, name, ready, firstOpen]); useCode(() => block([ cond( and(lessOrEq(translateY, _top), not(didFocusV)), [ set(didFocusV, 1), call([], () => { if(didFocusCallbackRef.current){ didFocusCallbackRef.current(); } }) ] ) ]), []); useEffect((): any => { // add android back handler if(IS_ANDROID && isLast){ const handleBackButtonPressAndroid = () => { onClosing.setValue(1); return true; } BackHandler.addEventListener('hardwareBackPress', handleBackButtonPressAndroid); return () => BackHandler.removeEventListener('hardwareBackPress', handleBackButtonPressAndroid); } }, [isLast]); // useCode(() => block([ // call([state], (state) => console.log(state)) // ]), []); // console.log(`${name} update`); return ( {!hideHeader && ( {typeof title === 'string' ? title : title(params)} {!hideHeaderButton && ( { if(isLast){ onClosing.setValue(1); } }}> {'完成'} )} )} {useMemo(() => , [])} ) } export default React.memo(Modal)