SystemUI 剖析
来源:互联网 发布:揭阳网络广告公司 编辑:程序博客网 时间:2024/05/21 03:56
序言
17年上半年主要做车机项目SystemUI的工作,一般的来说可以选择在源码的基础上定制,也可以重新编写。具体选择哪种方案,也要根据自身项目的需求和工作量来考虑,但是不管选择哪种,都需要对SystemUI源码有一定的了解。本文主要先从大体上了解整个SystemUI的运行机制,然后再从状态栏的图标如何显示,下拉菜单是如何的实现,来剖析这个模块的运行原理,最后用一个需求来深入的了解SystemUI模块。希望能够帮助有这方面需求的同学,当然了,本文都是笔者自己对代码的理解,如果有不正确的地方,欢迎留言指出!
整体架构
相信很多没有了解过SystemUI的同学都觉得它很神秘,因为这个Application很特殊,它和系统的联系很紧密,而且需要在源码的环境下编译,所以这也是和应用开发者关系比较疏远的原因之一。下图是SystemUI的整体架构图,我看过Android 4.4和6.0的代码,虽然代码变化很大,但是原理和核心代码都是差不多的。
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。
上图是整个调用流程,这样就可以动态的修改我们的状态栏高度了。
- SystemUI 剖析
- SystemUI
- SystemUI
- android systemui
- SystemUI 概述
- SystemUI 概述
- 打包systemUI
- Android: SystemUI
- SystemUI->截屏
- SystemUI浅析
- systemui浅析
- systemui 分析
- SystemUI 布局
- systemUI学习
- SystemUI VolumeService
- SystemUI RingtonePlayer
- android SystemUI浅析之SystemUI启动流程
- SystemUI之USB2(Framework UEvent -> SystemUI)
- 简单利用RecyclerView实现ListView的点击事件
- Could not load TestContextBootstrapper [null]. Specify @BootstrapWith's 'val
- ie的png透明图片兼容性问题
- js鼠标滚动 mousewheel
- JS各循环的差别
- SystemUI 剖析
- 华为1
- spring 注解
- mvp框架的简单使用
- 批量处理linux java -jar jar服务模块脚本
- matlab程序控制结构
- ssm
- 关于Statement
- 安装freeradius的GUI管理程序dialup_admin