開源一個ReactNative日曆控件

項目地址: 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 });
  }
複製代碼

ScrollViewonMomentumScrollEnd屬性監聽頁數變化, 記錄今天所在周的頁數和當前展現的頁數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

  1. 指定日期正好在當前展現的一個周內
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));
  }
複製代碼

直接設置選中日期爲指定日期.

  1. 指定日期不在當前展現周內, 可是當前控件日期數據包含指定日期
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);
      }
複製代碼

找到指定日期所在周的頁數, 滾動過去.

  1. 指定日期不在當前展現周內, 而且當前控件日期數據不包含指定日期
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);
  });
}
複製代碼

若是是將來某一天, 那麼生成那天所在周的週六到當前日期控件全部日期的第一天之間的全部日期, 找到最後一頁, 滾動過去.

若是是以前某一天, 那麼生成那天所在周的週日(第一天)到當前日期控件全部日期的最後一天之間的全部日期, 滾動到第一頁.

關於 pageOfTodaycurrentPage 交給 momentumEnd() 自動處理.

滾動到頁方法是利用 FlatListscrollToIndex 實現:

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
  • 點擊 跳轉到今天所在周的頁數
  • 最終整個控件的 state 只有 :
this.state = {
  datas: this.getInitialDates(), // 保存全部日期,
  isTodayVisible: true, // 今天所在周是否在展現
  pageOfToday: 2, // 今天在日曆的第幾頁, 從0開始
  currentPage: 2, // 當前是日曆的第幾頁, 從0開始
};
複製代碼
  • 全部保存的日期都是 Date格式, 而且是0點 Wed May 16 2018 00:00:00 GMT+0800 (CST)
  • 控件所須要的props:
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格式的日期.

如何開源

1. 託管到GitHub

2. 發佈到npmjs

3. travis持續集成(jest測試)

相關文章
相關標籤/搜索