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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章