SystemUI 剖析

来源:互联网 发布:揭阳网络广告公司 编辑:程序博客网 时间:2024/05/21 03:56

序言

17年上半年主要做车机项目SystemUI的工作,一般的来说可以选择在源码的基础上定制,也可以重新编写。具体选择哪种方案,也要根据自身项目的需求和工作量来考虑,但是不管选择哪种,都需要对SystemUI源码有一定的了解。本文主要先从大体上了解整个SystemUI的运行机制,然后再从状态栏的图标如何显示,下拉菜单是如何的实现,来剖析这个模块的运行原理,最后用一个需求来深入的了解SystemUI模块。希望能够帮助有这方面需求的同学,当然了,本文都是笔者自己对代码的理解,如果有不正确的地方,欢迎留言指出!

整体架构

相信很多没有了解过SystemUI的同学都觉得它很神秘,因为这个Application很特殊,它和系统的联系很紧密,而且需要在源码的环境下编译,所以这也是和应用开发者关系比较疏远的原因之一。下图是SystemUI的整体架构图,我看过Android 4.4和6.0的代码,虽然代码变化很大,但是原理和核心代码都是差不多的。

SystemUI架构图

SystemUI的启动是由SystemServer进程来操作的,在整个系统资源,服务加载完成差不多的时候,调用了startSystemUI() 来启动一个服务SystemUIService ,此时就进入了SystemUI的应用程序世界了。

    static final void startSystemUi(Context context) {        Intent intent = new Intent();        intent.setComponent(new ComponentName("com.android.systemui",                    "com.android.systemui.SystemUIService"));        //Slog.d(TAG, "Starting service: " + intent);        context.startServiceAsUser(intent, UserHandle.OWNER);    }

首先来说下一个aidl文件,叫做IStatusBar.aidl,它的路径在frameworks/base/core/java/com/android/internal/statusbar 下。它是framework和SystemUI的通信接口,包括我们的StatusBarManagerService封装的也是这个接口。我们的SystemUI主要是显示系统状态的改变,一般的由内部和外部导致,比如系统电量的改变,SystemUI会接收系统广播来修改,这种一般不会暴露给外部使用,不然就导致显示异常,还有种是外部导致SystemUI显示的变化,像其他应用发来通知,隐藏状态栏等,这个就需要我们的IStatusBar.aidl来承担之间的通信规则了。当我们的SystemSever进程来启动我们的SystemUIService,我们SystemUIService会启动很多的类,而这些类都是继承SystemUI接口的,并且都是有自己的职责所在。

   /**     * Makes sure that all the SystemUI services are running. If they are already running, this is a     * no-op. This is needed to conditinally start all the services, as we only need to have it in     * the main process.     *     * <p>This method must only be called from the main thread.</p>     */    public void startServicesIfNeeded() {        if (mServicesStarted) {            return;        }        if (!mBootCompleted) {            // check to see if maybe it was already completed long before we began            // see ActivityManagerService.finishBooting()            if ("1".equals(SystemProperties.get("sys.boot_completed"))) {                mBootCompleted = true;                if (DEBUG) Log.v(TAG, "BOOT_COMPLETED was already sent");            }        }        Log.v(TAG, "Starting SystemUI services.");        final int N = SERVICES.length;        for (int i=0; i<N; i++) {            Class<?> cl = SERVICES[i];            if (DEBUG) Log.d(TAG, "loading: " + cl);            try {                mServices[i] = (SystemUI)cl.newInstance();            } catch (IllegalAccessException ex) {                throw new RuntimeException(ex);            } catch (InstantiationException ex) {                throw new RuntimeException(ex);            }            mServices[i].mContext = this;            mServices[i].mComponents = mComponents;            if (DEBUG) Log.d(TAG, "running: " + mServices[i]);            mServices[i].start();            if (mBootCompleted) {                mServices[i].onBootCompleted();            }        }        mServicesStarted = true;    }

下面是启动的所有接口,

    /**     * The classes of the stuff to start.     */    private final Class<?>[] SERVICES = new Class[] {            com.android.systemui.tuner.TunerService.class,            com.android.systemui.keyguard.KeyguardViewMediator.class,            com.android.systemui.recents.Recents.class,            com.android.systemui.volume.VolumeUI.class,            com.android.systemui.statusbar.SystemBars.class,            com.android.systemui.usb.StorageNotification.class,            com.android.systemui.power.PowerUI.class,            com.android.systemui.media.RingtonePlayer.class,            com.android.systemui.keyboard.KeyboardUI.class,    };

我们这里只研究下主要试图的部分,也就是我们的SystemBars,这个类主要的工作就是通过类加载器生成我们的PhoneStatusBar,然后调用它的start()方法,我们切换到PhoneStatusBar这个类来,PhoneStatusBar这个类承担了最主要的添加,移除,更新视图的重任,它负责解析状态栏,导航栏的布局文件,然后通过WindowManagerService来生成Window对象,它实现了IStatusBar的所有接口,负责接收外界所有的关于SystemUI视图更新的消息。

视图

视图这块主要有三个模块分别是状态栏,导航栏,还有下拉模块。状态栏主要显示系统的状态,导航栏一般提供了快捷Home,最近打开的应用,和回到上一层,下拉菜单提供了一些快捷操作,通知的显示,这里就不讲导航栏了,感兴趣可以自行了解。来看看状态栏吧,小小的一个区域,Google却把它封装成一层又一层的,整个statusbar布局可以说是非常复杂,不花点精力真的不知道哪对哪。


这里写图片描述


statusbar的整体结构可以简化如上图所示,statusbar的图标显示在PhoneStatusBarView 那一层,其中又包裹着moreIcons和notificationIcons,下面是6.0的布局文件。

<com.android.systemui.statusbar.phone.PhoneStatusBarView    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"    android:id="@+id/status_bar"    android:background="@drawable/system_bar_background"    android:orientation="vertical"    android:focusable="false"    android:descendantFocusability="afterDescendants"    >    <ImageView        android:id="@+id/notification_lights_out"        android:layout_width="@dimen/status_bar_icon_size"        android:layout_height="match_parent"        android:paddingStart="6dip"        android:paddingBottom="2dip"        android:src="@drawable/ic_sysbar_lights_out_dot_small"        android:scaleType="center"        android:visibility="gone"        />    <LinearLayout android:id="@+id/status_bar_contents"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:paddingStart="6dp"        android:paddingEnd="8dp"        android:orientation="horizontal"        >        <com.android.systemui.statusbar.AlphaOptimizedFrameLayout            android:id="@+id/notification_icon_area"            android:layout_width="0dip"            android:layout_height="match_parent"            android:layout_weight="1"            android:orientation="horizontal"            >            <!-- The alpha of this area is both controlled from PhoneStatusBarTransitions and                 PhoneStatusBar (DISABLE_NOTIFICATION_ICONS), so we need two views here. -->            <com.android.keyguard.AlphaOptimizedLinearLayout                android:id="@+id/notification_icon_area_inner"                android:layout_width="match_parent"                android:layout_height="match_parent"                >                <com.android.systemui.statusbar.StatusBarIconView android:id="@+id/moreIcon"                    android:layout_width="@dimen/status_bar_icon_size"                    android:layout_height="match_parent"                    android:src="@drawable/stat_notify_more"                    android:visibility="gone"                    />                <com.android.systemui.statusbar.phone.IconMerger 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"/>            </com.android.keyguard.AlphaOptimizedLinearLayout>        </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>        <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/system_icon_area"            android:layout_width="wrap_content"            android:layout_height="match_parent"            android:orientation="horizontal"            >            <include layout="@layout/system_icons" />            <com.android.systemui.statusbar.policy.Clock                android:id="@+id/clock"                android:textAppearance="@style/TextAppearance.StatusBar.Clock"                android:layout_width="wrap_content"                android:layout_height="match_parent"                android:singleLine="true"                android:paddingStart="7dp"                android:gravity="center_vertical|start"                />        </com.android.keyguard.AlphaOptimizedLinearLayout>    </LinearLayout></com.android.systemui.statusbar.phone.PhoneStatusBarView>

我们一般动态显示的图标在标签下system_icons布局中,

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/system_icons"    android:layout_width="wrap_content"    android:layout_height="match_parent"    android:gravity="center_vertical">    <com.android.keyguard.AlphaOptimizedLinearLayout android:id="@+id/statusIcons"        android:layout_width="wrap_content"        android:layout_height="match_parent"        android:gravity="center_vertical"        android:orientation="horizontal"/>    <include layout="@layout/signal_cluster_view"        android:id="@+id/signal_cluster"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginStart="2.5dp"/>    <!-- battery must be padded below to match assets -->    <com.android.systemui.BatteryMeterView android:id="@+id/battery"        android:layout_height="14.5dp"        android:layout_width="9.5dp"        android:layout_marginBottom="@dimen/battery_margin_bottom"/></LinearLayout>

可以看到电池电量和信号是专门的控件去处理的,因为这块是一直显示在我们的状态栏上的,并且由系统去修改的,上面也提到了,这个是不提供外界修改的接口。其他的图标的更新是在id叫statusIcons的一个线性布局中,代码控制模块是通过CommandQueue将回调传递给PhoneStatusBar,然后PhoneStatusBar又交给StatusBarIconController中,以下是部分代码

    public void addSystemIcon(String slot, int index, int viewIndex, StatusBarIcon icon) {        boolean blocked = mIconBlacklist.contains(slot);        StatusBarIconView view = new StatusBarIconView(mContext, slot, null, blocked);        view.set(icon);        mStatusIcons.addView(view, viewIndex, new LinearLayout.LayoutParams(                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));        view = new StatusBarIconView(mContext, slot, null, blocked);        view.set(icon);        mStatusIconsKeyguard.addView(view, viewIndex, new LinearLayout.LayoutParams(                ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize));        applyIconTint();    }    public void updateSystemIcon(String slot, int index, int viewIndex,            StatusBarIcon old, StatusBarIcon icon) {        StatusBarIconView view = (StatusBarIconView) mStatusIcons.getChildAt(viewIndex);        view.set(icon);        view = (StatusBarIconView) mStatusIconsKeyguard.getChildAt(viewIndex);        view.set(icon);        applyIconTint();    }    public void removeSystemIcon(String slot, int index, int viewIndex) {        mStatusIcons.removeViewAt(viewIndex);        mStatusIconsKeyguard.removeViewAt(viewIndex);    }

mStatusIcons对应的就是前面介绍的id为statusIcons的布局。我们再来分析下下拉菜单的原理机制,当我们触摸手机的状态栏的位置,下拉菜单会随着我们手势做相应的处理。还记得我们状态栏最外层布局节点是PhoneStatusBarView,

    @Override    public boolean onTouchEvent(MotionEvent event) {        boolean barConsumedEvent = mBar.interceptTouchEvent(event);        if (DEBUG_GESTURES) {            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {                EventLog.writeEvent(EventLogTags.SYSUI_PANELBAR_TOUCH,                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),                        barConsumedEvent ? 1 : 0);            }        }        return barConsumedEvent || super.onTouchEvent(event);    }

首先先看看mBar是否要拦截这个事件,如果没有拦截则调用父类的onTouchEvent()方法,这里的mBar其实就是PhoneStatusBar这个类,这个类相当于一个控制器的作用,再看看PhoneStatusBar什么时候会拦截这个事件,

    public boolean interceptTouchEvent(MotionEvent event) {        if (DEBUG_GESTURES) {            if (event.getActionMasked() != MotionEvent.ACTION_MOVE) {                EventLog.writeEvent(EventLogTags.SYSUI_STATUSBAR_TOUCH,                        event.getActionMasked(), (int) event.getX(), (int) event.getY(),                        mDisabled1, mDisabled2);            }        }        if (SPEW) {            Log.d(TAG, "Touch: rawY=" + event.getRawY() + " event=" + event + " mDisabled1="                + mDisabled1 + " mDisabled2=" + mDisabled2 + " mTracking=" + mTracking);        } else if (CHATTY) {            if (event.getAction() != MotionEvent.ACTION_MOVE) {                Log.d(TAG, String.format(                            "panel: %s at (%f, %f) mDisabled1=0x%08x mDisabled2=0x%08x",                            MotionEvent.actionToString(event.getAction()),                            event.getRawX(), event.getRawY(), mDisabled1, mDisabled2));            }        }        if (DEBUG_GESTURES) {            mGestureRec.add(event);        }        if (mStatusBarWindowState == WINDOW_STATE_SHOWING) {            final boolean upOrCancel =                    event.getAction() == MotionEvent.ACTION_UP ||                    event.getAction() == MotionEvent.ACTION_CANCEL;            if (upOrCancel && !mExpandedVisible) {                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, false);            } else {                setInteracting(StatusBarManager.WINDOW_STATUS_BAR, true);            }        }        return false;    }

从这里可以看出,最后return的是false,而其中执行的一个比较重要的方法是setInteracting(),它的作用隐藏和显示状态栏拓展出来的部分就是下拉菜单这块,不是很难理解,我们在着重分析下panelBar的onTouchEvent()方法,

    @Override    public boolean onTouchEvent(MotionEvent event) {        // Allow subclasses to implement enable/disable semantics        if (!panelsEnabled()) {            if (event.getAction() == MotionEvent.ACTION_DOWN) {                Log.v(TAG, String.format("onTouch: all panels disabled, ignoring touch at (%d,%d)",                        (int) event.getX(), (int) event.getY()));            }            return false;        }        // figure out which panel needs to be talked to here        if (event.getAction() == MotionEvent.ACTION_DOWN) {            final PanelView panel = selectPanelForTouch(event);            if (panel == null) {                // panel is not there, so we'll eat the gesture                Log.v(TAG, String.format("onTouch: no panel for touch at (%d,%d)",                        (int) event.getX(), (int) event.getY()));                mTouchingPanel = null;                return true;            }            boolean enabled = panel.isEnabled();            if (DEBUG) LOG("PanelBar.onTouch: state=%d ACTION_DOWN: panel %s %s", mState, panel,                    (enabled ? "" : " (disabled)"));            if (!enabled) {                // panel is disabled, so we'll eat the gesture                Log.v(TAG, String.format(                        "onTouch: panel (%s) is disabled, ignoring touch at (%d,%d)",                        panel, (int) event.getX(), (int) event.getY()));                mTouchingPanel = null;                return true;            }            startOpeningPanel(panel);        }        final boolean result = mTouchingPanel != null                ? mTouchingPanel.onTouchEvent(event)                : true;        return result;    }

一般的下拉菜单有两种,一种是SettingPanelView还有种 NotificationPanelView, panelBar的onTouchEvent主要寻找哪块PanelView消费了这个MotionEvent,所以,我们继续跟踪PanelView的onTouchEvent,

  @Override    public boolean onTouchEvent(MotionEvent event) {        if (mInstantExpanding || mTouchDisabled                || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {            return false;        }        /*         * We capture touch events here and update the expand height here in case according to         * the users fingers. This also handles multi-touch.         *         * If the user just clicks shortly, we give him a quick peek of the shade.         *         * Flinging is also enabled in order to open or close the shade.         */        int pointerIndex = event.findPointerIndex(mTrackingPointer);        if (pointerIndex < 0) {            pointerIndex = 0;            mTrackingPointer = event.getPointerId(pointerIndex);        }        final float x = event.getX(pointerIndex);        final float y = event.getY(pointerIndex);        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {            mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures();            mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y);        }        switch (event.getActionMasked()) {            case MotionEvent.ACTION_DOWN:                startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);                mJustPeeked = false;                mPanelClosedOnDown = isFullyCollapsed();                mHasLayoutedSinceDown = false;                mUpdateFlingOnLayout = false;                mMotionAborted = false;                mPeekTouching = mPanelClosedOnDown;                mTouchAboveFalsingThreshold = false;                mCollapsedAndHeadsUpOnDown = isFullyCollapsed()                        && mHeadsUpManager.hasPinnedHeadsUp();                if (mVelocityTracker == null) {                    initVelocityTracker();                }                trackMovement(event);                if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) ||                        mPeekPending || mPeekAnimator != null) {                    cancelHeightAnimator();                    cancelPeek();                    mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning)                            || mPeekPending || mPeekAnimator != null;                    onTrackingStarted();                }                if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) {                    schedulePeek();                }                break;            case MotionEvent.ACTION_POINTER_UP:                final int upPointer = event.getPointerId(event.getActionIndex());                if (mTrackingPointer == upPointer) {                    // gesture is ongoing, find a new pointer to track                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;                    final float newY = event.getY(newIndex);                    final float newX = event.getX(newIndex);                    mTrackingPointer = event.getPointerId(newIndex);                    startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight);                }                break;            case MotionEvent.ACTION_POINTER_DOWN:                if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {                    mMotionAborted = true;                    endMotionEvent(event, x, y, true /* forceCancel */);                    return false;                }                break;            case MotionEvent.ACTION_MOVE:                float h = y - mInitialTouchY;                // If the panel was collapsed when touching, we only need to check for the                // y-component of the gesture, as we have no conflicting horizontal gesture.                if (Math.abs(h) > mTouchSlop                        && (Math.abs(h) > Math.abs(x - mInitialTouchX)                                || mIgnoreXTouchSlop)) {                    mTouchSlopExceeded = true;                    if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) {                        if (!mJustPeeked && mInitialOffsetOnTouch != 0f) {                            startExpandMotion(x, y, false /* startTracking */, mExpandedHeight);                            h = 0;                        }                        cancelHeightAnimator();                        removeCallbacks(mPeekRunnable);                        mPeekPending = false;                        onTrackingStarted();                    }                }                final float newHeight = Math.max(0, h + mInitialOffsetOnTouch);                if (newHeight > mPeekHeight) {                    if (mPeekAnimator != null) {                        mPeekAnimator.cancel();                    }                    mJustPeeked = false;                }                if (-h >= getFalsingThreshold()) {                    mTouchAboveFalsingThreshold = true;                    mUpwardsWhenTresholdReached = isDirectionUpwards(x, y);                }                if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) {                    setExpandedHeightInternal(newHeight);                }                trackMovement(event);                break;            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                trackMovement(event);                endMotionEvent(event, x, y, false /* forceCancel */);                break;        }        return !mGestureWaitForTouchSlop || mTracking;    }

PanelView的onTouchEvent()方法主要对手势的处理逻辑比较多,但是也是很常规的对自定义View的处理,和下拉刷新控件逻辑和用到的技术都差不多的东西,手一直下拉过程中,不断的刷新,重绘控件,如果在中途放手,那么会根据你放手的位置来决定到底是关闭or打开panelView。

动态更改状态栏高度

像一般的定制SystemUI,静态的修改状态栏高度是经常都会碰到的,我们会去修改frameworks/base/core/res/res/values/dimens.xml中的值,

    <dimen name="toast_y_offset">64dip</dimen>    <!-- Height of the status bar -->    <dimen name="status_bar_height">24dp</dimen>    <!-- Height of the bottom navigation / system bar. -->    <dimen name="navigation_bar_height">48dp</dimen>

然后将framework-res.jar push到/system/framework/底下重启机器就可以生效了,但是如何动态的修改状态的高度呢,我们知道在Android源码中,Window这个类非常的抽象,它会在Activity启动的时候附着在Activity中,然后承担着试图显示的职责,而PhoneWindow是Window的唯一实现类,PhoneWindowManager可以说是一个策略类,它实现了WindowManagerPolicy,从名字也可以看出,主要是处理一些和视图相关的逻辑部分,当然了整个Android的任何一个模块都是相当的复杂,我们这里并不去深究,我们知道PhoneWindowManager这个类做了很多初始化工作,包括从framework/res中获取状态栏的高度值

        mStatusBarHeight =                res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);

