Android之日曆原始碼淺析
前言:本文在整理過程中由於水平有限,若有不當之處,請指正!
1 常見介面及佈局的實現
1.1 日曆主介面:
日曆主介面是由AllInOneActivity實現,對應四種檢視型別動態載入相應的Fragment實現。各檢視如下:
(1) 日檢視:在AllInOneActivity上載入了DayFragment,DayFragment的佈局採用了自定義佈局DayView,而填充該佈局檔案時用到了ViewSwitcher,ViewSwitch是一個檢視切換元件,可以把多個檢視重疊在一起,而每次只顯示一個檢視,而給ViewSwitch而建立要顯示的View時,有兩種方式:既可以在xml檔案中新增,也可以通過實現
DayFragment的xml佈局:
Java程式碼:
日檢視效果如下:
(2) 周檢視:也是載入了DayFragment,效果如下:
(3) 月檢視:在AllInOneActivity上載入了MonthByWeekFragment,MonthByWeekFragment的佈局是一個自定義的MonthListView,而MonthByWeekFragment繼承了SimpleDayPickerFragment,這類繼承了ListFragment,ListFragment是一個自身帶有一個ListView
(4) 日程檢視:在AllInOneActivity上載入了AgendaFragment,AgendaFragment的佈局檔案也是使用了自定義的ListView:AgendaListView,並通過介面卡AdendaWindoeAdapter載入日程資料。效果如下:
1.2 新建活動介面
在EditEventActivity上動態載入了EditEventFragment
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()繫結佈局檔案。流程圖如下: