SystemUI之通知图标控制

本文是基于Android 10源码分析的。

SystemUI之状态图标控制 分析了状态栏上状态图标(例如 wifi, bt)的控制流程,比较简单。本文来分析下状态栏上通知图标的控制流程,主要分析当一个新通知来临时,新通知的图标是如何一步步显示到状态上的。

通知图标控制器

SystemUI之状态图标控制可知,状态图标是由一个叫StatusBarIconController接口控制显示的,而通知图标区域也有一个控制器,叫NotificationIconAreaController(它不是一个接口)。

NotificationIconAreaController的构造函数中会调用如下方法来创建通知图标的容器

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java

    protected void initializeNotificationAreaViews(Context context) {
        // ...

        LayoutInflater layoutInflater = LayoutInflater.from(context);
        // 实例化通知图标区域视图
        mNotificationIconArea = inflater.inflate(R.layout.notification_icon_area, null);
        // 这个才是真正存放通知图标的父容器
        mNotificationIcons = mNotificationIconArea.findViewById(R.id.notificationIcons);

        // ...
    }

这里加载了notification_icon_are.xml布局,来看下这个布局

<com.android.keyguard.AlphaOptimizedLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/notification_icon_area_inner"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false">
    <com.android.systemui.statusbar.phone.NotificationIconContainer
        android:id="@+id/notificationIcons"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentStart="true"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:clipChildren="false"/>
</com.android.keyguard.AlphaOptimizedLinearLayout>

ID为notificationIconsNotificationIconContainer就是通知图标容器,对应于上面代码的mNotificationIcons变量。

初始化通知图标区域

既然NotificationIconAreaController自己创建了通知图标容器,那么就需要加入到状态栏视图中,这个动作在创建状态栏视图后完成的

    // packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java

    protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) {
        // ...

        FragmentHostManager.get(mStatusBarWindow)
                .addTagListener(CollapsedStatusBarFragment.TAG, (tag, fragment) -> {
                    CollapsedStatusBarFragment statusBarFragment =
                            (CollapsedStatusBarFragment) fragment;
                    // 把控制器中的通知容器加入到状态栏的容器中
                    statusBarFragment.initNotificationIconArea(mNotificationIconAreaController);

                    // ...
                }).getFragmentManager()
                .beginTransaction()
                // CollapsedStatusBarFragment实现了状态栏的添加
                .replace(R.id.status_bar_container, new CollapsedStatusBarFragment(),
                        CollapsedStatusBarFragment.TAG)
                .commit();                
    }

调用的就是CollapsedStatusBarFragment#initNotificationIconArea()

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java

    public void initNotificationIconArea(NotificationIconAreaController
            notificationIconAreaController) {
        // 通知图标区域
        ViewGroup notificationIconArea = mStatusBar.findViewById(R.id.notification_icon_area);
        // 这个才是通知图标的父容器
        mNotificationIconAreaInner =
                notificationIconAreaController.getNotificationInnerAreaView();
        if (mNotificationIconAreaInner.getParent() != null) {
            ((ViewGroup) mNotificationIconAreaInner.getParent())
                    .removeView(mNotificationIconAreaInner);
        }
        // 把通知图标父容器添加到通知图标区域里
        notificationIconArea.addView(mNotificationIconAreaInner);

        // 省略处理中心图标区域的代码

        // 这里其实显示了通知图标区域和中心图标区域
        showNotificationIconArea(false);
    }

监听通知的服务端

当一条新通知发送后,它会存储到通知服务端,也就是NotificationManagerService,那么SystemUI是如何知道新通知来临的。这就需要SystemUI向ActivityManagerService注册一个"服务"(一个Binder)。

这个"服务"就相当于客户端SystemUI在服务端ActivityManagerService注册的一个回调。当有通知来临的时候,就会通过这个"服务"通知SystemUI。

这个注册是在StatusBar#start()中完成的。

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java

    public void start() {
        // 向通知服务端注册一个"服务",用于接收通知信息的回调
        mNotificationListener =  Dependency.get(NotificationListener.class);
        mNotificationListener.registerAsSystemService();
    }

