Android Battery視圖界面分析

每一個不曾起舞的日子澄步,都是對生命的辜負南缓。-----尼采

最近關(guān)注功耗問題蛀骇,順便看了下Settings模塊中Battery界面厌秒。這塊的UI還是寫的挺不錯的,在此分享下擅憔。

Battery界面分析

下圖是我在看該界面時鸵闪,腦中的一些疑惑點。


Battery界面

上圖列出的三大塊疑問暑诸,正是引起我好奇心的地方蚌讼。先來一個一個說下當初自己想的實現(xiàn)方式辟灰。

  • battery saver的跳轉(zhuǎn)處理:這個界面跳轉(zhuǎn)肯定是Preference里面弄個android:fragment屬性,把跳轉(zhuǎn)的fragment設(shè)置進來的篡石,其中的Summary內(nèi)容在跳轉(zhuǎn)回來后會變動翅雏,那么這里實際上就是兩個fragment之前通信問題哮兰,應(yīng)該是接口回調(diào)實現(xiàn)的。
  • 電量曲線的顯示:這個是勾起我好奇心的罪魁禍首。整個界面是由Preference構(gòu)建的玉锌,系統(tǒng)的Preference肯定實現(xiàn)不了這種效果央渣,那么應(yīng)該是自定義了一個Preference然后嵌套進來的求类。還沒擼過自定義Preference,而且這個view還有點小復雜呢矫膨,曲線用path就可以搞定,關(guān)鍵是下方的漸變效果怎么搞呢瘦材?LinearGradient到是可以厅须,但它填充規(guī)則圖形還好用,電量曲線變化多端食棕,如何保證曲線下方全部著上漸變色朗和,上方空白呢?難道挨個計算曲線上的點簿晓,然后連接到底部眶拉,用LinearGradient著色?真要這樣搞計算量有點大啊憔儿。
  • 耗電排行的顯示: 電量統(tǒng)計的數(shù)據(jù)肯定由系統(tǒng)接口上報忆植,有個listpreference貌似可以將list嵌套在perference里呢,百度以下我應(yīng)該就知道谒臼。

以上是我看到這個界面的一些想法朝刊。帶著這點好奇心,來觀摩下源碼是如何給我解釋的蜈缤。

Battery界面如何實現(xiàn)

battery saver的跳轉(zhuǎn)處理

packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java拾氓,
該類為Battery界面的主類。它繼承至PreferenceFragment.要想見識下Preference的各種花式用法底哥,源碼中的Settings模塊絕對是不二選擇咙鞍。
找到其加載的xml文件。
packages/apps/Settings/res/xml/power_usage_summary.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
        android:title="@string/power_usage_summary_title"
        settings:keywords="@string/keywords_battery">
        <com.android.settings.fuelgauge.BatterySaverPreference
            android:title="@string/battery_saver"
            android:fragment="com.android.settings.fuelgauge.BatterySaverSettings" />
        <SwitchPreference
            android:key="battery_pct"
            android:title="@string/show_battery_percentage"
            android:summary="@string/show_battery_percentage_summary"
            android:persistent="false" />
        <com.android.settings.fuelgauge.BatteryHistoryPreference
            android:key="battery_history" />
        <PreferenceCategory
            android:key="app_list"
            android:title="@string/power_usage_list_summary" />
</PreferenceScreen>

本小節(jié)我們關(guān)注的是BatterySaverPreference趾徽。沒有懸念的用了
android:fragment="com.android.settings.fuelgauge.BatterySaverSettings"
將點擊跳轉(zhuǎn)的BatterySaverSettings引入進來续滋。它自身自定義了BatterySaverPreference,注意到xml里只申明了title跟fragment孵奶,缺少了summary屬性疲酌,看看自定義的BatterySaverPreference是如何處理summary更新請求的。
packages/apps/Settings/src/com/android/settings/fuelgauge/BatterySaverPreference.java

