Android源码之DeskClock (四)

时间:2022-12-28 10:09:45

一.概述

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