之前写三的时候饶了个弯,通过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