public class BatterySaverPreference extends Preference {
    ...
    @Override
    public void onAttached() {
        super.onAttached();
        mPowerManager = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
        mObserver.onChange(true);
        getContext().getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(Global.LOW_POWER_MODE_TRIGGER_LEVEL), true, mObserver);
        getContext().getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(Global.LOW_POWER_MODE), true, mObserver);
    }
    ...
}

原來是通過監(jiān)聽SettingsProvider數(shù)據(jù)庫的值拒课,去更新summary徐勃。這里提下兩個知識點:

  1. registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,@NonNull ContentObserver observer)
    有三個參數(shù),第二個bool類型參數(shù)的為true則所監(jiān)聽的uri的子uri如果內(nèi)容有變化也會監(jiān)聽到早像。為false則只監(jiān)聽匹配的uri及其父uri僻肖。
  2. ContentObserver在數(shù)據(jù)變化后回調(diào)方法卻沒有走,排除監(jiān)聽了錯誤的uri卢鹦,需要去ContentProvider的update/insert/delete方法去檢查是否調(diào)用了notifyChange方法臀脏。

電量曲線項的實現(xiàn)

從power_usage_summary.xml文件中,可以得知電量曲線項的加載是一個自定義控件BatteryHistoryPreference冀自。
查看
packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java
文件揉稚,其繼承的是v7包下的Preference,在構(gòu)造方法里通過setLayoutResource(R.layout.battery_usage_graph);
將布局加載進來熬粗,向外暴露
setStats(BatteryStatsHelper batteryStats)
方法獲取顯示數(shù)據(jù)搀玖,在
onBindViewHolder
方法里更新數(shù)據(jù)顯示。
通過uiautomatorviewer工具來重點看下這個布局驻呐。

電量曲線視圖構(gòu)成

通過上圖非常直觀的展現(xiàn)出電量曲線視圖的構(gòu)成灌诅,
最感興趣的usage_graph視圖被包含在自定義控件UsageView中,也就是自定義控件嵌套自定義控件含末。

usage_graph視圖id對應(yīng)的是UsageGraph類猜拾,它直接繼承自View類。它是如何被層層嵌套進Preference的問題已經(jīng)明了佣盒,來重點看看:
1.UsageGraph如何去繪制電量曲線挎袜。
2.下方的陰影如何實現(xiàn)
3.另外還注意到有時電量曲線呈虛線,這個又是怎么出來的呢肥惭。

  • UsageGraph如何去繪制電量曲線
    繪制電量曲線的核心方法
    frameworks/base/packages/SettingsLib/graph/UsageGraph.java
        private void drawLinePath(Canvas canvas) {
            mPath.reset();
            mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
                int x = mLocalPaths.keyAt(i);
                int y = mLocalPaths.valueAt(i);
                if (y == PATH_DELIM) { //PATH_DELIM為-1盯仪,這個分支語句用來處理電量信息為null的情況
                    if (++i < mLocalPaths.size()) {
                        mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
                    }
                } else {
                    mPath.lineTo(x, y);
                }
            }
            canvas.drawPath(mPath, mLinePaint);
        }
    

這里主要用到了path類,其中moveTo方法移動了畫筆蜜葱,但卻不繪制內(nèi)容磨总,正好處理電量信息為null的情況,而lineTo方法用來繪制直線笼沥,串聯(lián)起各個電量信息點蚪燕。
最終調(diào)用canvas.drawPath,將電量曲線繪制出來奔浅。代碼對應(yīng)的視圖如下圖馆纳。


