前言:本文在整理过程中由于水平有限,若有不当之处,请指正!
1 常见界面及布局的实现
1.1 日历主界面:
日历主界面是由AllInOneActivity实现,对应四种视图类型动态加载相应的Fragment实现。各视图如下:
(1) 日视图:在AllInOneActivity上加载了DayFragment,DayFragment的布局采用了自定义布局DayView,而填充该布局文件时用到了ViewSwitcher,ViewSwitch是一个视图切换组件,可以把多个视图重叠在一起,而每次只显示一个视图,而给ViewSwitch而创建要显示的View时,有两种方式:既可以在xml文件中添加,也可以通过实现ViewFactory,重写makeView()添加。源码中采用第二种方式,如下:
DayFragment的xml布局:
Java代码:
日视图效果如下:
(2) 周视图:也是加载了DayFragment,效果如下:
(3) 月视图:在AllInOneActivity上加载了MonthByWeekFragment,MonthByWeekFragment的布局是一个自定义的MonthListView,而MonthByWeekFragment继承了SimpleDayPickerFragment,这类继承了ListFragment,ListFragment是一个自身带有一个ListView的Fragment,在SimpleDayPickerFragment中,通过适配器SimpleWeeksAdapter给对应的ListView添加数据,这些数据包括周数、是否显示周数、每周的起始日、高亮显示的日期。效果如下:
(4) 日程视图:在AllInOneActivity上加载了AgendaFragment,AgendaFragment的布局文件也是使用了自定义的ListView:AgendaListView,并通过适配器AdendaWindoeAdapter加载日程数据。效果如下:
1.2 新建活动界面
在EditEventActivity上动态加载了EditEventFragment,并将EditEventFragment的视图对象传给了EditEventView,所有的控件的实例化和事件处理都在自定义的EditEventView中完成。界面如下:
1.3 设置界面
设置界面的Activity:CalendarSettingsActivity继承了PreferenceActivity,在CalendarSettingsActivity中又加载了GeneralPreferences和AboutPreferences两个PreferenceFragment。在PreferenceActivity中使用“Header+ Fragment”的模式,实现首选项设置,在当前Activity中展示一个或者多个首选项的标题,每个标题对应一个相应的PreferenceFragment,使用PreferenceActivity时,需要重写onBuildHeaders(List<Fragment> target)方法填充标题对应的PreferenceFragment。如源码中:
CalendarSettingsActivity的xml布局文件:
Java代码:
而在PreferenceFragment中,通过xml文件定制它的首选项,在xml文件中,创建布局文件时,必须使用PreferenceScreen作为根节点,在根节点下可以设置许多子节点。常用的Preference有如下几种:
ListPreference:以对话框的形式显示一系列词目的Preference;
CheckBoxPreference:提供了复选框功能的Preference;
DialogPreference:提供了对话框功能的Preference;
EditTextPreference:DialogPreference的子类,加入了EditText的功能;
RingtonePreference:选择铃声的Preference;
源码中创建xml:
Java代码:
界面效果:
1.4 删除事件界面
删除活动界面为DeleteEventsActivity,该Activity继承了ListActivity,自身带有ListView,用来显示所有创建的事件,事件的加载使用了CursorLoader,通过CursorLoader对创建的事件进行查询并返回一个cursor对象,再通过适配器EventListAdapter将数据设置到ListView中。
2 常用类
2.1 Time类
Time类:属于android.text.format包中,在API22中被弃用,使用GregorianCalendar替代。日历中所有时间的设置都使用Time并开启一个子线程进行更新,如在DayView中:
2.1.1常用成员变量
isDst:设置是否为夏令时,(其他国家使用),设置为正数---是夏令时,为0---不是夏令时,负数---未知;
minute:分钟【0-59】;
hour:[0,23];
month:[0-11]
monthDay:[1-31]
weekDay:[0-6]
yearDay:[0-365]
......
2.1.2 构造方法
Time(String timezone);
Time();
2.1.3 常用方法
void setToNow():将给定的Time对象的时间设置为当前时间;
void set(int second, int minute, int hour, int monthDay, int month, int year);
void set(int monthDay, int month, int year);设置时间
long setJulianDay(int julianDay):设置时间为给定的儒历日,前提是必须处于同一时区;
String toString( ):返回当前时间以该格式:YYYYMMDDTHHMMSS ;
long normalize(boolean ignoreDst):确保每个字段的值在范围内,例如:3月32号,该方法调用后可以变为4月1 号;ignoreDst若为true,会自动将isDst的值变为-1,即未知;
long toMillis(boolean ignoreDst):将时间转变为毫秒,如果ignoreDst=true,则表示该方法忽视当前是否设置isDst变量,自动计算出正确的isDst的值;如果ignore设置为false,这个方法将会使用当前设置的“isDst”字段,并且调整返回的时间如果isDst的字段是错误的的话。
static int getJulianDay(long millis, long gmtoff):得到指定时区的指定时间点的julian日;对于给定的日期julian日在每个时区都是相同的。
2.2 CalendarController
CalendarController是Calendar的“控制台”,Calendar中所有的加载Fragment、事件处理等都是通过CalendarController来完成的,事件处理具体步骤如下:
(1) 获取CalendarController实例:
mController = CalendarController.getInsitance(this);
(2)注册EventHandler:
mController.registerFirstEventHandler(HANDLER_KEY, this);
EventHandler是CalendarController中的一个内部接口,事件的处理最终会在该接口中HandleEvent()方法中进行处理。
(3)调用sendEvent()发送事件进行处理:
mController.sendEvent(this, EventType.UPDATE_TITLE, t, t, -1, ViewType.CURRENT,mController.getDateFlags(), null, null);
sendEvent方法有许多的重载函数,通过这些重载函数,将参数全部封装到了EventInfo中。
(4)handleEvent()进行处理.
在调用sendEvent()时,需要传入的一个参数为事件类型,CalendarController中定义的事件类型有14种,常见的有:EventType.CREATE_EVENT:新建活动;
EventType.EDIT_EVENT:编辑活动
EventType.DELETE_EVENT:删除活动
EventType.GO_TO:切换视图
EventType.SEARCH:搜索活动
EventType.LAUNCH_SETTINGS:启动设置界面
根据不同的EventType从而处理不同的事件。
3 主要功能实现
3.1 视图的切换
在AllInOneActivity中,通过ActionBar进行视图的转换,调用actionBar的setNavigationMode()设置actionBar的导航栏模式为下拉列表式,并实现OnNavigationListener 接口,重写onNavigationItemSelected()方法,选择不同的条目时会触发此方法进行回调。代码如下:
当用户点击actionBar的导航列表中的条目时,会触发onNavigationItemSelected()方法,在该方法中,通过不同的itemId进行视图的切换,切换视图时,使用了CalendarController的sendEvent()方法,在通过sendEvent()的重载函数,将事件信息封装到EventInfo中,调用handleEvent(),handleEvent()方法是CalendarController中的内部接口EventHandler中的方法,在AllInOneActivity中继承了该接口,重写了该方法,从而在handleEvent()中,调用setMainPane()方法进行Fragment的切换。在setMainPane()中,分别对不同的视图类型进行不同的Fragment的实例化,并加载到Activity中,从而完成视图的切换。
3.2 事件的同步
在增加或者删除事件时,界面总能同时完成更新,使用了Loader加载器中的CursorLoader。CursorLoader可以实现异步加载数据,这样可以避免同步查询时UI线程阻塞的问题,使用CursorLoader时,调用getLoaderManager().initLoader(int id, Bundle args, LoaderCallbacks<D> callback)进行Loader的创建或复用。因此,需要实现LoaderManager.LoaderCallbacks接口作为上述方法的第三个参数,并重写三个方法:
onCreateLoader():创建CursorLoader对象;
onLoaderFinish():数据加载完毕时回调;
onLoaderReset():Loader对象重置时回调;
最后,将查询数据后返回的Cursor对象当做数据源填充给适配器,从而更新适配器所在的适配器控件。源码中使用如下:
3.3 添加账户功能
新建事件时,若没有添加账户或者没有同步,会弹出对话框进行添加账户。在EditEventViewFragment中,会通过实例化AsyncQueryHandler的子类QueryHandler进行查询日历。核心代码如下:
mHandler.startQuery(TOKEN_CALENDARS, null, Calendars.CONTENT_URI, EditEventHelper.CALENDARS_PROJECTION, EditEventHelper.CALENDARS_WHERE,selArgs /* selection args */, null /* sort order */);
当该方法执行完毕后,会触发onQueryComplete()方法,在该方法中,若查询完毕后返回的Cursor对象为空,说明不存在日历账户,会弹出对话框,不再执行后面方法,若返回的Cursor对象不为空,则会在EditEventView中的CalendarSpinner中填充Cursor中的日历对象。核心代码如下:
/*返回的Cursor对象为null时*/
if (cursor == null || cursor.getCount() == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(R.string.no_syncable_calendars).setIconAttribute(
android.R.attr.alertDialogIcon).setMessage(R.string.no_calendars_found)
.setPositiveButton(R.string.add_account, this)
.setNegativeButton(android.R.string.no, this).setOnCancelListener(this);
mNoCalendarsDialog = builder.show();
return;
}
若cursor不为空,会将Cursor中的数据添加给CalendarsSpinner,给CalendarsSpinner填充数据时,将cursor中的对应列的值取出加载到适配器CalendarsAdapter中,从而给CalendarsSpinner添加数据:
mCalendarsSpinner.setAdapter(adapter);
在CalendarsAdapter中取出Cursor中每列的列数,再通过列数获得该列的值:
int colorColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
int nameColumn = cursor.getColumnIndexOrThrow(Calendars.CALENDAR_DISPLAY_NAME);
int ownerColumn = cursor.getColumnIndexOrThrow(Calendars.OWNER_ACCOUNT);
点击“确定”按钮,会进行跳转到添加账户的页面,核心代码如下:
public void onClick(DialogInterface dialog, int which) {
if (dialog == mNoCalendarsDialog) {
mDone.setDoneCode(Utils.DONE_REVERT);
mDone.run();
if (which == DialogInterface.BUTTON_POSITIVE) {
//启动Settings包中的AddAccountSettings
Intent nextIntent = new Intent(Settings.ACTION_ADD_ACCOUNT);
final String[] array = {"com.android.calendar"};
nextIntent.putExtra(Settings.EXTRA_AUTHORITIES, array);
nextIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity.startActivity(nextIntent);
}
}
}
3.4 事件提醒功能流程浅析
事件提醒功能主要在AlertReceiver和AlertService中进行,AlertReceiver是一个广播接收者,AlertService是一个服务,当创建事件后,系统会在广播中进行监听,发送广播,并通过广播开启服务进行通知的发送,其流程如下:
5 视图的绘制
5.1 月视图的绘制
月视图对应的Fragment为MonthByWeekFragment,而MonthByWeekFragment的父类为SimpleDayPickerFragment,SimpleDayPickerFragment继承自ListFragment。它们之间的结构关系如下:
因此,在进行绘制时,在MonthWeekEventsView中进行绘制,相当于给适配器设置布局格式,绘制完成后,将对应Adapter设置给MonthListView,从而显示在界面。在视图绘制过程中,使用了Paint类和Canvas类对界面各组件进行绘制,主要包括:间隔线的绘制、背景色的绘制、日期数字的绘制、农历的绘制、事件标志的绘制、点击效果的绘制。
5.1.1 间隔线的绘制
间隔区域的绘制,使用了canvas.drawLines()方法,在区域内进行线条的绘制从而实现区域分割。核心代码如下:
protected void drawDaySeparators(Canvas canvas) {
float lines[] = new float[8 * 4];
int count = 6 * 4;
while (i < count) {
int x = computeDayLeftPosition(i / 4 - wkNumOffset);
lines[i++] = x;
lines[i++] = y0;
lines[i++] = x;
lines[i++] = y1;
}
p.setColor(mDaySeparatorInnerColor);
p.setStrokeWidth(DAY_SEPARATOR_INNER_WIDTH);
canvas.drawLines(lines, 0, count, p);//lines包括2个坐标,表示起始坐标和终点坐标
}
5.1.2 背景色的绘制
绘制背景时,分为三种情况,“今天”的背景色,奇数月的背景、偶数月的背景色,通过给奇数月和偶数月设置不同的背景,能快速的区别每个月,源码如下:
protected void drawBackground(Canvas canvas) {
int i = 0;
int offset = 0;
r.top = DAY_SEPARATOR_INNER_WIDTH;
r.bottom = mHeight;
/*奇数月背景*/
if (!mOddMonth[i]) {
while (++i < mOddMonth.length && !mOddMonth[i])
;
r.right = computeDayLeftPosition(i - offset);
r.left = 0;
p.setColor(mMonthBGOtherColor);
canvas.drawRect(r, p);
// compute left edge for i, set up r, draw
/*非奇数月但奇数月的前几天和获取焦点的月数的后几天位于同一行*/
} else if (!mOddMonth[(i = mOddMonth.length - 1)]) {
while (--i >= offset && !mOddMonth[i]);
i++;
// compute left edge for i, set up r, draw
r.right = mWidth;
r.left = computeDayLeftPosition(i - offset);
p.setColor(mMonthBGOtherColor);
canvas.drawRect(r, p);
}
if (mHasToday) {//“今天”的背景,高亮显示
p.setColor(mMonthBGTodayColor);
r.left = computeDayLeftPosition(mTodayIndex);
r.right = computeDayLeftPosition(mTodayIndex + 1);
canvas.drawRect(r, p);
}
}
5.1.3 天数的绘制
绘制天数时,也分为两种情况:获取了焦点的月份的天数和未获取焦点的月份的天数,天数的取值为[1,31],通过Time类的month属性就可以设置某天的天数。
源码如下:
得到天数的数组:
mDayNumbers[i] = Integer.toString(time.monthDay++);
绘制核心代码如下:
protected void drawWeekNums(Canvas canvas) {
boolean isFocusMonth = mFocusDay[i];
boolean isBold = false;
mMonthNumPaint.setColor(isFocusMonth ? Color.RED : Color.YELLOW);
// Get the julian monday used to show the lunar info.
int julianMonday = Utils.getJulianMondayFromWeeksSinceEpoch(mWeek);
Time time = new Time(mTimeZone);
time.setJulianDay(julianMonday);
/*判断是否是“今天”*/
for (; i < numCount; i++) {
if (mHasToday && todayIndex == i) {
mMonthNumPaint.setColor(Color.BLUE);
mMonthNumPaint.setFakeBoldText(isBold = true);
/*判断是否是获取了焦点的月*/
} else if (mFocusDay[i] != isFocusMonth) {
isFocusMonth = mFocusDay[i];
mMonthNumPaint.setColor(isFocusMonth ? Color.RED : Color.YELLOW);
}
x = computeDayLeftPosition(i - offset) - (SIDE_PADDING_MONTH_NUMBER);
canvas.drawText(mDayNumbers[i], x, y, mMonthNumPaint);
在绘制天数的方法中,会对农历也进行绘制,绘制时首先判断当前语言环境是否支持农历,其次进行绘制,通过LunarUtils中的静态方法进行判断是否显示农历,代码如下:
public static boolean showLunar(Context context) {
Locale locale = Locale.getDefault();
String language = locale.getLanguage().toLowerCase();
String country = locale.getCountry().toLowerCase();
return ("zh".equals(language) && ( "cn".equals(country) || ("tw".equals(country) ) || ("hk".equals(country))));
}
在绘制数字时通过调用该静态方法判断是否显示农历,若显示,则进行农历的绘制,核心代码如下:
ArrayList<String> infos = new ArrayList<String>();
/*获取给定日期的农历*/
LunarUtils.get(getContext(), year, month, monthDay,
LunarUtils.FORMAT_LUNAR_SHORT | LunarUtils.FORMAT_MULTI_FESTIVAL, false,
infos);
for (int index = 0; index < infos.size(); index++) {
String info = infos.get(index);
if (TextUtils.isEmpty(info)) continue;
infoX = x;
infoY = y + (mMonthNumHeight + LUNAR_PADDING_LUNAR) * (num + 1);
canvas.drawText(info, infoX, infoY, mMonthNumPaint);
num = num + 1;
}
}
5.1.4 事件标志的绘制
当某一天存在用户新建的活动时,会在当月的区域内绘制一个小矩形,绘制原理与绘制间隔线相同,略去。
5.1.5 点击事件效果的绘制
当点击月视图某天时,会出现类似于selector的效果,也是通过绘制进行实现,核心代码如下:
private void drawClick(Canvas canvas) {
if (mClickedDayIndex != -1) {
int alpha = p.getAlpha();
p.setColor(mClickedDayColor);
p.setAlpha(mClickedAlpha);
r.left = computeDayLeftPosition(mClickedDayIndex);
r.right = computeDayLeftPosition(mClickedDayIndex + 1);
r.top = DAY_SEPARATOR_INNER_WIDTH;
r.bottom = mHeight;
canvas.drawRect(r, p);
p.setAlpha(alpha);//设置透明度
}
}
5.1.6 星期的绘制
在界面的月数上端,会显示对应日期是周几,这部分内容是通过利用Strin[]数组和android.text.format包中的DateUtil类进行设置星期几。在setUpHeader()中:
protected void setUpHeader() {
mDayLabels = new String[7];
for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
/*获取 “星期几” */
mDayLabels[i-Calendar.SUNDAY]= DateUtils.getDayOfWeekString(i,DateUtils.LENGTH_MEDIUM).toUpperCase();} }
5.2 日视图、周视图的绘制
日视图和周视图都是通过在AllInOneActivity中动态加载DayFragment,并给DayFragment设置自定义视图DayView实现的。因此可以归纳在一起。在DayView中进行绘制时,区别绘制日视图还是周视图通过AllInOneActivity中实例化DayFragment时传入的参数numOfDays确定。源码如下:
加载日视图时,numOfDays = 1:
frag = new DayFragment(timeMillis, 1);
加载周视图时,numOfDays = 7:
frag = new DayFragment(timeMillis, 7);
从而在DayFragment中创建DayView时,通过numOfDays作为判断条件,获取不同效果的日视图和周视图。日视图和周视图的绘制都在DayView中完成。绘制各效果的方法之间的关系如下图:
各方法功能如下:
doDraw()里面包括:
drawBgColors(): 绘制视图背景色;
drawGridBackground():绘制布局间隔线,通过传入的mNumDays计算是周视图还是日视图;
drawHours() ---> setupHourTextPaint(p): 绘制小时
drawSelectedRect():绘制点击某一区域时的图案,包括所选中区域的背景和“+新建事件”的绘制。
drawEvents() ----> drawEventRect()、drawEventText();绘制事件;
drawCurrentTimeLine();绘制当前时间线
drawAfterScroll()里包括:
drawAllDayHighlights():左上角高亮表示全天活动;
drawAllDayEvents():绘制全天活动的边界;
drawUpperLeftCorner():当存在全天活动时,绘制全天活动左上角的图案;
drawDayHeaderLoop(): 绘制周视图标题栏;
drawAmPm(canvas, p):绘制“上午”、“下午”
drawScrollLine():绘制主界面与标题栏之间的分割线。
使用以上方法进行界面的绘制,绘制内容主要包括绘制字体、绘制矩形区域、绘制线条。绘制时调用以下方法,如下:
Canvas.drawText(String text, float x, float y, Paint paint);//绘制字体
Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint)//绘制线条
Canvas.drawRect(Rect r, Paint paint)//绘制矩形
5.3 日程视图的加载过程浅析
日程视图是在AllInOneActivity中加载了AgendaFragment,AgendaFragment中的布局文件是自定义的ListView,所有的事件都是以ListView中条目的形式展示在屏幕上,因此不存在View的绘制,而是通过继承ListView和Adapter来实现。
日程视图的最顶层布局是自定义的StickyHeaderListView,继承自FrameLayout,主要提供了一些接口,以及处理滑动事件的监听,主界面是AgendaListView,继承自ListView,适配器为AgendaWindowAdapter,日程中还包括一些适配器,它们的功能如下:
AgendaWindowAdapter:为AgendaListView添加数据,将AgendaAdapter和AgendaByDayAdapter中的数据进行整合;
AgendaByDayAdapter:显示星期、月份的条目。
AgendaAdapter:显示每个事件的标题、时间、地点和左边红点;
界面效果如下:
①区域:给ListView设置HeadTest和FooterText.源码如下:
mAgendaListView.addHeaderView(mHeaderView);
mAgendaListView.addFooterView(mFooterView);
当触摸更新Header时,每次在查询完成之后,也就是onQueryComplete()方法中调用以下方法进行日期的更新:updateHeaderFooter(final int start, final int end)。
②区域:使用AgendaByDayAdapter将数据加载到AgendaListView中:包括星期和日期。重写getView()加载布局,加载布局为:agenda_day.xml;
③区域:使用AgendaAdapter将数据加载到AgendaListView中,包括事件标题、时间、地点等,加载的布局为:agenda_item.xml ;
5.3.1 AgendaWindowAdapter的加载过程分析
AgendaFragment中只有一个ListView——AgendaListView,给该ListView设置适配器,源码如下:
setAdapter(mWindowAdapter);
需要适配器对象,实例化适配器时,通过重写getView()方法加载布局。核心代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
final View v;
DayAdapterInfo info = getAdapterInfoByPosition(position);
if (info != null) {
int offset = position - info.offset;
v = info.dayAdapter.getView(offset, convertView,
parent);
} else {
TextView tv = new TextView(mContext);
tv.setText("Bug! " + position);
v = tv;
}
DayAdapterInfo是AgendaWindowAdapter的内部类,这个类中将AgendaByDayAdapter的对象作为它的一个属性,也就是说DayAdapterInfo可以持有AgendaByDayAdapter的对象,通过DayAdapterInfo对象获取AgendaByDayAdapter的实例从而开始调用AgendaByDayAdapter的getView()方法。
在AgendaByDayAdapter中,有一个内部类RowInfo,主要作用是将数据库中查询到的事件信息作为它的属性,使用时可通过实例化它的对象进行获取。其中有个属性mType,区分是否是一个事件 (TYPE_DAY or an event TYPE_MEETING)。在getView()方法中,通过RowInfo.mType进行判断,从而进行不同条目布局的加载,核心代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
RowInfo row = mRowInfo.get(position);
/*是日期条目,也就是2区域*/
if (row.mType == TYPE_DAY) {
ViewHolder holder = null;
View agendaDayView = null;
if (holder == null) {
holder = new ViewHolder();
agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
holder.dayView = (TextView) agendaDayView.findViewById(R.id.day);
holder.dateView = (TextView) agendaDayView.findViewById
(R.id.date);
}
/*是一个事件*/
} else if (row.mType == TYPE_MEETING) {
View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);
AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag());
return itemView;
}
从上述代码中可以看出,当需要加载的数据项为日期时,直接加载布局,当需要加载的数据项为事件时,调用AgendaAdapter的getView()进行加载.在AgendaAdapter中,通过bindView()绑定布局文件。流程图如下: