一.概述
之前寫三的時候饒了個彎,通過DeskClock這個項目簡單實現了一下加固+熱修復,在這篇繼續回到正規繼續分析源碼.在二里面大致分析了DeskClock的主入口,跟四個主要功能Fragment的轉換,從這篇開始就着手分析這四大功能.先從Clock功能的Fragment開始講起.
二.源碼分析
1.onCreateView
這裏根據ClockFragment生命週期的順序分析,首先是onCreateView,這裏做的工作就是裝載佈局文件,初始化控件適配器和聲明監聽.
這裏佈局分橫屏和豎屏兩種,整體的結構是以listview爲主,掛載header,footer,menu和選擇城市構成.所以除了通用的控件,在初始化控件的時候需要區分橫屏豎屏.這裏時鐘的佈局在橫屏的時候是跟listview分開的,而在豎屏的時候是作爲listview的headerview存在的,所以源碼中就先去獲取橫屏中的clock的view,如果爲空說明當前是豎屏的佈局直接inflate出來掛到listview的headerview上.
// On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
// on as a header to the main listview.
mClockFrame = v.findViewById(R.id.main_clock_left_pane);
if (mClockFrame == null) {
mClockFrame = inflater.inflate(R.layout.main_clock_frame, mList, false);
mList.addHeaderView(mClockFrame, null, false);
} else {
// The main clock frame needs its own touch listener for night mode now.
v.setOnTouchListener(longPressNightMode);
}
mList.setOnTouchListener(longPressNightMode);
從上面的源碼看到橫屏的時候在Clock的view上和豎屏的時候listview上都設置了同一個TouchListener,從監聽的名字能感覺到是長按之後進入夜間模式的作用.爲什麼Android提供了長按的監聽(setOnLongClickListener),爲什麼還要騷騷得自己寫長按的監聽,當然自己寫長按監聽可以定製更加細節的規則,例如長按的時間,長按時滑動的容錯處理等.在初始化的時候通過ViewConfiguration中的配置進行填充容錯偏移和長按觸發的時間值,當監聽到用戶按下屏幕後通過handler
post一個進入夜間模式頁面的延遲消息到message queue並記錄當前Down的座標,之後如果用戶滑動的話就根據記錄的touch座標計算滑動的偏移量,當偏移量大於容錯時就把之前的消息從message queue中移除掉.如果用戶長按的時候沒有達到設定並離開屏幕的話也會執行default中的移除消息.OnTouchListener longPressNightMode = new OnTouchListener() {
private float mMaxMovementAllowed = -1;
private int mLongPressTimeout = -1;
private float mLastTouchX, mLastTouchY;
@Override
public boolean onTouch(View v, MotionEvent event) {
if (mMaxMovementAllowed == -1) {
mMaxMovementAllowed = ViewConfiguration.get(getActivity()).getScaledTouchSlop();
mLongPressTimeout = ViewConfiguration.getLongPressTimeout();
}
switch (event.getAction()) {
case (MotionEvent.ACTION_DOWN):
long time = Utils.getTimeNow();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
startActivity(new Intent(getActivity(), ScreensaverActivity.class));
}
}, mLongPressTimeout);
mLastTouchX = event.getX();
mLastTouchY = event.getY();
return true;
case (MotionEvent.ACTION_MOVE):
float xDiff = Math.abs(event.getX()-mLastTouchX);
float yDiff = Math.abs(event.getY()-mLastTouchY);
if (xDiff >= mMaxMovementAllowed || yDiff >= mMaxMovementAllowed) {
mHandler.removeCallbacksAndMessages(null);
}
break;
default:
mHandler.removeCallbacksAndMessages(null);
}
return false;
}
};
2.onResume
此時註冊SharedPreferenceChange監聽,當用戶在設置裏修改了時鐘樣式後會更新適配器,將listview中所有城市時間的item的樣式更新一下.並且當前Clock的樣式也是在onResume裏面設置的,用戶設置完時鐘樣式後回到主頁面會重新調用onResume,這樣所有的樣式更改後就全部生效了.
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
if (key == SettingsActivity.KEY_CLOCK_STYLE) {
mClockStyle = prefs.getString(SettingsActivity.KEY_CLOCK_STYLE, mDefaultClockStyle);
mAdapter.notifyDataSetChanged();
}
}
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
String defaultClockStyle = context.getResources().getString(R.string.default_clock_style);
String style = sharedPref.getString(clockStyleKey, defaultClockStyle);
View returnView;
if (style.equals(CLOCK_TYPE_ANALOG)) {
digitalClock.setVisibility(View.GONE);
analogClock.setVisibility(View.VISIBLE);
returnView = analogClock;
} else {
digitalClock.setVisibility(View.VISIBLE);
analogClock.setVisibility(View.GONE);
returnView = digitalClock;
}
開啓每刻鐘更新一下日期UI的異步任務.單看這一點就沒有問題的,但是每次捕獲到時間變化的廣播和UI onResume的時候都回去更新日期,那爲什麼還要開啓這個重複的校驗.不僅僅是同步日期,下面的同步時間和同步鬧鐘都做了雙重重複的校驗(標註**的地方).我get不到google工程師這麼做的點是什麼,希望跟能感覺到他們這麼幹的意圖的童鞋交流下.
Utils.setQuarterHourUpdater(mHandler, mQuarterHourUpdater);
// Thread that runs on every quarter-hour and refreshes the date.
private final Runnable mQuarterHourUpdater = new Runnable() {
@Override
public void run() {
// Update the main and world clock dates
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
if (mAdapter != null) {
mAdapter.notifyDataSetChanged();
}
Utils.setQuarterHourUpdater(mHandler, mQuarterHourUpdater);
}
};
這裏還是要監聽幾個系統廣播來更新日期和城市列表等.因爲時鐘UI上還是有鬧鐘信息的,所以也要監聽自定義的鬧鐘廣播來刷新鬧鐘信息的展示.
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
boolean changed = action.equals(Intent.ACTION_TIME_CHANGED)
|| action.equals(Intent.ACTION_TIMEZONE_CHANGED)
|| action.equals(Intent.ACTION_LOCALE_CHANGED);
if (changed) {
Utils.updateDate(mDateFormat, mDateFormatForAccessibility,mClockFrame);
if (mAdapter != null) {
// *CHANGED may modify the need for showing the Home City
if (mAdapter.hasHomeCity() != mAdapter.needHomeCity()) {
mAdapter.reloadData(context);
} else {
mAdapter.notifyDataSetChanged();
}
// Locale change: update digital clock format and
// reload the cities list with new localized names
if (action.equals(Intent.ACTION_LOCALE_CHANGED)) {
if (mDigitalClock != null) {
Utils.setTimeFormat(
(TextClock)(mDigitalClock.findViewById(R.id.digital_clock)),
(int)context.getResources().
getDimension(R.dimen.bottom_text_size));
}
mAdapter.loadCitiesDb(context);
mAdapter.notifyDataSetChanged();
}
}
Utils.setQuarterHourUpdater(mHandler, mQuarterHourUpdater);
}
if (changed || action.equals(AlarmNotifications.SYSTEM_ALARM_CHANGE_ACTION)) {
Utils.refreshAlarm(getActivity(), mClockFrame);
}
}
};
最後還註冊了一個數據庫變化的監聽,其實這個監聽跟上面的廣播是重複的,當最新的鬧鐘時間被更改了之後會接到一個刷新鬧鐘UI的廣播和數據庫的監聽,他們都是做的同一個操作.(**)
activity.getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED),
false,
mAlarmObserver);
private final Handler mHandler = new Handler();
private final ContentObserver mAlarmObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
Utils.refreshAlarm(ClockFragment.this.getActivity(), mClockFrame);
}
};
3.onPause
在onResume裏面註冊了一系列的服務,與之相對應得就要在onPause裏面解綁與onResume註冊相對應的服務.
@Override
public void onPause() {
super.onPause();
mPrefs.unregisterOnSharedPreferenceChangeListener(this);
Utils.cancelQuarterHourUpdater(mHandler, mQuarterHourUpdater);
Activity activity = getActivity();
activity.unregisterReceiver(mIntentReceiver);
activity.getContentResolver().unregisterContentObserver(mAlarmObserver);
}
4.AnalogClock
在設置中提供了兩種錶盤,一種是數字表盤一種是指針錶盤,在DeskClock中數字表盤使用的TextClock,而指針錶盤是自定義的.錶盤的繪製這裏就不說了.既然是自定義的,就要能夠讓時間同步系統時間,這裏主要是監聽了android.intent.action.TIME_TICK廣播,該廣播由系統每分鐘整點的時候發出,可以用來做定時時間校準.再開啓一個每1000毫秒執行一次的異步任務,去獲取當前時間更新指針的變化.
private final Runnable mClockTick = new Runnable () {
@Override
public void run() {
onTimeChanged();
invalidate();
AnalogClock.this.postDelayed(mClockTick, 1000);
}
};
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
String tz = intent.getStringExtra("time-zone");
mCalendar = new Time(TimeZone.getTimeZone(tz).getID());
}
onTimeChanged();
invalidate();
}
};
上面兩個方法都用來確保DeskClock的時間和系統一致.個人感覺這裏監聽TIME_TICK廣播有些多餘(**),因爲異步任務每次執行都會去校準時間.每次onTimeChanged被調用的時候最先做的就是校準當前時間,更改指針的屬性,等待invalidate重新繪製.最後部分的setContentDescription是開啓了系統輔助功能中的TalkBack功能之後設置內容描述Android系統會把設置的內容TTS讀出來(跟一中的RTL一樣都是比較冷門的用法).
private void onTimeChanged() {
mCalendar.setToNow();
if (mTimeZoneId != null) {
mCalendar.switchTimezone(mTimeZoneId);
}
int hour = mCalendar.hour;
int minute = mCalendar.minute;
int second = mCalendar.second;
// long millis = System.currentTimeMillis() % 1000;
mSeconds = second;//(float) ((second * 1000 + millis) / 166.666);
mMinutes = minute + second / 60.0f;
mHour = hour + mMinutes / 60.0f;
mChanged = true;
updateContentDescription(mCalendar);
}
5.ScreenSaverActivity
ScreenSaverActivity還是比較有意思的,當手機在充電狀態下ScreenSaver會運行在鎖屏頁面之上,所以就要用到各種各樣的廣播來控制ScreenSaver的各種狀態.首先在onStart的時候註冊時間相關,充電相關和用戶解鎖屏幕的廣播,註冊監聽存放下條鬧鐘數據的數據庫變化的observer.
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_POWER_CONNECTED);
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
filter.addAction(Intent.ACTION_USER_PRESENT);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
registerReceiver(mIntentReceiver, filter);
getContentResolver().registerContentObserver(
Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED),
false,
mSettingsContentObserver);
如果監聽到時間或時區變化的廣播,就更新日期和鬧鐘的UI數據.如果監聽到用戶解鎖屏幕就finish掉自己.如果當前設備正連接着外部電源,就啓動在鎖屏之上一直存活的模式.
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean changed = intent.getAction().equals(Intent.ACTION_TIME_CHANGED)
|| intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED);
if (intent.getAction().equals(Intent.ACTION_POWER_CONNECTED)) {
mPluggedIn = true;
setWakeLock();
} else if (intent.getAction().equals(Intent.ACTION_POWER_DISCONNECTED)) {
mPluggedIn = false;
setWakeLock();
} else if (intent.getAction().equals(Intent.ACTION_USER_PRESENT)) {
finish();
}
if (changed) {
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
Utils.setMidnightUpdater(mHandler, mMidnightUpdater);
}
}
};
這裏怎麼實現讓ScreenSaver運行在鎖屏之上的呢?需要先介紹幾個佈局參數屬性.
1) FLAG_DISMISS_KEYGUARD 解除鎖屏,運行在鎖屏之上的基礎
2) FLAG_SHOW_WHEN_LOCKED 讓當前View繪製在鎖屏頁面之上,點擊回退之後才能看到鎖屏頁面
3) FLAG_ALLOW_LOCK_WHILE_SCREEN_ON 當屏幕是開啓狀態的時候進行鎖屏操作
4) FLAG_KEEP_SCREEN_ON 讓屏幕一直保持開啓狀態,不受休眠的影響.
5) FLAG_FULLSCREEN 讓當前view爲全屏狀態
這些屬性都是通過16進制不同標誌位不同的值來區分,屬性疊加是通過或運算存儲.(例如FLAG_DISMISS_KEYGUARD | FLAG_SHOW_WHEN_LOCKED其實就是0x00400000 | 0x00080000 = 0x00480000 ,這樣兩個屬性就疊加起來了.)所以當前mFlags的總屬性就是解除鎖屏+在鎖屏的時候顯示+屏幕開啓的時候鎖屏+保持屏幕爲開啓狀態.
private final int mFlags = (WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
先給ScreenSaver設置上全屏的參數,如果當前頁面要運行在鎖屏之上的時候就通過或存運算,將上面mFlags的所有屬性都載入進來.如果要取消之前的操作怎麼辦呢? 要取消就需要把之前的或存的表達式和mFlags的值全部進行取反運算.
private void setWakeLock() {
Window win = getWindow();
WindowManager.LayoutParams winParams = win.getAttributes();
winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
if (mPluggedIn)
winParams.flags |= mFlags;
else
winParams.flags &= (~mFlags);
win.setAttributes(winParams);
}
只要前面接收到連接外部電源的廣播,就會開啓ScreenSaver模式,那如果我開啓ScreenSaverActivity之前插上的電源,然後開啓ScreenSaverActivity之後不是就接收不到這個廣播了嗎?當然這裏也處理了這個情況,當ScreenSaverActivity在onResume的時候會獲取一次當前電池的狀態,如果當前是插入座充或USB或高大上的無線充電都會開啓ScreenSaver模式. Intent chargingIntent =
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int plugged = chargingIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
mPluggedIn = plugged == BatteryManager.BATTERY_PLUGGED_AC
|| plugged == BatteryManager.BATTERY_PLUGGED_USB
|| plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS;
三.總結
這篇大致分析了DeskClock中Clock部分的主要功能實現,當然也有一些細節的地方沒有講解,例如AnalogClock錶盤指針的繪製,ScreenSaverActivity中表盤的移動動畫等.也發現了一些個人感覺不太妥當的代碼邏輯(標記**的日期時間鬧鐘UI數據同步部分),希望有想法(無論褒貶)的童鞋多多交流.
轉載請註明出處:http://blog.csdn.net/l2show/article/details/47298463