電量曲線code-view圖
  • 電量曲線下方的陰影如何實現(xiàn)
    繪制陰影的核心方法
    frameworks/base/packages/SettingsLib/graph/UsageGraph.java
    private void drawFilledPath(Canvas canvas) {
            mPath.reset();
            float lastStartX = mLocalPaths.keyAt(0);
            mPath.moveTo(mLocalPaths.keyAt(0), mLocalPaths.valueAt(0));
            for (int i = 1; i < mLocalPaths.size(); i++) {
                int x = mLocalPaths.keyAt(i);
                int y = mLocalPaths.valueAt(i);
                if (y == PATH_DELIM) {
                    mPath.lineTo(mLocalPaths.keyAt(i - 1), getHeight());
                    mPath.lineTo(lastStartX, getHeight());
                    mPath.close();//讓繪制的各個點形成閉環(huán),從而得到一個封閉的區(qū)域汹桦,后續(xù)通過畫筆對該區(qū)域著色
                    if (++i < mLocalPaths.size()) {
                        lastStartX = mLocalPaths.keyAt(i);
                        mPath.moveTo(mLocalPaths.keyAt(i), mLocalPaths.valueAt(i));
                    }prefe
                } else {
                    mPath.lineTo(x, y);
                }
            }
            canvas.drawPath(mPath, mFillPaint);
        }
    
    看過電量曲線的繪制過程鲁驶,再看該方法就沒有懸念了,mLocalPaths的值類似如下形式:

