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

React Native 倒计时组件 CountdownView 文档

一、组件简介

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


二、核心属性(Props)详解

属性名 类型 默认值 说明
endTime number 0 倒计时结束时间戳(单位:毫秒)。若传入秒级时间戳,需手动乘以 1000(组件内部会自动处理 >1e10 的情况)。
started boolean true 是否启动倒计时(设为 false 可暂停)。
timeFontColor string '#000000' 时间数字的字体颜色。
timeFontSize number 14 时间数字的字体大小(单位:px)。
suffixText string ':' 时间单位后缀(如小时与分钟间的分隔符 :)。
suffixFontSize number 14 后缀文本的字体大小(单位:px)。
isShowDay boolean true 是否显示天数部分。
isShowHour boolean true 是否显示小时部分。
isShowMinute boolean true 是否显示分钟部分。
isShowSecond boolean true 是否显示秒部分。
isShowMillisecond boolean false 是否显示毫秒部分(开启后更新频率为 10ms)。
onStart () => void - 倒计时开始时的回调函数。
onEnd () => void - 倒计时结束(剩余时间 ≤0)时的回调函数。
style StyleProp<ViewStyle> - 组件外层容器的自定义样式(如整体边距、对齐方式)。
timeTextBgStyle StyleProp<TextStyle> - 时间数字背景的文本样式(仅对 LinearGradient 背景生效,用于调整内边距等)。
dayStartBgColor string transparent 天数部分背景渐变的起始颜色(与 dayEndBgColor 配合使用)。
dayEndBgColor string transparent 天数部分背景渐变的结束颜色。
dayFontColor string '#000000' 天数数字的字体颜色(覆盖全局 timeFontColor)。
dayFontSize number 14 天数数字的字体大小(覆盖全局 timeFontSize)。
dayPrefix string '' 天数前缀(如 剩余)。
daySuffix string '天' 天数后缀(如 )。
dayPrefixStyle StyleProp<TextStyle> - 天数前缀的自定义样式。
daySuffixStyle StyleProp<TextStyle> - 天数后缀的自定义样式。
dayBgStyle StyleProp<ViewStyle> - 天数背景容器的自定义样式(如圆角、内边距)。
hourStyle/minuteStyle/secondStyle/millisecondStyle StyleProp<TextStyle> - 小时/分钟/秒/毫秒数字的自定义样式(覆盖全局 timeTextBgStyle)。
hourSuffixStyle 等后缀样式 StyleProp<TextStyle> - 各时间单位后缀的自定义样式(覆盖全局 suffixStyle)。

三、使用示例

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

tsx 复制代码
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>
  );
};

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

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

控制倒计时启停

tsx 复制代码
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>
);

四、源码

tsx 复制代码
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;
相关推荐
wen's7 小时前
React Native 0.79.4 中 [RCTView setColor:] 崩溃问题完整解决方案
javascript·react native·react.js
朝阳3917 小时前
ReactNative【实战系列教程】我的小红书 3 -- 自定义底栏Tab导航(含图片选择 expo-image-picker 的使用)
react native
朝阳3911 天前
React Native【实用教程】(含图标方案,常用第三库,动画,内置组件,内置Hooks,内置API,自定义组件,创建项目等)
react native
朝阳3911 天前
React Native【实战范例】同步跟随滚动
react native
朝阳3913 天前
React Native【详解】动画
react native
朝阳3914 天前
React Native【详解】内置 API
react native
xx240614 天前
React Native学习笔记
笔记·学习·react native
朝阳3914 天前
React Native【实战范例】弹跳动画菜单导航
react native
草明14 天前
解决: React Native iOS webview 空白页
react native·react.js·ios