来看下这个注册的实现

    // frameworks/base/core/java/android/service/notification/NotificationListenerService.java

    public void registerAsSystemService(Context context, ComponentName componentName,
            int currentUser) throws RemoteException {
        if (mWrapper == null) {
            // 这就是要注册的Binder,也就一个回调
            mWrapper = new NotificationListenerWrapper();
        }
        INotificationManager noMan = getNotificationInterface();
        // 向通知的服务端注册回调
        noMan.registerListener(mWrapper, componentName, currentUser);
    }

这个"服务"就是NotificationListenerWrapper

注册成功后,就会回调NotificationListenerWrapper#NotificationListenerWrapper()方法,并会附带所有的通知信息。

显示通知图标

当一条新的通知来临的时候,通知的服务端会通过NotificationListenerWrapper#onNotificationPosted()进行回调,而最终会调用NotificationListener#onNotificationPosted()

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java

    public void onNotificationPosted(final StatusBarNotification sbn,
            final RankingMap rankingMap) {
        if (sbn != null && !onPluginNotificationPosted(sbn, rankingMap)) {
            // 在主线程中进行更新
            Dependency.get(Dependency.MAIN_HANDLER).post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                if (isUpdate) {
                    // 更新通知操作
                    mEntryManager.updateNotification(sbn, rankingMap);
                } else {
                    // 添加新通知操作
                    mEntryManager.addNotification(sbn, rankingMap);
                }
            });
        }
    }

这里讨论添加新通知的操作,它调用的是NotificationManager#addNotification()方法,而内部是通过addNotificationInternal()实现的

    private void addNotificationInternal(StatusBarNotification notification,
            NotificationListenerService.RankingMap rankingMap) throws InflationException {
        // ...

        // NotificationEntry就代表一个通知实例
        NotificationEntry entry = new NotificationEntry(notification, ranking);

        // 异步加载视图,并绑定通知信息,由NotificationRowBinderImpl实现
        requireBinder().inflateViews(entry, () -> performRemoveNotification(notification,
                REASON_CANCEL));

        // ...
    }

首先为通知创建一个NotificationEntry实例,然后再通过NotificationRowBinderImpl#inflateViews()加载通知视图,绑定通知信息,并在通知栏添加通知视图,以及在状态栏添加通知图标。

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRowBinderImpl.java

    public void inflateViews(
            NotificationEntry entry,
            Runnable onDismissRunnable)
            throws InflationException {
        // ...

        if (entry.rowExists()) {

        } else {
            // 给通知创建图标
            entry.createIcons(mContext, sbn);
            // 异步加载通知视图
            new RowInflaterTask().inflate(mContext, parent, entry,
                    // 加载完成的回调,这里的加载指的仅仅是一个空视图
                    row -> {
                        // 绑定监听事件和回调
                        bindRow(entry, pmUser, sbn, row, onDismissRunnable);
                        // 在视图上更新通知信息
                        updateNotification(entry, pmUser, sbn, row);
                    });
        }
    }

RowInflaterTask#inflate()会使用status_bar_notification_row.xml布局创建一个通知视图,但是并没有把它加入到父容器中,更没有把把通知信息更新到视图中,这些工作都是在回调中完成的。

第一个回调bindRow(),会为视图绑定各种监听事件以及回调

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationRowBinderImpl.java

    private void bindRow(NotificationEntry entry, PackageManager pmUser,
            StatusBarNotification sbn, ExpandableNotificationRow row,
            Runnable onDismissRunnable) {
        // ...

        // 刚才只是创建了视图,并没有绑定数据,这里就是设置绑定数据后的回调,这个回调是由NotificationEntryManager实现
        row.setInflationCallback(mInflationCallback);

        // ...
    }

这里只列出了与本文分析相关的回调,这个回调是在视图与通知信息绑定后的回调。

