每一个不曾起舞的日子,都是对生命的辜负。-----尼采
最近关注功耗问题,顺便看了下Settings模块中Battery界面。这块的UI还是写的挺不错的,在此分享下。
Battery界面分析
下图是我在看该界面时,脑中的一些疑惑点。
上图列出的三大块疑问,正是引起我好奇心的地方。先来一个一个说下当初自己想的实现方式。
- battery saver的跳转处理:这个界面跳转肯定是Preference里面弄个android:fragment属性,把跳转的fragment设置进来的,其中的Summary内容在跳转回来后会变动,那么这里实际上就是两个fragment之前通信问题,应该是接口回调实现的。
- 电量曲线的显示:这个是勾起我好奇心的罪魁祸首。整个界面是由Preference构建的,系统的Preference肯定实现不了这种效果,那么应该是自定义了一个Preference然后嵌套进来的。还没撸过自定义Preference,而且这个view还有点小复杂呢,曲线用path就可以搞定,关键是下方的渐变效果怎么搞呢?LinearGradient到是可以,但它填充规则图形还好用,电量曲线变化多端,如何保证曲线下方全部着上渐变色,上方空白呢?难道挨个计算曲线上的点,然后连接到底部,用LinearGradient着色?真要这样搞计算量有点大啊。
- 耗电排行的显示: 电量统计的数据肯定由系统接口上报,有个listpreference貌似可以将list嵌套在perference里呢,百度以下我应该就知道。
以上是我看到这个界面的一些想法。带着这点好奇心,来观摩下源码是如何给我解释的。
Battery界面如何实现
battery saver的跳转处理
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>
本小节我们关注的是BatterySaverPreference。没有悬念的用了
android:fragment="com.android.settings.fuelgauge.BatterySaverSettings"
将点击跳转的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);
}
...
}
原来是通过监听SettingsProvider数据库的值,去更新summary。这里提下两个知识点:
-
registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,@NonNull ContentObserver observer)
有三个参数,第二个bool类型参数的为true则所监听的uri的子uri如果内容有变化也会监听到。为false则只监听匹配的uri及其父uri。 - ContentObserver在数据变化后回调方法却没有走,排除监听了错误的uri,需要去ContentProvider的update/insert/delete方法去检查是否调用了notifyChange方法。
电量曲线项的实现
从power_usage_summary.xml文件中,可以得知电量曲线项的加载是一个自定义控件BatteryHistoryPreference。
查看
packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryHistoryPreference.java
文件,其继承的是v7包下的Preference,在构造方法里通过setLayoutResource(R.layout.battery_usage_graph);
将布局加载进来,向外暴露
setStats(BatteryStatsHelper batteryStats)
方法获取显示数据,在
onBindViewHolder
方法里更新数据显示。
通过uiautomatorviewer工具来重点看下这个布局。
通过上图非常直观的展现出电量曲线视图的构成,
最感兴趣的usage_graph视图被包含在自定义控件UsageView中,也就是自定义控件嵌套自定义控件。
usage_graph视图id对应的是UsageGraph类,它直接继承自View类。它是如何被层层嵌套进Preference的问题已经明了,来重点看看:
1.UsageGraph如何去绘制电量曲线。
2.下方的阴影如何实现
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方法移动了画笔,但却不绘制内容,正好处理电量信息为null的情况,而lineTo方法用来绘制直线,串联起各个电量信息点。
最终调用canvas.drawPath,将电量曲线绘制出来。代码对应的视图如下图。
- 电量曲线下方的阴影如何实现
绘制阴影的核心方法
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();//让绘制的各个点形成闭环,从而得到一个封闭的区域,后续通过画笔对该区域着色 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}
上述值对应的代码视图如下: ![电量曲线阴影code-view图](http://upload-images.jianshu.io/upload_images/2912789-acb27cd4c32e44b3?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 闭合区域形成了,下来就该用画笔填充这些区域。此处用到的画笔mFillPaint设置了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填充这种不规则图像计算量过大的疑虑,利用path标记闭合区域,在用Style.FILL画笔着色,计算量大的疑虑也就没有了。
- 电量曲线呈虚线如何绘制
虚线表明的是系统预测电量变化的走势。电量曲线虚线绘制核心方法
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,表明当前电池处于充电状态,预测虚线走势向上,反之向下
mPath.lineTo(canvas.getWidth(), mProjectUp ? 0 : canvas.getHeight());
canvas.drawPath(mPath, mDottedPaint);
}
```
分析了之前两个疑问,这里path的绘制就更简单了,无需多讲。但这里的画笔--mDottedPaint比较特殊,它用到了DashPathEffect来实现虚线效果。具体实现如下
```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));
```
之前有同事问过一个问题,当进入省电模式后,预期的虚线应该有变化才对,从以上分析看,虚线的绘制只是简单的绘制了一条虚线,充电时向上延生至顶部,非充电时向下延生至底部。因此当然不会有变化了。
耗电排行的显示
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实现,而是用到了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);
...
}
...
}
}
总结
通过走读源码,看到了path在绘制曲线时的强大功能。另外也看到了源码在存储电量数据时用到了SparseIntArray,其相对与传统的HashMap,避免了自动装箱动作,转而用两个int 数组来存放key-value的映射关系,降低了内存开销,不过当数据量过大时(好几百项),进行add/remove操作效率会比不上HashMap,这是由于SparseIntArray在查找key时用到了二分查找,数据越大,二分查找的效率就越低,同时add/remove操作会使得整个int数组的内容位置都要改变。
在没有看到源码实现方案时,以为电量显示的view有什么高深莫测的实现方式,实则不然,对path有过了解后,实现起来是很easy的。真正的难点还是在电量数据的获取,以及view的视图组织上。