Androidカレンダーコントロールの実現コード



まず動的な効果図を見ましょう。



プロジェクト住所:https://github.com/Othershe/CalendarView
ここでは主にカレンダーコントロールの作成中の主要な点を記録します。
一、主要機能
1、旧暦、節気、一般祝日をサポートする
2、日付範囲の設定、デフォルトサポートの最大日付範囲[1900.1~209.11]
3、標準選択の日付設定
4、単一選択、複数選択
5、指定の日付にジャンプ
6、カスタム属性によって日付の外観をカスタマイズし、簡単な日付アイテムレイアウトの設定を行う。
二、基本構造
私たちが実現したいカレンダーコントロールはViewer Pagerをメインフレームとして、CalendarViewはViewer Pagerを継承しています。こうして、左右のスライドとキャッシュの機能が生まれました。現在、私達はカレンダーを左右にスライドさせて月に切り替える操作を設定しています。毎月の表示はカスタムViewで実現されます。つまり私達のMonthViewです。月の日付はlayoutレイアウトで解析されたViewです。月によっては各MonthViewは6 x 7または5 x 7日の日付Viewが含まれます。したがって、PagerAdapterを継承し、Calendar PagerAdapterを拡張して、MonthViewの関連初期化と日付データのバインディングを完了します。
三、各MonthViewを計算するには、充填が必要な日付データ
上のスクリーンショットから見ると、各MonthViewの日付データは先月の後0~6日間、現在の月の日数と来月の前0~6日間で構成されるべきです。まず、現在の月が何日間あるかを計算します。これは簡単です。また、年月から現在の月の初日は何曜日ですか?

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;
  }
0は日曜日を表し、1~6は月曜日から土曜日を表し、上のスクリーンショットを例にとって、2017年5月の最初の日は月曜日であることが分かります。

for (int i = 0; i < week; i++) {
      ld =       - week + 1 + i;
    }
来月の日付と現在のMonthViewで表示されている行数に関しては、現在の月の日数+weekが7で割り切れるなら、来月の日付は含まれていません。さもなければ、含める来月の日付を計算する必要があります。疑似コードは以下の通りです。

for (int i = 0; i < 7 *       -      - week; i++) {
      nd = i + 1;
    }
このように必要な日付データは計算済みです。詳しい計算方法はソースコードを参照してください。
四、カレンダーの総ページ数を計算する
総ページ数はカレンダーの開始年月によって得られるべきで、実はViewer Pagerの総ページ数を確定して、このように良い理解点です。次の方法で計算できます。

count = (dateEnd[0] - dateStart[0]) * 12 + dateEnd[1] - dateStart[1] + 1
その中でもdateStart、dateEndは、カレンダーの開始年月と終了年月を含む配列です。このcountもCalendarPager Adapterが必要です。
五、positionで日付を計算する
PagerAdapterはinstantiateItem()の方法があります。

public Object instantiateItem(ViewGroup container, int position) {
    return instantiateItem((View) container, position);
  }
を作成しますので、カレンダーのページごとにここで作成します。つまりMonthViewです。ここでポイントがあります。どのように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};
  }

そのうちstartY、startMはカレンダーの実際の年月を表します。対応する年月があれば、第二のポイントで日付データを計算し、MothViewに充填することができます。
六、MothView
前に述べましたが、MonthViewはViewを継承し、カレンダーの各ページを受信した後、MonthViewではデータ構造に応じた日付Viewを作成し、ViewをMonthViewに追加し、最後にoneMeasre、onLayoutを通じて各Viewの最終的な大きさと位置を決定します。ここでViewer Pagerの基本条件を実行すれば満足です。上記のinstantiateItem()方法でMothViewの初期化を完了します。

public Object instantiateItem(ViewGroup container, int position) {
    MonthView view = new MonthView(container.getContext());
    //  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;
  }

ここではコアコードのみを保持しています。カレンダーが月に切り替わると、自動的にpositionから対応月の日付データを計算してMonthViewに転送し、最後にMonthViewをViewer Pagerに追加します。
七、月の選択日を切り替えます。
現在の設定では、現在の月のある日を選択してから、月を切り替えます。新しい月の中に前回選択した日付が見つかり、選択状態としてマークされます。見つけられない場合は、新しい月の最後の日を選択します。実はロジックは簡単です。鍵はどのようにして新月に日付を見つけて選択しますか?まず、前回選択した日付を記録します。Viewer Pagerはデフォルトでは2ページキャッシュされます。現在のページを合わせて3ページを記録します。Calendar PagerAdapterではpositionによって3ページのキャッシュを保存します。ViePagerがあるページに切り替わると、次のようなコールが実行されます。

