項目地址: 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格式的日期.