Android 一個(gè)日歷控件的實(shí)現(xiàn)小記

先看幾張動(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

其中dateStartdateEnd是包含日歷開(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ì)大家有所幫助吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒜哀,一起剝皮案震驚了整個(gè)濱河市斩箫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撵儿,老刑警劉巖乘客,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異淀歇,居然都是意外死亡易核,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)浪默,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)牡直,“玉大人,你說(shuō)我怎么就攤上這事纳决∨鲆荩” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵岳链,是天一觀的道長(zhǎng)花竞。 經(jīng)常有香客問(wèn)我,道長(zhǎng)掸哑,這世上最難降的妖魔是什么约急? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮苗分,結(jié)果婚禮上厌蔽,老公的妹妹穿的比我還像新娘。我一直安慰自己摔癣,他們只是感情好奴饮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著择浊,像睡著了一般戴卜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琢岩,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天投剥,我揣著相機(jī)與錄音,去河邊找鬼担孔。 笑死江锨,一個(gè)胖子當(dāng)著我的面吹牛吃警,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播啄育,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼酌心,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了挑豌?” 一聲冷哼從身側(cè)響起安券,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浮毯,沒(méi)想到半個(gè)月后完疫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡债蓝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年壳鹤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饰迹。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡芳誓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啊鸭,到底是詐尸還是另有隱情锹淌,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布赠制,位于F島的核電站赂摆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钟些。R本人自食惡果不足惜烟号,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望政恍。 院中可真熱鬧汪拥,春花似錦、人聲如沸篙耗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宗弯。三九已至脯燃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒙保,已是汗流浹背曲伊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坟募。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像邑狸,于是被迫代替她去往敵國(guó)和親懈糯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345