【React Native】自定义倒计时组件CountdownView

4 阅读5分钟

React Native 倒计时组件 CountdownView 文档

一、组件简介

CountdownView 是一个功能灵活的 React Native 倒计时组件,支持天、小时、分钟、秒、毫秒多级时间显示,可自定义样式(字体颜色、背景渐变、尺寸等),并提供倒计时开始/结束的生命周期回调。适用于活动倒计时、秒杀场景、预约剩余时间展示等业务场景。


二、核心属性(Props)详解

属性名类型默认值说明
endTimenumber0倒计时结束时间戳(单位:毫秒)。若传入秒级时间戳,需手动乘以 1000(组件内部会自动处理 >1e10 的情况)。
startedbooleantrue是否启动倒计时(设为 false 可暂停)。
timeFontColorstring'#000000'时间数字的字体颜色。
timeFontSizenumber14时间数字的字体大小(单位:px)。
suffixTextstring':'时间单位后缀(如小时与分钟间的分隔符 :)。
suffixFontSizenumber14后缀文本的字体大小(单位:px)。
isShowDaybooleantrue是否显示天数部分。
isShowHourbooleantrue是否显示小时部分。
isShowMinutebooleantrue是否显示分钟部分。
isShowSecondbooleantrue是否显示秒部分。
isShowMillisecondbooleanfalse是否显示毫秒部分(开启后更新频率为 10ms)。
onStart() => void-倒计时开始时的回调函数。
onEnd() => void-倒计时结束(剩余时间 ≤0)时的回调函数。
styleStyleProp<ViewStyle>-组件外层容器的自定义样式(如整体边距、对齐方式)。
timeTextBgStyleStyleProp<TextStyle>-时间数字背景的文本样式(仅对 LinearGradient 背景生效,用于调整内边距等)。
dayStartBgColorstringtransparent天数部分背景渐变的起始颜色(与 dayEndBgColor 配合使用)。
dayEndBgColorstringtransparent天数部分背景渐变的结束颜色。
dayFontColorstring'#000000'天数数字的字体颜色(覆盖全局 timeFontColor)。
dayFontSizenumber14天数数字的字体大小(覆盖全局 timeFontSize)。
dayPrefixstring''天数前缀(如 剩余)。
daySuffixstring'天'天数后缀(如 )。
dayPrefixStyleStyleProp<TextStyle>-天数前缀的自定义样式。
daySuffixStyleStyleProp<TextStyle>-天数后缀的自定义样式。
dayBgStyleStyleProp<ViewStyle>-天数背景容器的自定义样式(如圆角、内边距)。
hourStyle/minuteStyle/secondStyle/millisecondStyleStyleProp<TextStyle>-小时/分钟/秒/毫秒数字的自定义样式(覆盖全局 timeTextBgStyle)。
hourSuffixStyle 等后缀样式StyleProp<TextStyle>-各时间单位后缀的自定义样式(覆盖全局 suffixStyle)。

三、使用示例

基础用法(默认显示所有时间单位)

import CountdownView from './CountdownView';

// 假设 24 小时后的时间戳(当前时间 + 24*3600*1000)
const endTime = Date.now() + 24 * 3600 * 1000;

const App = () => {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <CountdownView 
        endTime={endTime} 
        daySuffix="天" 
        suffixText=":" 
      />
    </View>
  );
};

自定义样式(隐藏秒,调整背景渐变)

<CountdownView
  endTime={endTime}
  isShowSecond={false} // 隐藏秒
  dayStartBgColor="#FF6B6B" // 天数背景渐变起始色
  dayEndBgColor="#FFE66D" // 天数背景渐变结束色
  hourStyle={{ fontWeight: 'bold' }} // 小时数字加粗
  suffixStyle={{ color: '#666' }} // 后缀颜色变灰
  timeFontSize={18} // 时间数字放大
/>

控制倒计时启停

const [isRunning, setIsRunning] = useState(true);

// 点击按钮暂停/恢复倒计时
const toggleCountdown = () => {
  setIsRunning(!isRunning);
};

return (
  <View>
    <CountdownView 
      endTime={endTime} 
      started={isRunning} 
      onStart={() => console.log('倒计时开始')}
      onEnd={() => console.log('倒计时结束!')}
    />
    <Button 
      title={isRunning ? '暂停' : '恢复'} 
      onPress={toggleCountdown} 
    />
  </View>
);

四、源码

