react-native-countdown RN的倒计时组件

概述

之所以会写这个小组件是因为学习RN的时候,跟着视频敲代码。 视频推荐了一款倒计时组件 因为长期没有维护(最后一次更新是一年前), 在我的RN环境下报错。

报错的原因大概是 React 把一个功能独立出去,要重新引进就可以了。 不过想着既然我已经看了源码找到问题,那么我何不自己造一个小轮子呢~

项目github

用法

需要手动引入,没有传到npm 懒得再维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {CountDownText} from 'react-native-sk-countdown';

<CountDownText
style={styles.cd}
countType='seconds' // 计时类型:seconds / date
auto={true} // 自动开始
afterEnd={() => {}} // 结束回调
timeLeft={10} // 正向计时 时间起点为0秒
step={-1} // 计时步长,以秒为单位,正数则为正计时,负数为倒计时
startText='获取验证码' // 开始的文本
endText='获取验证码' // 结束的文本
intervalText={(sec) => sec + '秒重新获取'} // 定时的文本回调
/>

<CountDownText // 倒计时
style={styles.cd}
countType='date' // 计时类型:seconds / date
auto={true} // 自动开始
afterEnd={() => {}} // 结束回调
timeLeft={10} // 正向计时 时间起点为0秒
step={-1} // 计时步长,以秒为单位,正数则为正计时,负数为倒计时
startText='' // 开始的文本
endText='' // 结束的文本
intervalText={(date, hour, min, sec) => date + '天' + hour + '时' + min + '分' + sec} // 定时的文本回调
/>

demo.gif

Prop

Prop Description Default
countType Countdown type, one of ‘seconds’ and ‘date’. None
auto Whether to start countdown right now. false
timeLeft Seconds lefted to countdown. None
step Number to increment in each step. -1
startText Text before countdown. None
endText Text after countdown. None
intervalText A function to reture a text during countdown. None
afterEnd A callback function after countdown. None

Methods

Method Description Params
start start countdown. None
end finish countdown. None

源码

主要有两个文件,一个是倒计时的逻辑,另一个是夹杂了RN的逻辑。

CountDown.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
export default class CountDown {

constructor(props) {
this.setData(props);
}

// 获取有多少秒
static getSeconds(time) {
let type = typeof time;
return (type === 'number' || type === 'string' && /^\d+$/.test(time))
? time
: new Date(time).getTime() / 1000;
}

// 位数补全
static ten(t) {
return t < 10? '0' + t: t;
}

// 设置数据
setData(props) {
this.countType = props.countType; // 支持两种计时方式,两个日期之间 && 秒数的倒计时
this.timerId = null; // 计时器
this.endTime = props.endTime; // 计时器结束时间
this.startTime = props.startTime; // 计时器开始时间
this.timeLeft = props.timeLeft; // 计时器剩余秒数, 区别于上面时间段的计时方式
this.timePassed = 0; // 正向为累计时间,反向为剩余时间
this.onInterval = props.onInterval; // 定时的回调
this.onEnd = props.onEnd; // 结束的回调
this.step = props.step; // 计时步长,以秒为单位,正数为正计时,负数为倒计时
this.counter = 0; // 累加器 TODO 疑问

// 数据校验
if (!this.countType) {
throw new Error('必须传入一个 countType: seconds || date');
}

if (
(this.timeLeft && (this.endTime || this.startTime)) ||
(!this.timeLeft && !(this.endTime || this.startTime))
) {
throw new Error('必须传入一个时间段 [timeLeft] [startTime] [endTime]');
}

if (!this.timeLeft && typeof this.startTime === 'undefined') {
this.startTime = Date.now()/ 1000;
}

if (!this.timeLeft) {
this.timeLeft = Math.floor(CountDown.getSeconds(this.endTime) - CountDown.getSeconds(this.startTime));
}

this.refreshTime(true);
}

// 周期启动更新时间
auto() {

this.timerId = setTimeout(()=> {
// 倒计时到0停止计时
if (this.timePassed <= 0 && this.step < 0) return this.end();

this.refreshTime(true);

}, 1000 * Math.abs(this.step)); // 时间间隔为整数, 对step求绝对值

}

refreshTime(isStart) {

this.timePassed = (this.timeLeft * 1000 + this.step * 1000 * this.counter++) / 1000;

if (this.countType === 'date') {

let _timePassed = this.timePassed,
second = CountDown.ten(_timePassed % 60);
_timePassed = parseInt(_timePassed / 60);
let minute = CountDown.ten(_timePassed % 60);
_timePassed = parseInt(_timePassed / 60);
let hour = CountDown.ten(_timePassed % 24);
_timePassed = CountDown.ten(parseInt(_timePassed / 24));
this.onInterval(_timePassed,hour,minute, second);

} else if (this.countType === 'seconds') {

this.onInterval(this.timePassed);

}

isStart && this.auto(); // 是否开始计时

}

// 开始计时
start() {
clearTimeout(this.timerId);
this.refreshTime(true);
}

// 结束: 没有清空计数 + 停止计时
end() {
clearTimeout(this.timerId);
this.onEnd(this.timeLeft);
}

reset() {
this.counter = 0;
clearTimeout(this.timerId);
this.refreshTime(false);
}

}

CountDownText.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import React, {Component} from 'react';
import {
Text
} from 'react-native';
import CountDown from './CountDown';


class CountDownText extends Component {

constructor(props) {
super(props);

this.state = {
text: this.props.startText,
};
}

counter= null;

static isTimeEquals(t1, t2) {
return Math.abs(t1 - t2) < 2;
}

componentWillReceiveProps(nextProps) {

let updating = true;

// 倒计时的情况
if (this.props.step === nextProps.step && this.props.step < 0) {
if (this.props.endTime) {
// 1. 按起始日期来计时
updating = !CountDownText.isTimeEquals(this.props.endTime, nextProps.endTime);
} else {
// 2. 按间隔秒数来计时
updating = !CountDownText.isTimeEquals(nextProps.timeLeft, this.counter.timePassed);
}
}


if (updating) {
// 重置: 清空计数 + 停止计时
this.counter.reset();

this.counter.setData(Object.assign({}, nextProps, {
onInterval: this.onInterval.bind(this),
onEnd: this.onEnd.bind(this),
}));

if (nextProps.auto) {
this.start();
}
}
}

componentDidMount() {

this.counter = new CountDown(Object.assign({}, this.props, {
onInterval: this.onInterval.bind(this),
onEnd: this.onEnd.bind(this),
}));

if (this.counter.timeLeft <= 0 && this.counter.step <= 0) {
return this.end();
}

if (this.props.auto) this.start();

}

componentWillUnmount() {
this.reset();
}

start() {
this.counter.start();
}

end() {
this.counter.end();
}

reset() {
this.counter.reset();
}

render() {
return (
<Text style={this.props.style}> {this.state.text} </Text>
)
}

getTimePassed() {
return this.counter.timePassed;
}

onInterval(...args) {
this.setState({text: this.props.intervalText.apply(null, args)})
}

onEnd(timePassed) {
this.setState({
text: this.props.endText,
});

this.props.afterEnd(timePassed);
}

}

CountDownText.defaultProps = {
countType: 'seconds',
onEnd: null,
timeLeft: 0,
step: -1,
startText: null,
intervalText: null,
endText: null,
auto: false,
afterEnd: ()=> {},
};

export default CountDownText;

后记

大量借鉴 https://github.com/shigebeyond/react-native-sk-countdown 项目

其实说白了只是在原来代码上优化改进,主要兼容新版的React-NativeReact 和用 ES6语法改写。