当前位置: 代码迷 >> Android >> Android源码之DeskClock (4)
  详细解决方案

Android源码之DeskClock (4)

热度:74   发布时间:2016-04-27 23:33:16.0
Android源码之DeskClock (四)

一.概述

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

  相关解决方案