Skyroc Native UI

CountDown

按格式展示剩余时间的倒计时组件

倒计时(CountDown)把一段毫秒时长渲染成可读的剩余时间。内部用 requestAnimationFrame 驱动,秒级模式下只在跨秒的那一帧才更新状态,空转的帧不会触发渲染;同时监听 AppState,从后台回到前台时按真实时钟校正剩余时间,而不是把挂起期间的时间"补跑"一遍。

import { CountDown } from '@skyroc/native-ui';

基础用法

time 是总时长(毫秒),默认 formatHH:mm:ss,挂载后自动开始。

CountDownBasic.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CountDownBasic = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">60 秒倒计时</Text>
        <CountDown time={60 * 1000} />
      </View>
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">零时长</Text>
        <CountDown
          autoStart={false}
          time={0}
        />
      </View>
    </View>
  );
};

export { CountDownBasic };

何时使用

  • 展示距某个时刻还剩多久:秒杀、限时优惠、订单支付倒计时。
  • 短时冷却:验证码重发、按钮防重复点击(见下方验证码场景)。
  • 只需要一个静态时长文本、不随时间变化时,直接用 Text 自己格式化即可。
  • 需要显示"已过去多久"的正计时,本组件不支持——它只向 0 递减,到 0 自动停止。

自定义格式

format 支持以下占位符,其余字符原样输出:

占位符含义补零
DD2 位
HH小时2 位
mm分钟2 位
ss2 位
SSS毫秒3 位
SS百分秒2 位
S十分秒1 位
CountDownFormat.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CountDownFormat = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">完整日期时间</Text>
        <CountDown
          format="DD 天 HH 时 mm 分 ss 秒"
          time={30 * 60 * 60 * 1000}
        />
      </View>
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">高位累加到分钟</Text>
        <CountDown
          format="mm:ss"
          time={90 * 1000}
        />
      </View>
    </View>
  );
};

export { CountDownFormat };

缺失的高位单位会累加到相邻的低位单位上formatmm:ss 时天和小时都并入分钟,formatss 时整段时长都用秒表示。这样任何单一单位的格式都能表示完整时长而不丢时间,代价是数值可能超出常规范围(90 秒不会显示成 01:30,就是 90)。

毫秒级

millisecond 打开后每帧都会更新状态(约 60 次/秒),只在确实需要毫秒精度时开启。不开启时 S / SS / SSS 仍能渲染,但只会在跨秒时刷新,看起来像是卡住。

CountDownMillisecond.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CountDownMillisecond = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">十分秒(S)</Text>
        <CountDown
          millisecond
          format="ss:S"
          time={10 * 1000}
        />
      </View>
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">百分秒(SS)</Text>
        <CountDown
          millisecond
          format="ss:SS"
          time={10 * 1000}
        />
      </View>
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">总毫秒数(SSS)</Text>
        <CountDown
          millisecond
          format="SSS 毫秒"
          time={10 * 1000}
        />
      </View>
    </View>
  );
};

export { CountDownMillisecond };

自定义样式

className 覆盖根容器,classNames 按 slot 细粒度覆盖,冲突时 className 优先级更高。

slot作用位置
root根容器 View
text默认渲染的时间文本,被 children 接管后不生效
CountDownStyles.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CountDownStyles = () => {
  return (
    <View className="gap-4 bg-background p-4">
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">根容器 className</Text>
        <CountDown
          className="items-center rounded-xl bg-secondary py-3"
          time={60 * 1000}
        />
      </View>
      <View className="gap-1">
        <Text className="text-sm text-muted-foreground">root / text slot</Text>
        <CountDown
          classNames={{
            root: 'items-center rounded-xl border border-primary py-3',
            text: 'text-2xl font-semibold text-primary'
          }}
          time={60 * 1000}
        />
      </View>
    </View>
  );
};

export { CountDownStyles };

自定义渲染