然后PhoneWindowManager还要确定绘制的各个区域,然后进行测量,绘制的工作,所以我们只要动态的设置PhoneWindowManager中的mStatusBarHeight,然后在重新绘制系统的视图即可。具体的,我们该如何动态修改mStatusBarHeight的值,我们的好好的分析PhoneWindowManager这个类了,可以看到

 ... IStatusBarService mStatusBarService; ...

在PhoneWindowManager这个类中有mStatusBarService这个属性,这就意味着我们可以在这里直接操作SystemUI,StatusBarManagerService同样也实现了这个接口,而StatusBarManagerService内部有IStatusBar这个代理类,同样,SystemUI中CommandQueue也实现了IStatusBar(上文提到,这里SystemUI相当于服务端),这样在PhoneWindowManager就可以直接控制SystemUI的行为,那么我们怎么样能够修改PhoneWindowManager中的状态栏高度值呢,我们看到PhoneWindowManager实现了WindowPolicy接口,而在WindowManagerService是持有WindowPolicy这个引用的。

    ...    final WindowManagerPolicy mPolicy = new PhoneWindowManager();    ...

这样,我们只需要能够调用WindowManagerService里面的方法,就能直接控制PhoneWindowManager,从而控制SystemUI了,再看看我们的WindowManagerService是继承了IWindowManager.Stub这个通信借口,所以,所有问题都解决了,我们只需要在IWindowManager.aidl文件中,添加updateStatusBarHeight()方法,然后在之前我们提到的所有地方都添加方法实现,最终就会调用到PhoneWindowManager,修改状态栏的高度值,当SystemUI接受到回调之后,调用WindowManager的updateLayoutView()方法,就会重新绘制我们的系统UI。

这里写图片描述

上图是整个调用流程,这样就可以动态的修改我们的状态栏高度了。

原创粉丝点击