Android SystemUI 通知面板实现

前言

这篇文章给大家分享下Android SystemUI中下拉通知面板时所看到的开关面板(即QS面板)的实现原理,包括其整体架构,UI构建流程与事件处理流程,对这块感兴趣的同学可以看看

一. QS面板构成元素解析

QS面板实际上有多种状态,包括:

  • Quick Quick Settings (QQS) : 即初级展开面板,是一次下拉面板看到的简版QS面板,包含少量的开关,如下左侧的图
  • Quick Settings (QS) : 完整QS面板,是二次下拉面板看到的完成QS面板,其包含更多的开关,如下右侧的图
  • 另外还有开关编辑面板,开关详情页面,本文不展开描述

备注:SystemUI中称通知栏下拉面板开关区域中的单个开关为Tile

先看看整个QS面板中主要的几大类簇:

下面分别介绍各个类簇的主要作用

1. QSTile类簇,该类簇包括的主要类如下,类图如下

packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java
packages/SystemUI/src/com/android/systemui/qs/tiles/XxxTile.java

该类簇构成了的Tile的"后端",负责处理单个Tile的逻辑处理,其中:

  • QSTile : 接口,主要定义了所有Tile的通用行为,如注册监听,点击事件的处理,Tile视图中Icon元素(QSIconView)的构建,刷新Tile状态(state)等
  • QSTileImpl : 实现了QSTile 定义的通用行为,同时提供了一系列的抽象接口(详见类图)允许不同类型的子类去做差异化实现.后续所有的开关都需要继承自QSTileImpl. 如WifiTile,通过继承QSTileImpl来享用其提供的通用方法,不需要每个开关都去做实现,同时利用差异化接口便可实现开关自身的特有逻辑,如WifiTilehandleClick可以打开Wifi开关,而BluetoothTile的handleClick则可以打开蓝牙开关

2. QSTileView类簇 该类簇包括的主要类如下,类图如下

packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileView.java
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSIconView.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java

该类簇构成了的Tile的视图层,负责处理单个Tile的界面展示,其中:

  • qs/QSTileView : 抽象类,定义了Tile视图相关的操作,如点击事件设置,视图刷新onStateChanged(State state)
  • QSTileBaseView : 实现了QSTileView中定义的抽象接口,同时在其构造方法中完成了Tile视图的构建,包括背景的处理,点击效果的处理(如ripple),点击事件的处理等. 在QQS面板中使用
  • QSTileView : 注意与上面的同名抽象类区分开来,这里的起名容易让人疑惑,实际上这里的QSTileView继承了QSTileBaseView,在其构建的视图的基础上,扩展了label相关的东西,label即为开关中的文字描述视图,如蓝牙开关对应的label为"蓝牙". 在QS面板中使用

3. QSHost类簇,该类簇包括的主要类如下,类图如下

packages/SystemUI/src/com/android/systemui/qs/QSHost.java
packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSFactory.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java

该类簇主要完成单个Tile的构建,其中:

  • QSHost : 接口,主要向外界提供获取QSTile集合的接口(getTiles())以及作为Tile对外沟通的桥梁,例如点击某个开关后需要触发收起面板的操作,开关便会通过QSHost来触发收起面板的操作
  • QSTileHost : 实现了QSHost中定义的接口,同时扩展了创建Tile后端对象QSTile和创建Tile视图对象QSTileView的接口. 创建时使用了工厂模式,由QSFactoryImpl类实现

其中QSTileHost作为外界创建Tile的入口,会在对象构造的过程中先去创建Tile后端对象QSTile集合,这个集合在后续创建完整Tile对象时会用到. 具体创建哪些Tile则是通过获取配置在 config.xml 中的字段来决定了,具体过程可查看 QSTileHost.onTuningChanged(String key, String newValue) 方法

4. QSPanel类簇,该类簇包括的主要类如下,类图如下

packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
packages/SystemUI/src/com/android/systemui/qs/QSPanel#QSTileLayout
packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java

该类簇主要完成QS面板元素的动态添加,其中:

  • QSPanel : 对应我们前面说的QS面板,即二次展开面板,是该页面的顶层容器,其嵌套在一个ScrollView中. 负责动态添加相应的元素,添加的元素包括 [亮度条 / 根据屏幕方向动态添加开关容器 QSTileLayout / Footer ] 等. 同时为这些元素提供了一系列的操作接口,如

