先看幾張動(dòng)態(tài)的效果圖吧束亏!
項(xiàng)目地址:https://github.com/SheHuan/CalendarView
這里主要記錄一下在編寫(xiě)日歷控件過(guò)程中一些主要的點(diǎn):
一日矫、主要功能
- 1纷责、支持農(nóng)歷而柑、節(jié)氣寻馏、常用節(jié)假日
- 2膳汪、日期范圍設(shè)置,默認(rèn)支持的最大日期范圍[1900.1~2049.12]
- 3访雪、禁用日期范圍設(shè)置
- 4详瑞、初始化選中單個(gè)或多個(gè)日期
- 5、單選臣缀、多選操作
- 6坝橡、跳轉(zhuǎn)到指定日期
- 7、替換農(nóng)歷為指定文字
- 8精置、通過(guò)自定義屬性定制日期外觀计寇,以及簡(jiǎn)單的日期item布局配置
- 9、......
二脂倦、基本結(jié)構(gòu)
我們要實(shí)現(xiàn)的日歷控件采用ViewPager作為主框架番宁,CalendarView繼承ViewPager,這樣就天生擁有左右滑動(dòng)和緩存的功能赖阻。目前我們?cè)O(shè)定日歷左右滑動(dòng)為月份切換的操作蝶押,每一個(gè)月份顯示通過(guò)自定義ViewGroup實(shí)現(xiàn),也就是我們的MonthView火欧,月份中的日期是通過(guò)layout布局解析出的View棋电,根據(jù)月份的不同每個(gè)MonthView可能包含6 x 7或5 x 7個(gè)日期View,由于給ViewPager綁定數(shù)據(jù)需要通過(guò)PagerAdapter苇侵,所以繼承PagerAdapter我們擴(kuò)展了一個(gè)CalendarPagerAdapter赶盔,來(lái)完成MonthView的相關(guān)初始化和日期數(shù)據(jù)的綁定。
三榆浓、計(jì)算每個(gè)MonthView需要填充的日期數(shù)據(jù)
從上邊的截圖可以看出于未,每個(gè)MonthView的日期數(shù)據(jù)應(yīng)該由上個(gè)月的后0~6天、當(dāng)前月的天數(shù)和下個(gè)月的前0~6天組成陡鹃。首先計(jì)算出當(dāng)前月有多少天烘浦,這個(gè)簡(jiǎn)單,以及根據(jù)年月算出當(dāng)前月的第一天是星期幾:
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月的第一天是周一:week = getFirstWeekOfMonth(2017猿推, 5-1)
片习,按照如下偽碼則可計(jì)算出包含的上個(gè)月的日期:
for (int i = 0; i < week; i++) {
ld = 上個(gè)月天數(shù) - week + 1 + i;
}
至于包含的下個(gè)月的日期和當(dāng)前MonthView顯示的行數(shù)有關(guān)捌肴,如果 當(dāng)前月的天數(shù)+week
可以被7整除則不需要包含下月日期,否則需要計(jì)算包含的下月日期藕咏,偽碼如下:
for (int i = 0; i < 7 * 顯示的行數(shù) - 當(dāng)月天數(shù) - week; i++) {
nd = i + 1;
}
這樣需要的日期數(shù)據(jù)就計(jì)算完了状知,詳細(xì)的算法可參考源碼。
四孽查、 計(jì)算日歷的總頁(yè)數(shù)
總頁(yè)數(shù)應(yīng)由日歷的起始年月得到饥悴,其實(shí)就是確定ViewPager的總頁(yè)數(shù),這樣好理解點(diǎn)盲再∥魃瑁可按照如下方法計(jì)算:
count = (dateEnd[0] - dateStart[0]) * 12 + dateEnd[1] - dateStart[1] + 1
其中dateStart、dateEnd是包含日歷開(kāi)始年月和結(jié)束年月的數(shù)組答朋。這個(gè)count也是CalendarPagerAdapter必須的贷揽。
五、用position計(jì)算日期
PagerAdapter有個(gè)instantiateItem()方法:
public Object instantiateItem(ViewGroup container, int position) {
return instantiateItem((View) container, position);
}
來(lái)創(chuàng)建ViewPager的每一頁(yè)梦碗,所以日歷每一頁(yè)也是在這里創(chuàng)建的禽绪,也就是MonthView,這里有個(gè)關(guān)鍵的點(diǎn)就是根據(jù) positon 參數(shù)推算出日歷每一頁(yè)對(duì)應(yīng)的年月洪规,然后通過(guò)年月計(jì)算出當(dāng)前MonthView需要的日期數(shù)據(jù)印屁。如何根據(jù)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代表日歷的其實(shí)年月雄人。有了對(duì)應(yīng)的年月就可以用第二點(diǎn)中的方式計(jì)算日期數(shù)據(jù),然后填充到MothView中念赶。
六柠衍、MothView
前邊已經(jīng)提到了,MonthView繼承ViewGroup晶乔,也就是日歷的每一頁(yè),接收到日期數(shù)據(jù)后牺勾,在MonthView中根據(jù)數(shù)據(jù)構(gòu)造對(duì)應(yīng)的日期View正罢,然后添加View到MonthView中,最后通過(guò)onMeasure驻民、onLayout確定每個(gè)View最終大小和位置翻具。到這里運(yùn)行一個(gè)ViewPager的基本條件就滿足了,在上邊提到的instantiateItem()方法中完成MothView的初始化:
public Object instantiateItem(ViewGroup container, int position) {
MonthView view = new MonthView(container.getContext())回还;
//根據(jù)position計(jì)算對(duì)應(yīng)年裆泳、月
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;
}
這里只保留了核心的代碼,當(dāng)日歷切換月份時(shí)柠硕,會(huì)自動(dòng)根據(jù)position計(jì)算出對(duì)應(yīng)月份的日期數(shù)據(jù)工禾,然后傳給MonthView运提,最后將MonthView添加到ViewPager中。
七闻葵、切換月份選中日期
按照目前的設(shè)定民泵,當(dāng)選擇當(dāng)前月的某天后,然后切換月份槽畔,新的月份中會(huì)找到上次選中的日期栈妆,并標(biāo)記為選中狀態(tài),如果找不到則選中新月份的最后一天厢钧。其實(shí)邏輯很簡(jiǎn)單鳞尔,關(guān)鍵是如何在新月份中找到相應(yīng)的日期并選中。首先記錄上次選中的日期早直,由于ViewPager默認(rèn)會(huì)緩存兩頁(yè)寥假,再加上當(dāng)前頁(yè)共三頁(yè),在CalendarPagerAdapter中根據(jù)position保存三頁(yè)緩存莽鸿,當(dāng)ViewPager切換到某一頁(yè)后會(huì)執(zhí)行如下回調(diào):
addOnPageChangeListener(new SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
}
});
在onPageSelected(int position)方法中通過(guò)position從緩存中拿到對(duì)應(yīng)的MonthView昧旨,也是是切換到的頁(yè),這樣就能在MonthView中根據(jù)記錄的日期找到對(duì)應(yīng)的子日期View祥得,然后更改為選中狀態(tài)兔沃。
八、多選
一個(gè)理想的多選功能應(yīng)該是在當(dāng)前月份選中多個(gè)日期后级及,切換到其它月份乒疏,之后回到有選中日期的月份依然能夠標(biāo)記出選中的日期,因?yàn)閂iewPager有默認(rèn)的三頁(yè)緩存饮焦,所以在當(dāng)前月份切換到上月或下月不會(huì)有什么問(wèn)題怕吴,但如果切換到前幾個(gè)月或后幾個(gè)月,再回到有選中日期的月份县踢,由于之前緩存的頁(yè)面已經(jīng)被銷毀重建转绷,所以選中的月份也就看不到了。我們的日期點(diǎn)擊事件在MonthView中硼啤,當(dāng)每次點(diǎn)擊選中時(shí)我們需要記錄對(duì)應(yīng)年月選中的日期议经,取消選中時(shí)要從記錄中刪除對(duì)應(yīng)日期,怎么保存呢谴返?在CalendarView類中我們定義一個(gè)SparseArray
SparseArray<HashSet<Integer>> chooseDate = new SparseArray<>()
其中的HashSet就是指定年月選中的日期煞肾,按照我們的規(guī)則設(shè)定不同年月轉(zhuǎn)換得到的position是唯一對(duì)應(yīng)的,所以我們用position作為SparseArray的key嗓袱,最后在CalendarView中接收選中或取消選中的操作:
public void setChooseDate(int day, boolean flag, int position) {
if (position == -1) {
position = currentPosition;
}
HashSet<Integer> days = chooseDate.get(position);
if (flag) {
if (days == null) {
days = new HashSet<>();
chooseDate.put(position, days);
}
days.add(day);
positions.add(position);
} else {
days.remove(day);
}
}
之后就是在月份切換過(guò)程中籍救,根據(jù)保存的日期數(shù)據(jù)刷新對(duì)應(yīng)的MonthView,實(shí)現(xiàn)選中狀態(tài)的恢復(fù)渠抹,這個(gè)和第六點(diǎn)類似蝙昙。
九闪萄、跳轉(zhuǎn)到指定日期
要跳轉(zhuǎn)到指定日期,首先要根據(jù)日期的年月計(jì)算出目標(biāo)MonthView在日歷中的position:
public static int dateToPosition(int year, int month, int startY, int startM) {
return (year - startY) * 12 + month - startM;
}
ViewPager有一個(gè)setCurrentItem(int item, boolean smoothScroll)
方法耸黑,這樣就能跳轉(zhuǎn)到position對(duì)應(yīng)的MonthView桃煎,然后結(jié)合第六點(diǎn)的方法選中對(duì)應(yīng)的日期View。這樣跳轉(zhuǎn)到日歷設(shè)定日期范圍內(nèi)的任意一天都是沒(méi)問(wèn)題的大刊。
十为迈、自定義日歷樣式
CalendarView提供的自定義屬性如下:
屬性名 | 格式 | 描述 | 默認(rèn)值 |
---|---|---|---|
choose_type | enum | 設(shè)置單選(single)、多選(multi) | single |
show_lunar | boolean | 是否顯示農(nóng)歷 | true |
show_last_next | boolean | 是否在MonthView顯示上月和下月日期 | true |
show_holiday | boolean | 是否顯示節(jié)假日 | true |
show_term | boolean | 是否顯示節(jié)氣 | true |
switch_choose | boolean | 單選時(shí)切換月份缺菌,是否選中上次的日期 | true |
solar_color | color | 陽(yáng)歷日期的顏色 | |
solar_size | integer | 陽(yáng)歷的日期尺寸 | 14 |
lunar_color | color | 農(nóng)歷的日期顏色 | |
lunar_size | integer | 農(nóng)歷的日期尺寸 | 8 |
holiday_color | color | 節(jié)假日葫辐、節(jié)氣的顏色 | |
choose_color | color | 選中的日期顏色 | |
day_bg | reference | 選中的日期背景(圖片) |
CalendarView相關(guān)方法:
方法名 | 描述 |
---|---|
setInitDate(String date) | 設(shè)置日歷的初始顯示年月 |
setStartEndDate(String startDate, String endDate) | 設(shè)置日歷開(kāi)始、結(jié)束年月 |
setDisableStartEndDate(String startDate, String endDate) | 設(shè)置日歷的禁用日期范圍(小于startDate伴郁、大于endDate禁用) |
setSpecifyMap(HashMap<String, String> map) | 將顯示農(nóng)歷的區(qū)域替換成指定文字 |
setSingleDate(String date) | 設(shè)置單選時(shí)初始選中的日期(不設(shè)置則不默認(rèn)選中) |
getSingleDate() | 得到單選時(shí)選中的日期 |
setMultiDate(List<String> dates) | 設(shè)置多選時(shí)默認(rèn)選中的日期集合 |
getMultiDate() | 得到多選時(shí)選中的全部日期 |
toSpecifyDate(int year, int month, int day) | 單選時(shí)跳轉(zhuǎn)到指定年月日 |
setOnCalendarViewAdapter(int layoutId, CalendarViewAdapter adapter) | 設(shè)置自定義日期item樣式 |
init() | 日期初始化(以上屬性配置完后調(diào)用) |
setOnPagerChangeListener(OnPagerChangeListener listener) | 設(shè)置月份切換回調(diào) |
setOnSingleChooseListener(OnSingleChooseListener listener) | 設(shè)置單選回調(diào) |
setOnMultiChooseListener(OnMultiChooseListener listener) | 設(shè)置多選回調(diào) |
today() | 單選時(shí)跳轉(zhuǎn)到今天 |
nextMonth() | 跳轉(zhuǎn)到下個(gè)月 |
lastMonth() | 跳轉(zhuǎn)到上個(gè)月 |
nextYear() | 跳轉(zhuǎn)到下一年的當(dāng)前月 |
lastYear() | 跳轉(zhuǎn)到上一年的當(dāng)前月 |
toStart() | 跳轉(zhuǎn)到日歷的開(kāi)始年月 |
toEnd() | 跳轉(zhuǎn)到日歷的結(jié)束年月 |
CalendarUtil.getCurrentDate() | 獲得當(dāng)前日期(今天) |
默認(rèn)的日期布局是陽(yáng)歷耿战、陰歷垂直排列,節(jié)假日會(huì)覆蓋在農(nóng)歷上顯示焊傅,這個(gè)從上邊的靜態(tài)截圖可以看出剂陡。如果要使用其它的排列方式,例如水平排列等狐胎,就需要提供一個(gè)自定的layout(但目前只支持兩個(gè)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綁定一個(gè)接口,傳入lauoyt握巢,然后返回一個(gè)代表陽(yáng)歷和農(nóng)歷的TextView數(shù)組晕鹊。
十一、WeekView
我們將日期和星期的顯示功能分割開(kāi)了暴浦,所以CalendarView并不負(fù)責(zé)星期的顯示溅话,
WeekView是星期顯示的自定義View,從周日開(kāi)始依次是周一到周六歌焦,可通過(guò)自定義屬性來(lái)配置星期的顯示文字飞几,以及文字的顏色、尺寸独撇,這個(gè)還是相對(duì)簡(jiǎn)單循狰,具體可見(jiàn)Github中的使用介紹。
十二券勺、小結(jié)
這里我們只介紹了日歷的基本實(shí)現(xiàn)原理,和一些關(guān)鍵的點(diǎn)灿里,其實(shí)這種實(shí)現(xiàn)方式相對(duì)還是比較簡(jiǎn)單的关炼,容易理解,當(dāng)然難免有不足的地方匣吊,后邊根據(jù)需要再逐步完善和擴(kuò)展吧儒拂。盡管Github上有許多現(xiàn)成的Calendar寸潦,但自己動(dòng)手實(shí)現(xiàn)一個(gè)還是收獲滿滿,一個(gè)看起來(lái)簡(jiǎn)單的東西社痛,只有親自嘗試了才能體會(huì)到其中的滋味见转,最后希望對(duì)大家有所幫助吧!