[置顶] 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

你可能感兴趣的:(源码,android,DeskClock,原生程序)