import React, { useState, useRef, useEffect, ReactNode, forwardRef, FunctionComponent, useImperativeHandle } from 'react'; import { View, Text, StyleProp, ViewStyle } from 'react-native'; import toObj from '../utils/style-to-obj'; import fConStyle from '../utils/filter-container-style'; import { useConfig } from '../configprovider/configprovider'; import countdownStyles from './styles'; export interface CountDownProps { style?: StyleProp; paused: boolean; startTime: number; endTime: number; millisecond: boolean; format: string; autoStart: boolean; time: number; onEnd: () => void; onPaused: (restTime: number) => void; onRestart: (restTime: number) => void; onUpdate: (restTime: any) => void; children: ReactNode; } const defaultProps = { paused: false, startTime: Date.now(), endTime: Date.now(), millisecond: false, format: 'HH:mm:ss', autoStart: true, time: 0 } as CountDownProps; const InternalCountDown: FunctionComponent> = forwardRef((props, ref) => { const { locale } = useConfig(); const styles = countdownStyles(); const { paused, startTime, endTime, millisecond, format, autoStart, time, style, onEnd, onPaused, onRestart, onUpdate, children, ...rest } = { ...defaultProps, ...props }; const [restTimeStamp, setRestTime] = useState(0); const stateRef = useRef({ pauseTime: 0, curr: 0, isPaused: paused, isIninted: false, timer: 0, restTime: 0, // 倒计时剩余时间时间 counting: !paused && autoStart, // 是否处于倒计时中 handleEndTime: Date.now(), // 最终截止时间 diffTime: 0 // 设置了 startTime 时,与 date.now() 的差异 }); // 时间戳转换 或 获取当前时间的时间戳 const getTimeStamp = (timeStr?: string | number) => { if (!timeStr) return Date.now(); let t = timeStr; t = t > 0 ? +t : t.toString().replace(/-/g, '/'); return new Date(t).getTime(); }; // 倒计时 interval const initTime = () => { stateRef.current.handleEndTime = endTime; stateRef.current.diffTime = Date.now() - getTimeStamp(startTime); // 时间差 if (!stateRef.current.counting) stateRef.current.counting = true; tick(); }; const tick = () => { stateRef.current.timer = +setTimeout(() => { if (stateRef.current.counting) { const currentTime = Date.now() - stateRef.current.diffTime; const remainTime = Math.max( stateRef.current.handleEndTime - currentTime, 0 ); stateRef.current.restTime = remainTime; setRestTime(remainTime); if (!remainTime) { stateRef.current.counting = false; pause(); onEnd && onEnd(); } if (remainTime > 0) { tick(); } } }); }; // 将倒计时剩余时间格式化 参数: t 时间戳 type custom 自定义类型 const formatRemainTime = (t: number, type?: string) => { const ts = t; const rest = { d: 0, h: 0, m: 0, s: 0, ms: 0 }; const SECOND = 1000; const MINUTE = 60 * SECOND; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; if (ts > 0) { rest.d = ts >= SECOND ? Math.floor(ts / DAY) : 0; rest.h = Math.floor((ts % DAY) / HOUR); rest.m = Math.floor((ts % HOUR) / MINUTE); rest.s = Math.floor((ts % MINUTE) / SECOND); rest.ms = Math.floor(ts % SECOND); } return type === 'custom' ? rest : parseFormat({ ...rest }); }; const parseFormat = (time: { d: number; h: number; m: number; s: number; ms: number; }) => { const { d } = time; let { h, m, s, ms } = time; let formatCache = format; if (formatCache.includes('DD')) { formatCache = formatCache.replace('DD', padZero(d)); } else { h += Number(d) * 24; } if (formatCache.includes('HH')) { formatCache = formatCache.replace('HH', padZero(h)); } else { m += Number(h) * 60; } if (formatCache.includes('mm')) { formatCache = formatCache.replace('mm', padZero(m)); } else { s += Number(m) * 60; } if (formatCache.includes('ss')) { formatCache = formatCache.replace('ss', padZero(s)); } else { ms += Number(s) * 1000; } if (formatCache.includes('S')) { const msC = padZero(ms, 3).toString(); if (formatCache.includes('SSS')) { formatCache = formatCache.replace('SSS', msC); } else if (formatCache.includes('SS')) { formatCache = formatCache.replace('SS', msC.slice(0, 2)); } else if (formatCache.includes('S')) { formatCache = formatCache.replace('SS', msC.slice(0, 1)); } } return formatCache; }; const padZero = (num: number | string, length = 2) => { num += ''; while ((num as string).length < length) { num = `0${num}`; } return num.toString(); }; // 暂定 const pause = () => { // cancelAnimationFrame(stateRef.current.timer) clearTimeout(stateRef.current.timer); stateRef.current.counting = false; onPaused && onPaused(stateRef.current.restTime); }; useImperativeHandle(ref, () => ({ start: () => { if (!stateRef.current.counting && !autoStart) { stateRef.current.counting = true; stateRef.current.handleEndTime = Date.now() + Number(stateRef.current.restTime); tick(); onRestart && onRestart(stateRef.current.restTime); } }, pause: () => { // cancelAnimationFrame(stateRef.current.timer) clearTimeout(stateRef.current.timer); stateRef.current.counting = false; onPaused && onPaused(stateRef.current.restTime); }, reset: () => { if (!autoStart) { pause(); stateRef.current.restTime = time; setRestTime(time); } } })); // 监听值变更 useEffect(() => { const tranTime = formatRemainTime(stateRef.current.restTime, 'custom'); onUpdate && onUpdate(tranTime); }, [restTimeStamp]); // 监听暂停 useEffect(() => { if (stateRef.current.isIninted) { if (paused) { if (stateRef.current.counting) { pause(); } } else { if (!stateRef.current.counting) { stateRef.current.counting = true; stateRef.current.handleEndTime = Date.now() + Number(stateRef.current.restTime); tick(); } onRestart && onRestart(stateRef.current.restTime); } } }, [paused]); // 监听开始结束时间变更 // useEffect(() => { // if (stateRef.current.isIninted) { // initTime() // } // }, [endTime, startTime]) // 初始化 useEffect(() => { if (autoStart) { initTime(); } else { stateRef.current.restTime = time; setRestTime(time); } if (!stateRef.current.isIninted) { stateRef.current.isIninted = true; } return componentWillUnmount; }, []); const componentWillUnmount = () => { clearTimeout(stateRef.current.timer); clearInterval(stateRef.current.timer); }; const renderTime = (() => { return formatRemainTime(stateRef.current.restTime); })(); const wrapStyle = [ styles.container, fConStyle(toObj(style || {})) ]; return ( { children || ( {renderTime} ) } ); }); export const CountDown = InternalCountDown; CountDown.defaultProps = defaultProps; CountDown.displayName = 'NutCountDown';