mLocalPaths.toString={0=2, 2=14, 4=27, 7=39, 9=52, 11=64, 13=76, 15=89,17=101,
19=114, 21=126, 24=139, 25=151, 27=164, 29=176, 30=189,
32=201,34=-1, 422=205, 431=193, 435=180, 438=168, 441=156, 444=143, 448=131, 453=118, 459=106,
465=93, 470=81, 482=85, 494=85, 506=89, 518=93, 530=95, 541=108, 544=116, 545=-1}
上述值對應(yīng)的代碼視圖如下: ![電量曲線陰影code-view圖](http://upload-images.jianshu.io/upload_images/2912789-acb27cd4c32e44b3?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 閉合區(qū)域形成了舞骆,下來就該用畫筆填充這些區(qū)域钥弯。此處用到的畫筆mFillPaint設(shè)置了Style.FILLjava
mFillPaint.setStyle(Style.FILL);
并且確實如當初預期的用到了LinearGradientjava
private void updateGradient() {
mFillPaint.setShader(new LinearGradient(0, 0, 0, getHeight(),
getColor(mAccentColor, .2f), 0, TileMode.CLAMP));
}
```
在回顧當初擔心的LinearGradient填充這種不規(guī)則圖像計算量過大的疑慮径荔,利用path標記閉合區(qū)域,在用Style.FILL畫筆著色脆霎,計算量大的疑慮也就沒有了总处。

- 電量曲線呈虛線如何繪制
虛線表明的是系統(tǒng)預測電量變化的走勢。電量曲線虛線繪制核心方法
frameworks/base/packages/SettingsLib/graph/UsageGraph.java
```java
private void drawProjection(Canvas canvas) {
    mPath.reset();
    int x = mLocalPaths.keyAt(mLocalPaths.size() - 2);
    int y = mLocalPaths.valueAt(mLocalPaths.size() - 2);
    mPath.moveTo(x, y);
    //mProjectUp為true睛蛛,表明當前電池處于充電狀態(tài)鹦马,預測虛線走勢向上,反之向下
    mPath.lineTo(canvas.getWidth(), mProjectUp ? 0 : canvas.getHeight());
    canvas.drawPath(mPath, mDottedPaint);
}
```
分析了之前兩個疑問忆肾,這里path的繪制就更簡單了荸频,無需多講。但這里的畫筆--mDottedPaint比較特殊客冈,它用到了DashPathEffect來實現(xiàn)虛線效果旭从。具體實現(xiàn)如下
```java
mDottedPaint = new Paint(mLinePaint);
mDottedPaint.setStyle(Style.STROKE);
float dots = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_size);
float interval = resources.getDimensionPixelSize(R.dimen.usage_graph_dot_interval);
mDottedPaint.setStrokeWidth(dots * 3);
mDottedPaint.setPathEffect(new DashPathEffect(new float[] {dots, interval}, 0));
mDottedPaint.setColor(context.getColor(R.color.usage_graph_dots));
```
之前有同事問過一個問題,當進入省電模式后场仲,預期的虛線應(yīng)該有變化才對遇绞,從以上分析看,虛線的繪制只是簡單的繪制了一條虛線燎窘,充電時向上延生至頂部摹闽,非充電時向下延生至底部。因此當然不會有變化了褐健。

耗電排行的顯示

packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java

public class PowerUsageSummary extends PowerUsageBase {
...
@Override
    public void onCreate(Bundle icicle) {
        addPreferencesFromResource(R.xml.power_usage_summary);
    }
...
}

packages/apps/Settings/res/xml/power_usage_summary.xml

...
<PreferenceCategory
    android:key="app_list"
    android:title="@string/power_usage_list_summary" />
...

這里并不是預期的用listpreference實現(xiàn)付鹿,而是用到了PreferenceGroup,然后將每一個子耗電項add進來的蚜迅。

public class PowerUsageSummary extends PowerUsageBase {
    private static final String KEY_APP_LIST = "app_list";
    private PreferenceGroup mAppListGroup;
    ...
    @Override
    public void onCreate(Bundle icicle) {
        ...
        mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST);
        ...
    }
    protected void refreshStats() {
        ...
        final int numSippers = usageList.size();
        for (int i = 0; i < numSippers; i++) {
            ...
            mAppListGroup.addPreference(pref);
            ...
        }
        ...
    }
}

總結(jié)

通過走讀源碼舵匾,看到了path在繪制曲線時的強大功能。另外也看到了源碼在存儲電量數(shù)據(jù)時用到了SparseIntArray谁不,其相對與傳統(tǒng)的HashMap坐梯,避免了自動裝箱動作,轉(zhuǎn)而用兩個int 數(shù)組來存放key-value的映射關(guān)系刹帕,降低了內(nèi)存開銷吵血,不過當數(shù)據(jù)量過大時(好幾百項),進行add/remove操作效率會比不上HashMap偷溺,這是由于SparseIntArray在查找key時用到了二分查找蹋辅,數(shù)據(jù)越大,二分查找的效率就越低挫掏,同時add/remove操作會使得整個int數(shù)組的內(nèi)容位置都要改變侦另。

在沒有看到源碼實現(xiàn)方案時,以為電量顯示的view有什么高深莫測的實現(xiàn)方式,實則不然褒傅,對path有過了解后弃锐,實現(xiàn)起來是很easy的。真正的難點還是在電量數(shù)據(jù)的獲取殿托,以及view的視圖組織上霹菊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碌尔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌券敌,老刑警劉巖唾戚,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異待诅,居然都是意外死亡叹坦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門卑雁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來募书,“玉大人,你說我怎么就攤上這事测蹲∮瘢” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵扣甲,是天一觀的道長篮赢。 經(jīng)常有香客問我,道長琉挖,這世上最難降的妖魔是什么启泣? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮示辈,結(jié)果婚禮上寥茫,老公的妹妹穿的比我還像新娘。我一直安慰自己矾麻,他們只是感情好纱耻,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著险耀,像睡著了一般膝迎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胰耗,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天限次,我揣著相機與錄音,去河邊找鬼。 笑死卖漫,一個胖子當著我的面吹牛费尽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播羊始,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼旱幼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了突委?” 一聲冷哼從身側(cè)響起柏卤,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匀油,沒想到半個月后缘缚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡敌蚜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年桥滨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弛车。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡齐媒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纷跛,到底是詐尸還是另有隱情喻括,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布贫奠,位于F島的核電站双妨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏叮阅。R本人自食惡果不足惜刁品,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望浩姥。 院中可真熱鬧挑随,春花似錦、人聲如沸勒叠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽眯分。三九已至拌汇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弊决,已是汗流浹背噪舀。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工魁淳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人与倡。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓界逛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纺座。 傳聞我的和親對象是個殘疾皇子息拜,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容