import React, {useEffect, useState} from 'react';
import {
  StyleProp,
  StyleSheet,
  Text,
  TextStyle,
  View,
  ViewStyle,
} from 'react-native';
// @ts-ignore
import LinearGradient from 'react-native-linear-gradient';

const CountdownView = ({
  endTime = 0,
  started = true,
  timeFontColor = '#000000',
  timeStartBgColor = 'transparent',
  timeEndBgColor = 'transparent',
  dayStartBgColor,
  dayEndBgColor,
  timeFontSize = 14,
  suffixText = ':',
  suffixHour,
  suffixMinute,
  suffixSecond,
  suffixMillisecond,
  suffixFontSize = 14,
  dayFontColor = '#000000',
  dayFontSize = 14,
  dayPrefix = '',
  daySuffix = '天',
  isShowDay = true,
  isShowHour = true,
  isShowMinute = true,
  isShowSecond = true,
  isShowMillisecond = false,
  style,
  timeTextBgStyle,
  suffixStyle,
  dayPrefixStyle,
  dayStyle,
  dayBgStyle,
  daySuffixStyle,
  hourStyle,
  hourSuffixStyle,
  minuteStyle,
  minuteSuffixStyle,
  secondStyle,
  secondSuffixStyle,
  millisecondStyle,
  millisecondSuffixStyle,
  onStart,
  onEnd,
  updateTime,
}: {
  endTime: number;
  started?: boolean;
  timeFontColor?: string;
  timeStartBgColor?: string;
  timeEndBgColor?: string;
  dayStartBgColor?: string;
  dayEndBgColor?: string;
  timeFontSize?: number;
  timeWidth?: number;
  timeHeight?: number;
  timeTextAlign?: 'top' | 'bottom' | 'center';
  suffixText?: string;
  suffixHour?: string;
  suffixMinute?: string;
  suffixSecond?: string;
  suffixMillisecond?: string;
  style?: StyleProp<ViewStyle>;
  timeTextBgStyle?: StyleProp<TextStyle>;
  timeBgStyle?: StyleProp<ViewStyle>;
  timeSuffixTextStyle?: StyleProp<TextStyle>;
  timeSuffixBgStyle?: StyleProp<ViewStyle>;
  suffixFontSize?: number;
  suffixWidth?: number;
  suffixTextAlign?: 'top' | 'bottom' | 'center';
  dayFontColor?: string;
  dayFontSize?: number;
  dayWidth?: number;
  dayTextAlign?: 'top' | 'bottom' | 'center';
  dayPrefix?: string;
  daySuffix?: string;
  isShowDay?: boolean;
  isShowHour?: boolean;
  isShowMinute?: boolean;
  isShowSecond?: boolean;
  isShowTimeSuffix?: boolean;
  isShowTimePrefix?: boolean;
  isShowMillisecond?: boolean;
  dayPrefixStyle?: StyleProp<ViewStyle>;
  dayStyle?: StyleProp<ViewStyle>;
  suffixStyle?: StyleProp<TextStyle>;
  dayBgStyle?: StyleProp<ViewStyle>;
  daySuffixStyle?: StyleProp<ViewStyle>;
  hourStyle?: StyleProp<ViewStyle>;
  hourSuffixStyle?: StyleProp<ViewStyle>;
  minuteStyle?: StyleProp<ViewStyle>;
  minuteSuffixStyle?: StyleProp<ViewStyle>;
  secondStyle?: StyleProp<ViewStyle>;
  secondSuffixStyle?: StyleProp<ViewStyle>;
  millisecondStyle?: StyleProp<ViewStyle>;
  millisecondSuffixStyle?: StyleProp<ViewStyle>;
  onStart?: () => void;
  onEnd?: () => void;
  updateTime?: (time: {
    day: number;
    hour: number;
    minute: number;
    second: number;
    millisecond: number;
  }) => void;
}) => {
  const [state, setState] = useState({
    day: 0,
    hour: 0,
    minute: 0,
    second: 0,
    millisecond: 0,
    left: 0,
    intervalId: null,
  });

  // 计算剩余时间
  const calculateTimeLeft = () => {
    const realEndTime = endTime > 1e10 ? endTime : endTime * 1000;
    const left = realEndTime - Date.now();
    return left <= 0
      ? {left: 0, ...getDefaultTime()}
      : {
          left,
          day: Math.floor(left / (1000 * 24 * 60 * 60)),
          hour: Math.floor((left % (1000 * 24 * 60 * 60)) / (1000 * 60 * 60)),
          minute: Math.floor((left % (1000 * 60 * 60)) / (1000 * 60)),
          second: Math.floor((left % (1000 * 60)) / 1000),
          millisecond: left % 1000,
        };
  };

  // 初始化时间状态
  const getDefaultTime = () => ({
    day: 0,
    hour: 0,
    minute: 0,
    second: 0,
    millisecond: 0,
  });

  // 启动倒计时
  const startInterval = () => {
    stopInterval();
    onStart?.();
    const intervalId = setInterval(
      () => {
        const {left, day, hour, minute, second, millisecond} =
          calculateTimeLeft();
        setState(prev => ({
          ...prev,
          left,
          day,
          hour,
          minute,
          second,
          millisecond,
        }));
        if (left <= 0) {
          stopInterval();
          onEnd?.();
        }
      },
      isShowMillisecond ? 10 : 500,
    );
    // setState(prev => ({...prev, intervalId}));
  };

  // 停止倒计时
  const stopInterval = () => {
    if (state.intervalId) {
      clearInterval(state.intervalId);
    }
  };

  // 生命周期管理
  useEffect(() => {
    if (started) {
      startInterval();
    }
    return () => stopInterval(); // 清理副作用
  }, [started]);

  // 格式化数字为两位
  const formatNumber = (num, length = 2) =>
    num.toString().padStart(length, '0');

  // 构建单个时间单元
  const renderTimeUnit = (value, suffix, show, aStyle, aSuffixStyle) => {
    if (!show) return null;
    return (
      <View style={[styles.timeContainer]}>
        <LinearGradient
          style={timeTextBgStyle}
          colors={[timeStartBgColor, timeEndBgColor]}
          start={{x: 0, y: 0}}
          end={{x: 1, y: 0}}>
          <Text
            style={[
              styles.timeText,
              {color: timeFontColor, fontSize: timeFontSize},
              aStyle || {},
            ]}>
            {formatNumber(value)}
          </Text>
        </LinearGradient>
        {suffix && (
          <Text
            style={[
              styles.suffixText,
              {fontSize: suffixFontSize},
              aSuffixStyle || {},
              suffixStyle,
            ]}>
            {suffix}
          </Text>
        )}
      </View>
    );
  };

  return (
    <View
      style={[
        {
          flexDirection: 'row',
          alignItems: 'center',
        },
        style,
      ]}>
      {isShowDay && (
        <View style={styles.dayOut}>
          <Text
            style={[
              styles.dayPrefix,
              {fontSize: dayFontSize, color: dayFontColor},
              dayPrefixStyle,
            ]}>
            {dayPrefix}
          </Text>
          <LinearGradient
            style={dayBgStyle}
            colors={[dayStartBgColor, dayEndBgColor]}
            start={{x: 0, y: 0}}
            end={{x: 1, y: 0}}>
            <Text
              style={[
                styles.dayValue,
                {fontSize: dayFontSize, color: dayFontColor},
                dayStyle,
              ]}>
              {formatNumber(state.day)}
            </Text>
          </LinearGradient>
          <Text
            style={[
              styles.daySuffix,
              {fontSize: dayFontSize, color: dayFontColor},
              daySuffixStyle,
            ]}>
            {daySuffix}
          </Text>
        </View>
      )}

      {isShowHour &&
        renderTimeUnit(
          state.hour,
          suffixHour ?? suffixText,
          isShowHour,
          hourStyle,
          hourSuffixStyle,
        )}

      {isShowMinute &&
        renderTimeUnit(
          state.minute,
          suffixMinute ?? suffixText,
          isShowMinute,
          minuteStyle,
          minuteSuffixStyle,
        )}

      {isShowSecond &&
        renderTimeUnit(
          state.second,
          suffixSecond,
          isShowSecond,
          secondStyle,
          secondSuffixStyle,
        )}

      {isShowMillisecond &&
        renderTimeUnit(
          state.millisecond,
          suffixMillisecond,
          isShowMillisecond,
          millisecondStyle,
          millisecondSuffixStyle,
        )}
    </View>
  );
};

// 样式定义
const styles = StyleSheet.create({
  timeContainer: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  timeText: {
    textAlign: 'center',
  },
  suffixText: {
    textAlign: 'center',
  },
  dayOut: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  dayPrefix: {
    textAlign: 'center',
  },
  dayValue: {
    textAlign: 'center',
  },
  daySuffix: {
    textAlign: 'center',
  },
});

export default CountdownView;