一.概述
之前写三的时候饶了个弯,通过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从上面的源码看到横屏的时候在Clock的view上和竖屏的时候listview上都设置了同一个TouchListener,从监听的名字能感觉到是长按之后进入夜间模式的作用.为什么Android提供了长按的监听(setOnLongClickListener),为什么还要骚骚得自己写长按的监听,当然自己写长按监听可以定制更加细节的规则,例如长按的时间,长按时滑动的容错处理等.在初始化的时候通过ViewConfiguration中的配置进行填充容错偏移和长按触发的时间值,当监听到用户按下屏幕后通过handler post一个进入夜间模式页面的延迟消息到message queue并记录当前Down的坐标,之后如果用户滑动的话就根据记录的touch坐标计算滑动的偏移量,当偏移量大于容错时就把之前的消息从message queue中移除掉.如果用户长按的时候没有达到设定并离开屏幕的话也会执行default中的移除消息.
// 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);
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() {这里怎么实现让ScreenSaver运行在锁屏之上的呢?需要先介绍几个布局参数属性.
@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);
}
}
};
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() {只要前面接收到连接外部电源的广播,就会开启ScreenSaver模式,那如果我开启ScreenSaverActivity之前插上的电源,然后开启ScreenSaverActivity之后不是就接收不到这个广播了吗?当然这里也处理了这个情况,当ScreenSaverActivity在onResume的时候会获取一次当前电池的状态,如果当前是插入座充或USB或高大上的无线充电都会开启ScreenSaver模式.
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);
}
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