Here’s a still shot of the calendar, and a few more in motion:













Project address: github.com/Othershe/Ca…

The following is to record some of the main points in the process of writing calendar controls:

I. Main functions

  • 1. Support the Lunar calendar, solar terms and common holidays
  • 2. Set the date range. The maximum date range supported by default is 1900.1 to 2049.12.
  • 3. Select the date setting by default
  • 4, single selection, multiple selection
  • 5. Skip to the specified date
  • 6. Customize the appearance of the date through custom properties, as well as the simple layout configuration of the date Item

    Ii. Basic structure

    The calendar control we want to implement uses ViewPager as the main frame, CalendarView inherits ViewPager, so it naturally has the function of sliding left and right and caching. At present, we set the operation of sliding calendar left and right to switch months. The display of each month is realized by custom ViewGroup, that is, our MonthView. The date in the month is the View resolved by layout. Depending on the month, each MonthView may contain 6 x 7 or 5 x 7 date views. Since binding data to ViewPager requires PagerAdapter, we inherit PagerAdapter and extend CalendarPagerAdapter. To complete the initialization of MonthView and the binding of date data.

    Three, calculate the date data of each MonthView to be filled

    As shown in the screenshot above, the date data of each MonthView should consist of the last 0 to 6 days of the previous month, the number of days of the current month, and the first 0 to 6 days of the next month. The first day of the month is the day of the week.

    public static int getFirstWeekOfMonth(int year.int month) {
          Calendar calendar = Calendar.getInstance();
          calendar.set(year.month.1);
          return calendar.get(Calendar.DAY_OF_WEEK) - 1;
      }Copy the code

    Return 0 for Sunday and 1 to 6 for Monday to Saturday. As shown in the screenshot above, the first day of May 2017 is Monday: week = getFirstWeekOfMonth (2017, 5-1).

    for (int i = 0; i < week; i++) {ld = number of days last month - week +1 + i;
          }Copy the code

    If the number of days in a MonthView + Week is divisible by 7, do not include the date of MonthView. If the number of days in a MonthView + Week is divisible by 7, calculate the date of MonthView. The pseudo-code is as follows:

    for (int i = 0; i < 7* Number of rows displayed - days of the month - week;i++) {
              nd = i + 1;
          }Copy the code

    In this way, the required date data is calculated, and the detailed algorithm can refer to the source code.

    Count the total number of pages in the calendar

    The total number of pages should be derived from the starting date of the calendar, which essentially determines the total number of pages in the ViewPager to make it easier to understand. It can be calculated as follows:

    count = (dateEnd[0] - dateStart[0]) * 12 + dateEnd[1] - dateStart[1] + 1Copy the code

    DateStart and dateEnd are arrays containing the start and end dates of the calendar. This count is also required by CalendarPagerAdapter.

    5. Calculate the date by position

    The PagerAdapter has a instantiateItem() method:

    public Object instantiateItem(ViewGroup container, int position) {
          return instantiateItem((View) container, position);
      }Copy the code

    “To create each page of ViewPager, so each page of calendar is also created here, that is, MonthView. The key point here is to calculate the corresponding month of each page of calendar according to the positon parameter, and then calculate the date data required by the current MonthView. How to calculate the date from position?

    public static int[] positionToDate(int position, int startY, int startM) {
          int year = position / 12 + startY;
          int month = position % 12 + startM;
    
          if (month > 12) {
              month = month % 12;
              year = year + 1;
          }
    
          return new int[] {year.month};
      }Copy the code

    StartY and startM indicate the actual date of the calendar. Once you have the corresponding month and year, you can calculate the date data in the same way as in point 2 and populate it into the MothView.

    Six, MothView

    “MonthView” as mentioned earlier, “MonthView” inherits ViewGroup, that is, each page of the calendar. When receiving the date data, construct the corresponding date View in “MonthView”. Then add the View to “MonthView”. Finally, determine the final size and position of each View by onMeasure and onLayout. Here the basic conditions for executing a ViewPager are met and the MothView is initialized in the instantiateItem() method mentioned above:

    public Object instantiateItem(ViewGroup container.int position) {
          MonthView view = new MonthView(containerGetContext ());// Calculate the corresponding year and month according to position
          int[] date = CalendarUtil.positionToDate(position, dateStart[0], dateStart[1]);
          view.setDateList(CalendarUtil.getMonthDate(date[0].date[1]), SolarUtil.getMonthDays(date[0].date[1]));
          container.addView(view);
    
          return view;
      }Copy the code

    Here only the core code is reserved. When the calendar switches months, it will automatically calculate the date data of the corresponding month according to position, and then transfer it to MonthView. Finally, it adds MonthView to ViewPager.

    7. Switch Months Select the date

    According to the current setting, when you select a certain day of the current month, and then switch months, the last selected date will be found in the new month, and marked as selected state, if not, the last day of the new month will be selected. In fact, the logic is very simple, the key is how to find the corresponding date in the new month and select. First, record the date selected last time, because the ViewPager caches two pages by default, plus the current page altogether three pages, save three pages in CalendarPagerAdapter according to position, when the ViewPager switches to a page, the following callback will be executed:

    addOnPageChangeListener(new SimpleOnPageChangeListener() {
              @Override
              public void onPageSelected(int position) {}});Copy the code

    OnPageSelected (int Position) MonthView onPageSelected(int Position) MonthView onPageSelected(int Position) onPageSelected(int Position) MonthView

    Eight, multi-select

    An ideal alternative function should be in the current month after the date to select multiple, switch to the other month, after the return to a selected date in still can mark the selected date, because the ViewPager have three pages by default cache, so in the current month to switch to the last month or next month, will not have what problem, but if you switch to a few months before or after a few months, Returning to the month with the selected date, the selected month is no longer visible because the previously cached page has been destroyed and rebuilt. Our date click event in MonthView, when each click is selected we need to record the selected date of the corresponding month, when unselected we need to delete the corresponding date from the record, how to save? In the CalendarView class we define a SparseArray

    SparseArray<HashSet<Integer>> chooseDate = new SparseArray< > ()Copy the code

    According to our rules, the position obtained by the conversion of different years is the only corresponding, so we use position as the key of SparseArray, and finally receive selected or unselected operations in CalendarView:

    public void setLastChooseDate(int day.boolean flag) {
          HashSet<Integer> days = chooseDate.get(currentPosition);
          if (flag) {
              if (days == null) {
                  days = new HashSet<>();
                  chooseDate.put(currentPosition, days);
              }
              days.add(day);
          } else {
              days.remove(day); }}Copy the code

    After that, in the process of switching months, refresh the corresponding MonthView according to the saved date data to realize the recovery of selected state, which is similar to the sixth point.

    Jump to the specified date

    To jump to the specified date, first compute the position of the target MonthView in the calendar based on the month and month of the date:

    public static int dateToPosition(int year.int month.int startY, int startM) {
          return (year - startY) * 12 + month - startM;
      }Copy the code

    ViewPager has a setCurrentItem(int Item, Boolean smoothScroll) method to jump to a MonthView corresponding to position and select the corresponding date View in combination with the sixth method. It’s okay to jump to any day in the calendar range.

    Custom calendar style

    Currently CalendarView provides the following custom attributes:

    <declare-styleable name="CalendarView">
          <! -- Select multiple options -->
          <attr name="multi_choose" format="boolean" />
          <! -- Whether to show lunar calendar -->
          <attr name="show_lunar" format="boolean" />
          <! -- Show last month and next month -->
          <attr name="show_last_next" format="boolean" />
          <! -- Whether to display holidays -->
          <attr name="show_holiday" format="boolean" />
          <! -- Whether to display solar terms -->
          <attr name="show_term" format="boolean" />
          <! -- Start date (1990.1)-->
          <attr name="date_start" format="string" />
          <! -- End date (2020.12)-->
          <attr name="date_end" format="string" />
          <! -- Default display, selected date (2016.10.1)-->
          <attr name="date_init" format="string" />
          <! -- Whether all dates before the selected date are available -->
          <attr name="disable_before" format="boolean" />
          <! -- Solar calendar date color -->
          <attr name="color_solar" format="color" />
          <! -- Gregorian calendar date size -->
          <attr name="size_solar" format="integer" />
          <! -- Lunar date color -->
          <attr name="color_lunar" format="color" />
          <! -- Lunar date size -->
          <attr name="size_lunar" format="integer" />
          <! -- Holiday color -->
          <attr name="color_holiday" format="color" />
          <! -- Selected date text color -->
          <attr name="color_choose" format="color" />
          <! -- Selected date background (image) -->
          <attr name="day_bg" format="reference" />
      </declare-styleable>Copy the code

    The default date layout is vertically arranged on the solar calendar and the lunar calendar, and holidays will be overlaid on the lunar calendar, as can be seen from the static screenshot above. If you want to use other layouts, such as horizontal layouts, you need to provide a custom layout (currently, only two TextViews are supported). Such as:

    calendarView.setOnCalendarViewAdapter(R.layout.item_layout, new CalendarViewAdapter() {
              @Override
              public TextView[] convertView(View view, DateBean date) {
                  TextView solarDay = (TextView) view.findViewById(R.id.solar_day);
                  TextView lunarDay = (TextView) view.findViewById(R.id.lunar_day);
                  return new TextView[]{solarDay, lunarDay};}});Copy the code

    Bind CalendarView to an interface, pass in Lauoyt, and return an array of TextViews representing the solar and lunar calendar.

    Eleven, WeekView

    WeekView is a customized View that is displayed on a weekday, starting from Sunday and then from Monday to Saturday. You can configure the displayed text, color, and size of the text by using customized properties. Relatively simple, specific visible source code.

    Twelve, summary

    This way of implementation is relatively simple, relatively easy to understand, after all, the ability is limited there are inevitably insufficient place, behind according to the need to gradually improve and expand it. Even though Github has a lot of Calendar out there, implementing one on your own can still be rewarding. It’s a seemingly simple thing, and you have to try it yourself to get a taste of it.