children 是一个接收 CurrentTime 的渲染函数,用于把时间拆成独立的数字块。它只替换文本内容,根容器照常渲染,因此 className / classNames.root 依旧生效。

CountDownCustomRender.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const CountDownCustomRender = () => {
  return (
    <View className="bg-background p-4">
      <CountDown
        className="flex-row items-center justify-center gap-2 rounded-xl bg-muted p-4"
        time={60 * 60 * 1000}
      >
        {current => (
          <>
            <Text className="min-w-10 rounded-lg bg-primary px-2 py-1 text-center text-primary-foreground">
              {current.hours}
            </Text>
            <Text className="text-primary">:</Text>
            <Text className="min-w-10 rounded-lg bg-primary px-2 py-1 text-center text-primary-foreground">
              {current.minutes}
            </Text>
            <Text className="text-primary">:</Text>
            <Text className="min-w-10 rounded-lg bg-primary px-2 py-1 text-center text-primary-foreground">
              {current.seconds}
            </Text>
          </>
        )}
      </CountDown>
    </View>
  );
};

export { CountDownCustomRender };

children 接管后 formatclassNames.text 都不再参与渲染,格式化完全由你自己决定。

手动控制

autoStart={false} 时挂载后不自动开始,交给 ref 驱动:

方法说明
start()开始或从暂停处继续;已在计时时是空操作
pause()暂停,保留当前剩余时间
reset()回到 time 的时长
reset(totalTime)重置为指定时长(毫秒)
CountDownManual.tsx
Loading…
import { Button, CountDown } from '@skyroc/native-ui';
import type { CountDownRef } from '@skyroc/native-ui';
import { useRef } from 'react';
import { View } from 'react-native';

const CountDownManual = () => {
  const manualRef = useRef<CountDownRef>(null);

  function handleReset() {
    manualRef.current?.reset();
  }

  function handleResetTo(seconds: number) {
    manualRef.current?.reset(seconds * 1000);
  }

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <CountDown
          autoStart={false}
          format="mm:ss"
          ref={manualRef}
          time={20 * 1000}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        <Button
          variant="tonal"
          onPress={() => manualRef.current?.start()}
        >
          开始
        </Button>
        <Button
          variant="tonal"
          onPress={() => manualRef.current?.pause()}
        >
          暂停
        </Button>
        <Button
          variant="outline"
          onPress={handleReset}
        >
          重置
        </Button>
        <Button
          variant="outline"
          onPress={() => handleResetTo(5)}
        >
          重置为 5 秒
        </Button>
      </View>
    </View>
  );
};

export { CountDownManual };

reset 之后是否自动重新开始由 autoStart 决定:autoStarttrue 时重置即重新跑,为 false 时停在起点等 start()

动态时长

计时过程中改变 time 会按新时长重新开始;改变时如果处于暂停状态,则停在新时长的起点。

CountDownDynamicTime.tsx
Loading…
import { Button, CountDown } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const DURATIONS = [10, 30, 60];

const CountDownDynamicTime = () => {
  const [duration, setDuration] = useState(30);

  return (
    <View className="bg-background p-4">
      <View className="mb-4">
        <CountDown
          format="mm:ss"
          time={duration * 1000}
        />
      </View>
      <View className="flex-row flex-wrap items-center gap-3">
        {DURATIONS.map(seconds => (
          <Button
            key={seconds}
            variant={seconds === duration ? 'solid' : 'tonal'}
            onPress={() => setDuration(seconds)}
          >
            {`${seconds} 秒`}
          </Button>
        ))}
      </View>
    </View>
  );
};

export { CountDownDynamicTime };

结束回调

剩余时间归零时先停止计时再触发 onFinish,每轮只触发一次;前后台切换时会取消旧的动画帧链再重启,不会因为补发的回调而重复触发。onChange 则在每次更新时触发——毫秒模式下这意味着每帧一次,回调里不要做重活。

