每一個不曾起舞的日子澄步,都是對生命的辜負南缓。-----尼采
最近關(guān)注功耗問題蛀骇,順便看了下Settings模塊中Battery界面厌秒。這塊的UI還是寫的挺不錯的,在此分享下擅憔。
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徐勃。這里提下兩個知識點:
-
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僻肖。 - 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工具來重點看下這個布局驻呐。
通過上圖非常直觀的展現(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.javaprivate 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)的視圖如下圖馆纳。
- 電量曲線下方的陰影如何實現(xiàn)
繪制陰影的核心方法
frameworks/base/packages/SettingsLib/graph/UsageGraph.java
看過電量曲線的繪制過程鲁驶,再看該方法就沒有懸念了,mLocalPaths的值類似如下形式: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.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.FILL
java
mFillPaint.setStyle(Style.FILL);
并且確實如當初預期的用到了LinearGradient
java
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的視圖組織上霹菊。