项目地址: react-native-slideable-calendar-stripjavascript
演示地址: Calendar-Strip.mp4java
已经有了react-native-calendar-strip为什么还须要我这个日历控件?react
通常的甲方都会在一个页面上拖动拖动, 看到一个日历, 就想滑动切换上下周, 因为react-native-calendar-strip没有滑动特性, 而且在这个issue上讨论了很久, 并无可行的方案. 因而就萌发本身写一个日历插件的冲动.git
要开发一个日历控件, 最大的问题就是日期的转换, 虽然Moment.js
被不少人使用, 可是Moment使用大量的面向对象的API, 严重影响性能, 这也是在我尝试了Moment以后发现的, 因而就换上了datefns, 轻量级js日期控件, 彻底的函数式风格, 在日历控件中只需保存Date数据, 其余的日期比较/转换等操做都交给datefns.github
其次最头疼的问题是使用FlatList
展现数据时候, 如何动态生成新的数据.npm
在日历控件首次加载时候, 会生成5个周的日期, 将FlatList
滚动到中间一页(今天所在的周, 第2页, 从0开始). 当用户滑动到最后一页, 就须要再次生成2个周的数据拼接到尾部, 当用户滑动到第一页, 就须要生成2个周的数据拼接到数组首部, 而且这时候今天所在的页数也会变化, 因此要将今天所在的周的页数+2, 拼接到首部会影响FlatList数据展现, 会展现第一页数据, 此时的第一页数据是最新生成的日期, 因此要滚动到第二页(从0页开始).react-native
loadPreviousTwoWeek(originalDates) {
const originalFirstDate = originalDates[0];
const originalLastDate = originalDates[originalDates.length-1];
const firstDayOfPrevious2Week = subDays(originalFirstDate, 7 * 2);
// 生成两周以前的第一天到原始数据最后一天的日期
const eachDays = eachDay(firstDayOfPrevious2Week, originalLastDate);
this.setState(prevState => ({
datas: eachDays,
currentPage: prevState.currentPage+2,
pageOfToday: prevState.pageOfToday+2,
}), () => {
// 悄无声息滚动
this.scrollToPage(2, false);
});
}
复制代码
滑动到最后一页须要加载下两周日期:数组
// onEndReached={() => { this.onEndReached(); } }
// onEndReachedThreshold={0.01}
onEndReached() {
// console.log('onEndReached');
this.loadNextTwoWeek(this.state.datas);
}
loadNextTwoWeek(originalDates) {
const originalFirstDate = originalDates[0];
const originalLastDate = originalDates[originalDates.length-1];
const lastDayOfNext2Week = addDays(originalLastDate, 7 * 2);
const eachDays = eachDay(originalFirstDate, lastDayOfNext2Week);
this.setState({ datas: eachDays });
}
复制代码
ScrollView
的onMomentumScrollEnd
属性监听页数变化, 记录今天所在周的页数和当前展现的页数bash
// onMomentumScrollEnd={this.momentumEnd}
// scrollEventThrottle={500}
momentumEnd = (event) => {
const firstDayInCalendar = this.state.datas ? this.state.datas[0] : new Date();
// 从第一天到今天一共多少天
const daysBeforeToday = differenceInDays(firstDayInCalendar, new Date());
// ~~向下取整, 第一天到今天一共几周, 也就是今天所在周所在的页数
const pageOfToday = ~~(Math.abs(daysBeforeToday / 7));
const screenWidth = event.nativeEvent.layoutMeasurement.width;
// 经过offset来获取当前所在页数
const currentPage = event.nativeEvent.contentOffset.x / screenWidth;
// 记录今天所在周页数, 当前展现周的页数, 今天所在周是否被展现
this.setState({
pageOfToday,
currentPage,
isTodayVisible: currentPage === pageOfToday,
});
// 若是滑动到第一页了就须要加载以前两周数据
if (event.nativeEvent.contentOffset.x < width) {
this.loadPreviousTwoWeek(this.state.datas);
}
}
复制代码
最棘手的问题是用户点击了日历以外的一个button, 跳转到日历上指定的一天.ide
currentPageDatesIncludes = (date) => {
const { currentPage } = this.state;
const currentPageDates = this.state.datas.slice(7*currentPage, 7*(currentPage+1));
// dont use currentPageDates.includes(date); because can't compare Date in it
return !!currentPageDates.find(d => isSameDay(d, date));
}
复制代码
直接设置选中日期为指定日期.
const sameDay = (d) => isSameDay(d, nextSelectedDate);
if (this.state.datas.find(sameDay)) {
let selectedIndex = this.state.datas.findIndex(sameDay);
if (selectedIndex === -1) selectedIndex = this.state.pageOfToday; // in case not find
const selectedPage = ~~(selectedIndex / 7);
this.scrollToPage(selectedPage);
}
复制代码
找到指定日期所在周的页数, 滚动过去.
if (isFuture(nextSelectedDate)) {
const head = this.state.datas[0];
const tail = endOfWeek(nextSelectedDate);
const days = eachDay(head, tail);
this.setState({
datas: days,
isTodayVisible: false,
}, () => {
const page = ~~(days.length/7 - 1);
// to last page
this.scrollToPage(page);
});
} else {
const head = startOfWeek(nextSelectedDate);
const tail = this.state.datas[this.state.datas.length - 1];
const days = eachDay(head, tail);
this.setState({
datas: days,
isTodayVisible: false,
}, () => {
// to first page
this.scrollToPage(0);
});
}
复制代码
若是是将来某一天, 那么生成那天所在周的周六到当前日期控件全部日期的第一天之间的全部日期, 找到最后一页, 滚动过去.
若是是以前某一天, 那么生成那天所在周的周日(第一天)到当前日期控件全部日期的最后一天之间的全部日期, 滚动到第一页.
关于 pageOfToday
和 currentPage
交给 momentumEnd()
自动处理.
滚动到页方法是利用 FlatList
的 scrollToIndex
实现:
scrollToPage = (page, animated=true) => {
this._calendar.scrollToIndex({ animated, index: 7 * page });
}
复制代码
下滑手势:
componentWillMount() {
const touchThreshold = 50;
const speedThreshold = 0.2;
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
const { dy, vy } = gestureState;
// 滑动距离大雨50, 而且滑动速度大于0.2, 有效下滑
if (dy > touchThreshold && vy > speedThreshold) {
const { onSwipeDown } = this.props;
onSwipeDown && onSwipeDown();
}
return false;
},
onPanResponderRelease: () => {},
});
}
// 最外层 <View {...this._panResponder.panHandlers}>
复制代码
其余:
ChineseLunar
来转换中国农历.isTodayVisible
为false时在日历Header上展现一个 今
button今
跳转到今天所在周的页数this.state = {
datas: this.getInitialDates(), // 保存全部日期,
isTodayVisible: true, // 今天所在周是否在展现
pageOfToday: 2, // 今天在日历的第几页, 从0开始
currentPage: 2, // 当前是日历的第几页, 从0开始
};
复制代码
Wed May 16 2018 00:00:00 GMT+0800 (CST)
CalendarStrip.propTypes = {
selectedDate: PropTypes.object.isRequired,
onPressDate: PropTypes.func,
onPressGoToday: PropTypes.func,
markedDate: PropTypes.array,
onSwipeDown: PropTypes.func,
};
复制代码
PS. 使用datefns另外一个好处是, 当传给控件
markedDate = ['2018-01-01', '2018-05-01', '2018-06-01']
复制代码
也是支持的, 没必要须传一个Date格式的日期.