为开关容器创建具体开关对象(通过QSHost) 并为其添加刷新监听器,在后续开关后端收到开关状态刷新需求时,将刷新需求分发到对应的开关视图层.
开关详情页(Details)的创建与刷新
QS面板展开状态变化时做相应的处理(setExpanded(boolean expanded))

  • QuickQSPanel : 对应我们前面说的初级展开面板QQS面板,继承自QSPanel并对展示的Tile数做了限制(通过setMaxTiles(int)),同时复写了父类提供的添加子元素的方法,按需添加QQS面板的元素,因为QQS面板是QS面板的精简版,所以很多子元素未做添加
  • QSTileLayout : 开关容器接口,主要定义了开关容器绘制相关的接口
  • TileLayout : QQS面板开关容器类,负责精简QS面板的绘制,继承自 Viewgroup
  • PagedTileLayout : QS面板开关容器类,负责QS面板的绘制,继承自 ViewPager

5.QS类簇,该类簇包括的主要类如下,类图如下

packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java

该类簇主要作为整个QS面板的顶层容器,主要处理QS面板的展开/收起逻辑,其中:

  • QS : 接口,主要定义了 QS 面板展开/收起相关的接口
  • QSFragment : 继承自 Fragment,实现了 QS 接口,主要负责接收 QS 面板展开/收起状态的改变,并将最新状态同步给该 Fragment 中的 View 元素,如 QSContainerImpl
  • QSContainerImpl : 添加到QSFragment中的自定义 View,继承自FrameLayout对应布局 qs_panel.xml ,主要通过接收来自 QSFragment 的面板展开/收起状态的变化并做刷新

至此,整个QS面板中的关键类就介绍完了,下面将通过几条主线来洞悉整个QS面板的具体实现:

  • QS面板开关集合构建流程
  • Tile后端是如何与Tile视图层产生联系的
  • Tile的一次点击事件背后的流程是怎么样的

二 QS面板内部实现梳理

2.1 QS面板开关集合构建流程

QsPanel 中除了创建各个开关View,还创建了亮度条,Footer等元素,本文重心放在开关的构建上,故不会分析其他元素的创建流程. 另外QsFragment / QSHost 等元素是在 SystemUI 启动流程中通过注入或反射构建的,其前期构建流程跳过分析
先看看整体的流程图

流程比较简单,对照源码阅读即可.简单说就是QSTileHost对象在构建初期就借助QSFactoryImpl工具对象提前创建好了各个开关的后端对象QSTile,而后QSPanel在初始化的过程中,再次利用QSTileHost去构建各个开关的视图对象QSTileView,至此一个完整的开关就构建完成,最后add到开关容器PagedTileLayout中去.若对上述各个类的作用不清楚的话可回头看看前面对各个类簇的介绍

2.2 Tile后端 是如何与 Tile视图层 产生联系的

前面QSTile的类簇中我们可以看到其有多个内部类,与此相关的内部类包括 CallbackState

20240203009.png

Tile 视图与后端的联系就是借助这两个内部类以及QSPanel这个中介产生联系的,我们来看看代码.

前面 2.1小节我们在梳理QS面板开关集合构建流程时可以看到步骤10通过addTile函数来构建Tile开关对象,其代码细节如下

[packages/SystemUI/src/com/android/systemui/qs/QSPanel.java]

    public static final class TileRecord extends Record {
        public QSTile tile;
        public com.android.systemui.plugins.qs.QSTileView tileView;
        public boolean scanState;
        public QSTile.Callback callback;
    }

    protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
        final TileRecord r = new TileRecord();
        r.tile = tile;
        r.tileView = createTileView(tile, collapsedView); // 构建开关视图层

        final QSTile.Callback callback = new QSTile.Callback() {
            @Override
            public void onStateChanged(QSTile.State state) {
                drawTile(r, state);
            }
            ......
        };
        r.tile.addCallback(callback); // 向开关后端注册回调
        r.callback = callback;
        r.tileView.init(r.tile); // 初始化点击事件
        r.tile.refreshState(); // 首次刷新
        mRecords.add(r);
        mCachedSpecs = getTilesSpecs();
        if (mTileLayout != null) {
            mTileLayout.addTile(r); // add到开关容器
        }
        return r;
    }

    protected void drawTile(TileRecord r, QSTile.State state) {
        r.tileView.onStateChanged(state); // 将变化交给开关视图层
    }