addOnPageChangeListener(new SimpleOnPageChangeListener() {
      @Override
      public void onPageSelected(int position) {
      }
    });
onPageSelected(int position)メソッドでは、positionを通じてキャッシュから対応するMonthViewを取得し、切り替えられたページでもあり、MonthViewでは、記録した日付に基づいて対応するサブ日付Viewを見つけ、選択状態に変更することができます。
八、複数選択
理想的な多選択機能は、現在の月に複数の日付を選択した後、他の月に切り替えます。その後、選択された日付がある月に戻っても、選択された日付をマークすることができます。ViePagerにはデフォルトの3ページのキャッシュがありますので、現在の月に切り替えると、先月または来月には問題はありません。ただし、前の数ヶ月または後数ヶ月に切り替えると、選択された月に戻ります。前にキャッシュされたページが破壊されて再建されたので、選択された月は見えなくなります。私達の日付クリックイベントはMonthViewの中にあります。選択をクリックするたびに、対応する年月の選択を記録しなければなりません。選択をキャンセルする時は記録から対応日を削除します。どうやって保存しますか?CalendarViewではSparseArayを定義します。

SparseArray<HashSet<Integer>> chooseDate = new SparseArray<>()
その中のHashSetは指定年月に選択された日付で、私達の規則に従って異なった年月に変換されたpositionを設定するのは唯一の対応です。だから私達はpositionをSparsearrayのkeyとして使って、最後に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);
    }
  }
その後、月切替中に、保存した日付データに基づいて対応するモンテビューを更新し、選択状態の回復を実現します。これは6番目と同じです。
九、指定日までジャンプ
指定された日付にジャンプするには、まず日付の年月日からターゲットのMonthViewのカレンダーのpositionを計算します。

public static int dateToPosition(int year, int month, int startY, int startM) {
    return (year - startY) * 12 + month - startM;
  }
View Pagerにはset CurrenntItemという方法があり、position対応のMonthViewにジャンプし、6番目の方法と合わせて対応する日付Viewを選択します。このようにカレンダーの日付設定範囲にジャンプする日はいつでも大丈夫です。
十、ユーザー定義のカレンダースタイル
現在のCalendarViewが提供しているカスタム属性は以下の通りです。

<declare-styleable name="CalendarView">
    <!--    -->
    <attr name="multi_choose" format="boolean" />
    <!--      -->
    <attr name="show_lunar" format="boolean" />
    <!--         -->
    <attr name="show_last_next" format="boolean" />
    <!--       -->
    <attr name="show_holiday" format="boolean" />
    <!--      -->
    <attr name="show_term" format="boolean" />
    <!--    (1990.1)-->
    <attr name="date_start" format="string" />
    <!--    (2020.12)-->
    <attr name="date_end" format="string" />
    <!--    、     (2016.10.1)-->
    <attr name="date_init" format="string" />
    <!--                -->
    <attr name="disable_before" format="boolean" />
    <!--       -->
    <attr name="color_solar" format="color" />
    <!--       -->
    <attr name="size_solar" format="integer" />
    <!--       -->
    <attr name="color_lunar" format="color" />
    <!--       -->
    <attr name="size_lunar" format="integer" />
    <!--      -->
    <attr name="color_holiday" format="color" />
    <!--         -->
    <attr name="color_choose" format="color" />
    <!--       (  )-->
    <attr name="day_bg" format="reference" />
    <!--       ,         -->
    <attr name="switch_choose" format="boolean" />
  </declare-styleable>
基本的に日常の需要を満たすことができます。デフォルトの日付配列は太陽暦、旧暦の垂直配列です。祝日は旧暦でカバーされます。これは上の静止画から分かります。他の配列、例えば水平配列などを使用するには、所定のlayoutを提供する必要がある(ただし、現在は2つのTextView表示のみをサポートしている)。たとえば:

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};
      }
    });
CalendarViewにインターフェースをバインドし、lauoytに伝え、太陽暦と旧暦を表すTextView配列を返す。
十一、WeekView
日付と曜日の表示機能を分割しましたので、CalendarViewは曜日の表示を担当していません。WeekViewは曜日に表示されるカスタムViewです。日曜日から順番に月曜日から土曜日までです。曜日の表示文字と文字の色、サイズをカスタマイズすることができます。これはまだ比較的簡単です。
十二、まとめ
ここではカレンダーの基本的な実現原理といくつかの重要な点を紹介しましたが、実はこのような実現方式は比較的簡単で、理解しやすいです。もちろん足りないところがあります。後は必要に応じて徐々に改善し、拡張していきましょう。Githubには既成のCalendarがたくさんありますが、自分で一つを実現するにはやはり収穫がいっぱいで、一つは簡単に見えるもので、自分で試してこそ初めてその味を味わうことができます。