前言:公司前阵子更换了一个新的日历控件,如丝般顺滑。看了下发现,竟然出自大名鼎鼎的square!毕竟square出品,必属精品的,所以研读了一番,有了这篇文章。
控件的Git项目在此:android-times-square
整个项目结构比较简单,代码也不复杂:
Demo效果图:
首先来认识下此日历都由什么构成,建议边看下图,边看下面这段描述:
整个日历
CalendarPickerView
继承自ListView
;每个月份
MonthView
继承自LinearLayout
,装了一个title
和一个CalendarGridView
;CalendarGridView
并不是继承GridView
,而是一个自定义ViewGroup
,内部包含7个CalendarRowView
。第一个Row
包含7个TextView
用于显示星期,其余6个Row
(为什么有6个?因为每个月最多可能有6行)各自包含7个CalendarCellView
用于显示每周的日期;CalendarRowView
也继承自ViewGroup
,内含7个CalendarCellView
。值得一提的是3和4这些控件组成都写在MonthView
的布局文件:month.xml
里;CalendarCellView
是该日历控件的最基本单元,继承自FrameLayout
。
这个5个View就是日历控件的构成,其余类例如:MonthCellDescriptor
、MonthDescriptor
都是一些辅助,这里暂时不表。
下文在导读源码的同时还会分析下控件的设计技巧,为什么会这样设计?如果让我来做,我会怎么做?了解自己跟大神之间的设计思想差异,我觉得这才是阅读源码的精髓。
这里我主要是想列出让我觉得耳目一新的点,并分析实现,然后希望以后自己能学以致用!
首先,每个自定义View的实现都会有麻烦的渲染工作。而纵观此控件所有相关View的源码,渲染工作有条不紊的发布给几个控件,大家各司其职,而另一些View(例如CanlendarCellView
、CanlendarRowView
)中,只有对外的接口,如此清爽!
再想想我自己写自定义View的时候,各种自定义属性解析,onMeasure
、onDraw
满天飞,不行还得再来个onTouchEvent
,然后便堆砌出几千行代码……
下面我将根据控件中一些类的作用,把它们比作软件开发过程中的各个角色,便于大家理解。
产品经理:MonthAdapter
+ MonthView
MonthView
做的工作很简单:根据布局生成日历整体框架,再将MonthAdapter
传递的“属性”,进一步传递给所有的CanlendarCellView
。
上面提到的属性(List<List<MonthCellDescriptor>> cells
)是一系列状态的集合,包括:是否高亮、是否选中、是否是今天,在不在所选日期范围内等等,而这些属性最终决定着渲染的外观。
而MonthAdapter
在 CalendarPickerView
中。还记得吗, CalendarPickerView
是一个ListView
,所以MonthAdapter
也是一个普通ListView
的Adapter
,它长这样:
@Override public View getView(int position, View convertView, ViewGroup parent) {
MonthView monthView = (MonthView) convertView;
if (monthView == null //
|| !monthView.getTag(R.id.day_view_adapter_class).equals(dayViewAdapter.getClass())) {
monthView =
MonthView.create(parent, inflater, weekdayNameFormat, listener, today, dividerColor,
dayBackgroundResId, dayTextColorResId, titleTextColor, displayHeader,
headerTextColor, decorators, locale, dayViewAdapter);
monthView.setTag(R.id.day_view_adapter_class, dayViewAdapter.getClass());
} else {
monthView.setDecorators(decorators);
}
if (monthsReverseOrder) {
position = months.size() - position - 1;
}
monthView.init(months.get(position), cells.getValueAtIndex(position), displayOnly,
titleTypeface, dateTypeface);
return monthView;
}
}
确实极其普通:MonthView.create(…)
根据布局文件month.xml
构建View
,monthView.init(…)
塞数据。months
是所有日期数据,它的初始化在这:
monthCounter.setTime(minCal.getTime());
final int maxMonth = maxCal.get(MONTH);
final int maxYear = minCal.get(YEAR);
while ((monthCounter.get(MONTH) <= maxMonth // Up to, including the month.
|| monthCounter.get(YEAR) < maxYear) // Up to the year.
&& monthCounter.get(YEAR) < maxYear + 1) { // But not > next yr.
Date date = monthCounter.getTime();
MonthDescriptor month =
new MonthDescriptor(monthCounter.get(MONTH), monthCounter.get(YEAR), date,
monthNameFormat.format(date));
cells.put(monthKey(month), getMonthCells(month, monthCounter));
Logr.d("Adding month %s", month);
months.add(month);
monthCounter.add(MONTH, 1);
}
这里minCal
、minCal
,即最小和最大日期都是用户可以设置的。
如果把整个控件的渲染工作看作一项开发工作,MonthAdapter
和 MonthView
就是产品。现在产品来提需求了,我的刀呢?!