可以看到QSPanelr.tile 即后端注册了一个回调器,并在回调发生时将开关状态State传递给 r.tileView 即开关视图层去做视图刷新.
至于这个State是在哪里刷新的这个我们2.3 小节再分析.

2.3 Tile的一次点击事件背后的流程是怎么样的

前面 2.2 小结介绍addTile函数时我们可以看到这么一句 r.tileView.init(r.tile) ; ,这里完成了将视图层的点击事件转交给后端的操作

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java]

    @Override
    public void init(QSTile tile) {
        init(v -> tile.click(), v -> tile.secondaryClick(), view -> {
            tile.longClick();
            return true;
        });
    }

    public void init(OnClickListener click, OnClickListener secondaryClick,
            OnLongClickListener longClick) {
        setOnClickListener(click);
        setOnLongClickListener(longClick);
    }

即将QSTileView收到的点击事件分别交给QSTile对应的点击函数处理

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java]

    public void click() {
        mHandler.sendEmptyMessage(H.CLICK);
    }

public void handleMessage(Message msg) {
    if (msg.what == CLICK) {
                    if (mState.disabledByPolicy) {
                        ......
                    } else {
                        handleClick();
                    }
                }
}

abstract protected void handleClick();

QSTileImplhandleClick() 函数是个抽象方法,分别由对应的开关子类去实现,例如Wifi开关

[packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java]

    @Override
    protected void handleClick() {
        ......
        refreshState(wifiEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
        ......
    }

又回到QSTileImpl

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java]

    protected final void refreshState(Object arg) {
        mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
    }

public void handleMessage(Message msg) {
  if (msg.what == REFRESH_STATE) {
                    handleRefreshState(msg.obj);
                }
}

    protected void handleRefreshState(Object arg) {
        handleUpdateState(mTmpState, arg);

        final boolean changed = mTmpState.copyTo(mState);
        if (changed) {
            handleStateChanged();
        }
        ...
    }

    abstract protected void handleUpdateState(TState state, Object arg);

可以看到QSTileImpl用一个State类型的临时变量去handleUpdateState函数中收集当前开关的最新状态,而这个函数是个抽象方法,实现依旧是在各个开关子类中,我们看下Wifi开关

[packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java]

    @Override
    protected void handleUpdateState(SignalState state, Object arg) {
            state.state = Tile.STATE_ACTIVE;
            state.dualTarget = true;
            state.value = transientEnabling || cb.enabled;
            state.activityIn = cb.enabled && cb.activityIn;
            state.activityOut = cb.enabled && cb.activityOut;
}

这里抽取了一些代码片段,可以看到开关后端会根据当前开关状态对 state 进行赋值,这些赋值会在后续开关视图刷新时产生作用

handleUpdateState函数收集完开关状态后,我们回到前面

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java]
    protected void handleRefreshState(Object arg) {
        handleUpdateState(mTmpState, arg);

        final boolean changed = mTmpState.copyTo(mState);
        if (changed) {
            handleStateChanged();
        }
        ...
    }

可以看到假如开关状态发生了改变则会导致 handleStateChanged() 被调用

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java]

    private void handleStateChanged() {
        if (mCallbacks.size() != 0) {
            for (int i = 0; i < mCallbacks.size(); i++) {
                mCallbacks.get(i).onStateChanged(mState);
            }
        }
    }

这里就和前面 2.2 小节关联起来了,即 state会被传递到视图层

[packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java]

    public void onStateChanged(QSTile.State state) {
        mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget();
    }

    private class H extends Handler {
        private static final int STATE_CHANGED = 1;

        public H() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == STATE_CHANGED) {
                handleStateChanged((QSTile.State) msg.obj);
            }
        }
    }

handleStateChanged函数不展开讲,该函数是真正做开关视图显示刷新的地方,细节看源码。

至此,我们就将开关点击背后的流程梳理清楚了,相信阅读完这三条主线的代码流程后,对整个QS面板的整体实现就很清晰了

设计思考

阅读前面 2.3小节的代码我们可以看到,不管是在Tile的逻辑层还是视图层,其内部均通过Handler来组织开关的状态刷新,因为整个QS面板有各种各样的开关,各个开关的刷新时机是不确定的,而通过消息机制则可以有条不紊得将所有开关的刷新有序组织起来,这里体现了Android中很重要的一个特性-有序性,这是我们值得借鉴和参考的.

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

推荐阅读更多精彩内容