CountDownFinish.tsx
Loading…
import { CountDown, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const CountDownFinish = () => {
  const [seconds, setSeconds] = useState(5);
  const [finishCount, setFinishCount] = useState(0);

  return (
    <View className="gap-2 bg-background p-4">
      <View className="flex-row items-center gap-2">
        <CountDown
          format="ss"
          time={5 * 1000}
          onChange={current => setSeconds(current.seconds)}
          onFinish={() => setFinishCount(prev => prev + 1)}
        />
        <Text className="text-sm text-muted-foreground">onChange:剩余 {seconds} 秒</Text>
      </View>
      <Text className="text-sm text-muted-foreground">onFinish:已触发 {finishCount} 次</Text>
    </View>
  );
};

export { CountDownFinish };

验证码场景

倒计时结束前禁用按钮,onFinish 里恢复可点,是最常见的用法。

CountDownSms.tsx
Loading…
import { Button, CountDown, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const SMS_SECONDS = 60;

const CountDownSms = () => {
  const [smsSending, setSmsSending] = useState(false);

  return (
    <View className="flex-row items-center gap-3 bg-background px-6 py-4">
      <Button
        disabled={smsSending}
        variant="solid"
        onPress={() => setSmsSending(true)}
      >
        {smsSending ? '重新发送' : '发送验证码'}
      </Button>
      {smsSending ? (
        <CountDown
          time={SMS_SECONDS * 1000}
          onFinish={() => setSmsSending(false)}
        >
          {current => <Text color="muted">{current.seconds} 秒后可重发</Text>}
        </CountDown>
      ) : null}
    </View>
  );
};

export { CountDownSms };

API

CountDown

属性说明类型默认值
time倒计时总时长(毫秒),计时过程中改变会按新时长重新开始number0
format时间格式,支持 DD / HH / mm / ss / S / SS / SSSstring'HH:mm:ss'
autoStart是否挂载后自动开始,同时决定 reset() 之后是否自动重新开始booleantrue
millisecond毫秒级渲染,开启后每帧更新(约 60 次/秒)booleanfalse
children自定义渲染函数,接收当前时间对象;根容器仍照常渲染(current: CurrentTime) => ReactNode-
onChange每次更新时触发(current: CurrentTime) => void-
onFinish倒计时归零时触发,每轮只触发一次() => void-
className根容器类名string-
classNames各 slot 的类名覆盖;children 接管渲染时 text slot 不再生效SlotClassNames<CountDownSlots>-
ref命令式控制,暴露 start / pause / resetRef<CountDownRef>-

useCountDown

组件内部的计时逻辑同样对外导出,用于完全自定义渲染容器的场景:

import { useCountDown } from '@skyroc/native-ui';

const { current, pause, reset, start } = useCountDown({
  autoStart: true,
  millisecond: false,
  onChange: current => {},
  onFinish: () => {},
  time: 60 * 1000
});

入参与同名的组件属性含义一致(time 必填),返回值中的 currentCurrentTimestart / pause / resetCountDownRef 的三个方法完全相同。

类型

import type { CountDownProps, CountDownRef, CountDownSlots, CurrentTime } from '@skyroc/native-ui';

CountDownSlots

可通过 classNames 覆盖的 slot 名称。

'root' | 'text'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 CountDownSlots。

Partial<Record<Slots, string>>

CurrentTime

剩余时间对象,传给 children 与 onChange。各单位已按天/时/分/秒拆分,不随 format 变化。

字段类型说明
daysnumber天数。
hoursnumber小时(0-23)。
minutesnumber分钟(0-59)。
secondsnumber秒(0-59)。
millisecondsnumber毫秒(0-999)。
totalnumber总剩余毫秒数。

CountDownRef

CountDown 暴露的命令式方法。

字段类型说明
start() => void开始或继续计时,已在计时时为空操作。
pause() => void暂停并保留当前剩余时间。
reset(totalTime?: number) => void重置为指定时长,缺省回到 time;autoStart 为 true 时自动重新开始。