Android Battery视图界面分析

每一个不曾起舞的日子,都是对生命的辜负。-----尼采

最近关注功耗问题,顺便看了下Settings模块中Battery界面。这块的UI还是写的挺不错的,在此分享下。

Battery界面分析

下图是我在看该界面时,脑中的一些疑惑点。


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。这里提下两个知识点:

  1. registerContentObserver(@NonNull Uri uri, boolean notifyForDescendants,@NonNull ContentObserver observer)
    有三个参数,第二个bool类型参数的为true则所监听的uri的子uri如果内容有变化也会监听到。为false则只监听匹配的uri及其父uri。
  2. 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.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方法移动了画笔,但却不绘制内容,正好处理电量信息为null的情况,而lineTo方法用来绘制直线,串联起各个电量信息点。
最终调用canvas.drawPath,将电量曲线绘制出来。代码对应的视图如下图。


电量曲线code-view图
  • 电量曲线下方的阴影如何实现
    绘制阴影的核心方法
    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();//让绘制的各个点形成闭环,从而得到一个封闭的区域,后续通过画笔对该区域着色
                    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}
上述值对应的代码视图如下: ![电量曲线阴影code-view图](http://upload-images.jianshu.io/upload_images/2912789-acb27cd4c32e44b3?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 闭合区域形成了,下来就该用画笔填充这些区域。此处用到的画笔mFillPaint设置了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填充这种不规则图像计算量过大的疑虑,利用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的视图组织上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容