第二个回调updateNotification(),用数据更新视图,更新完成后就会进行回调刚才绑定的回调事件,而这个回调是由NotificationEntryManager#onAsyncInflationFinished()实现的

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java

    public void onAsyncInflationFinished(NotificationEntry entry,
            @InflationFlag int inflatedFlags) {
        mPendingNotifications.remove(entry.key);
        if (!entry.isRowRemoved()) {
            boolean isNew = mNotificationData.get(entry.key) == null;
            if (isNew) {
                // ...

                if (mPresenter != null) {
                    // 显示视图
                    // 这个由StatusBarNotificationPresenter实现
                    mPresenter.updateNotificationViews();
                }

                // ...
            } else {

            }
        }
    }

数据已经准备完毕,那么现在就是要显示视图了,这个视图包括通知栏里的通知,以及状态栏时的通知图标。这个由StatusBarNotificationPresenter#updateNotificationViews()实现

    // frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java

    public void updateNotificationViews() {
        // ...

        // 1\. 把通知视图添加到通知面版的通知栏中
        mViewHierarchyManager.updateNotificationViews();

        // 这里不仅仅更新了通知面版的通知视图,也更新了状态栏的通知图标
        mNotificationPanel.updateNotificationViews();

        // ...
    }

    public void updateNotificationViews() {
        // ...省略更新通知栏的相关视图的代码

        updateShowEmptyShadeView();
        // 2\. 调用mIconAreaController更新了状态栏通知图标
        // 其实就是调用 mIconAreaController.updateNotificationIcons();
        mNotificationStackScroller.updateIconAreaViews();
    }

首先是往通知栏里添加通知视图,然后再更新状态栏视图。现在只看下如何向状态栏添加通知图标的,它最终是由NotificationIconAreaController#updateIconsForLayout()实现的

// frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java

    private void updateIconsForLayout(Function<NotificationEntry, StatusBarIconView> function,
            NotificationIconContainer hostLayout, boolean showAmbient, boolean showLowPriority,
            boolean hideDismissed, boolean hideRepliedMessages, boolean hideCurrentMedia,
            boolean hideCenteredIcon) {

        // toShow保存即将显示的图标
        ArrayList<StatusBarIconView> toShow = new ArrayList<>(
                mNotificationScrollLayout.getChildCount());

        // 过滤通知,并保存需要显示的通知图标
        for (int i = 0; i < mNotificationScrollLayout.getChildCount(); i++) {
            // 获取一个通知视图
            View view = mNotificationScrollLayout.getChildAt(i);
            if (view instanceof ExpandableNotificationRow) {
                NotificationEntry ent = ((ExpandableNotificationRow) view).getEntry();
                if (shouldShowNotificationIcon(ent, showAmbient, showLowPriority, hideDismissed,
                        hideRepliedMessages, hideCurrentMedia, hideCenteredIcon)) {
                    // 获取图标
                    StatusBarIconView iconView = function.apply(ent);
                    if (iconView != null) {
                        toShow.add(iconView);
                    }
                }
            }
        }

        // ...

        // 把需要显示的图标添加到hostLayout中
        final FrameLayout.LayoutParams params = generateIconLayoutParams();
        for (int i = 0; i < toShow.size(); i++) {
            StatusBarIconView v = toShow.get(i);
            // The view might still be transiently added if it was just removed and added again
            hostLayout.removeTransientView(v);
            if (v.getParent() == null) {
                if (hideDismissed) {
                    v.setOnDismissListener(mUpdateStatusBarIcons);
                }
                hostLayout.addView(v, i, params);
            }
        }

        // ...

    }

纵观整个过程,它的原理是根据通知栏的通知视图,来获取通知图标,然后经过一系列的过滤过程,最终把图标添加到状态栏通知图标容器中。

结束

本文简要分析了通知图标的显示流程,其中穿插提到了通知栏的通知视图的添加过程。掌握了大纲,就可以对细节进行考究,甚至对SystemUI进行定制。

不管好与不好,动动你的小手为小编点个赞吧,你的点赞将是小编最大的动力。

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

推荐阅读更多精彩内容