Android Battery视图界面分析

来源:互联网 发布:桔子桌面软件 编辑:程序博客网 时间:2024/05/19 17:03

  • Battery界面分析
  • Battery界面如何实现
    • battery saver的跳转处理
    • 电量曲线项的实现
    • 耗电排行的显示
  • 总结

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

最近关注功耗问题,顺便看了下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图
    闭合区域形成了,下来就该用画笔填充这些区域。此处用到的画笔mFillPaint设置了Style.FILL

    mFillPaint.setStyle(Style.FILL);

    并且确实如当初预期的用到了LinearGradient

        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
      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来实现虚线效果。具体实现如下

    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的视图组织上。

原创粉丝点击