SystemUI之通知圖標控制

SystemUI之狀態圖標控制 分析了狀態欄上狀態圖標(例如 wifi, bt)的控制流程,比較簡單。本文來分析下狀態欄上通知圖標的控制流程,主要分析當一個新通知來臨時,新通知的圖標是如何一步步顯示到狀態上的。java

通知圖標控制器

SystemUI之狀態圖標控制可知,狀態圖標是由一個叫StatusBarIconController接口控制顯示的,而通知圖標區域也有一個控制器,叫NotificationIconAreaController(它不是一個接口)。android

NotificationIconAreaController的構造函數中會調用以下方法來建立通知圖標的容器app

// 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變量。ide

初始化通知圖標區域

既然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)。post

這個"服務"就至關於客戶端SystemUI在服務端ActivityManagerService註冊的一個回調。當有通知來臨的時候,就會經過這個"服務"通知SystemUI。ui

這個註冊是在StatusBar#start()中完成的。spa

// 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進行定製。

相關文章
相關標籤/搜索