Android8.0多窗口调研
一、概述
Android8.0上面原生的多窗口功能支持四种模式:全屏、分屏、画中画、FreeForm模式。多窗口主要涉及ActivityManagerService、WindowManagerService、Input三个模块。以下分析基于Android8.0代码。
二、原理框架
Android原生多窗口是多Stack方案,即存在多个ActivityStack。ActivityStack是一个抽象的栈,每个栈都有自己的屏幕区域bound和id,Activity是以Task方式组织并放在某一个Stack中的。比如,Launcher、Recents属于id=HOME_STACK的栈中。8.0主要涉及以下:
HOME_STACK_ID、FULLSCREEN_WORKSPACE_STACK_ID、FREEFORM_WORKSPACE_STACK_ID、DOCKED_STACK_ID、PINNED_STACK_ID、RECENTS_STACK_ID、ASSISTANT_STACK_ID
AMS和WMS中对Stack分别用ActivityStack和TaskTack描述,通过StackId来映射。对Task分别用TaskRecord、Task描述,通过TaskId来映射。
每个Activity显示在所属ActivityStack的bound区域内,多个Activity显示在各自ActivityStack的bound区域内,这样就可以实现多窗口。但是FreeForm模式下,Activity的bound由所属Task决定,而非Stack。多窗口不仅仅是控制Activity放入不同ActivityStack中,同时还要改变Activity的生命周期,即FocusActivity是resume状态,其他可见Activity是Pause状态,并不会进入Stop状态。
整个系统中只会有一个FocusStack,一个FocusActivity。用户在哪个Activity中操作,FocusActivity便指向该Activity,FocusStack便指向FocusActivity所属的Stack。注意,画中画模式下,浮层Activity无法成为FocusActivity,故浮层属于的Stack并非FocusStack。
进入/退出多窗口按以下逻辑框架顺序进行处理:
step1.调整ActivityStack栈
step2.调整Task栈
step3.调整Activity堆栈,调整WMS中APPWindowToken堆栈
step4.调用Activity生命周期函数
step5.添加Window到WMS
step6.过度动画及应用窗口显示
三、关键函数
移栈:
AMS.moveTaskToStack()
AMS.moveTaskToDockedStack()
AMS.moveTopActivityToPinnedStack()
分屏相互切换:
AMS.swapDockedAndFullscreenStack()
真正切换task(重点):
TaskRecord.reparent()//8.0新增分屏task移动都在此函数执行
调整栈大小:
AMS.resizeDockedStack()
AMS.resizePinnedStack()
ActivityStackSupervisor.resizeStackUncheckedLocked()
Activity生命周期:
ActivityStack.resumeTopActivityInnerLocked():
Activity启动:
ActivityStarter.startActivityUnchecked()
四、多窗口变更通知和查询
Activity.isInMultiWindowMode()调用该方法以确认 Activity是否处于多窗口模式。 Activity.isInPictureInPictureMode()调用该方法以确认 Activity是否处于画中画模式。 Activity.onMultiWindowModeChanged() Activity 进入或退出多窗口模式时系统将调用此方法。在Activity 进入多窗口模式时,系统向该方法传递true 值,在退出多窗口模式时,则传递false 值。 Activity.onPictureInPictureModeChanged() Activity 进入或退出画中画模式时系统将调用此方法。在Activity 进入画中画模式时,系统向该方法传递true 值,在退出画中画模式时,则传递false 值。每个方法还有 Fragment版本,例如 Fragment.isInMultiWindowMode()。五、源码分析
1、分屏模式
进入分屏有两种入口:
AMS.startActivityFromRecents()//在任务栏中拖动一个分屏应用到顶部
AMS.moveTaskToDockedStack()//长按底部任务按键
简单分析下AMS.startActivityFromRecents():
进入分屏之前PhoneActivity属于id=1(FullScreenStack),任务栏Recents属于id=0(HomeStack),FocusStack指向HomeStack。进入分屏相当于启动PhoneActivity,并将其放到上半区域Stack中,上半区域为DockedStack(id=3),即将PhoneActivity从id=1(FullScreenStack)移到id=3的DockedStack中,最后将HomeStack的boundresize,处在下半屏。注意分屏后FocusStack指向上半区域DockedStack。整个时序如下所示。
AMS.moveTaskToDockedStack():
在支持分屏应用界面长按底部任务按键,进入分屏模式。相当于把PhoneActivity从id=1移到id=3的DockedStack中,同时resizeHomeStack。类似前者,不做详细分析。
2、画中画模式
画中画即置顶Activity,Activity的窗口永远位于所有窗口之上,所属的Stack自然位于所有Stack的上面,这类特殊的Stack称为PinnedStack(id=4)。PinnedStack无法成为FocusStack,Activity也无法成为FocusActivity。
如何启动画中画,只需在AndroidManifest中声明Activity时添加android:supportsPictureInPicture="true"和android:resizeableActivity="true”属性,并调用新方法 Activity.enterPictureInPictureMode()。时序图如下:
ps:reparent没做详细展出
PinnedStack为非Focusablestack,Activity为非focusactivity,resumeFocusedStackTopActivityLocked()只会将stack堆栈中FocusStack的focusactivity进行resume,其他任何activity均为paused或stoped状态,所以画中画Activity是处于paused状态的.
3、FreeForm模式
FreeForm模式下并不存在与之对应的多个Stack,而是只有一个FreeFormStack(stackId=2)存放了多个Task。这种模式下决定Activity显示位置的不是Stackbound,而是Taskbound,即每个Task都有自己的bound。
FreeForm模式下Activity从Recents中启动,调用的接口为AMS.startActivityFromRecents(),其时序跟从任务栏拖动一个应用进入分屏差不多,只是此处Activity在FreeFormStack中启动,其他基本流程基本一致,不再详细分析。
4、窗口resize/拖动
在分屏模式下,拖动中间bar条改变分屏大小,中间bar条是SystemUI添加到系统一个名为“DockedStackDivider”的浮窗,接收触摸事件,根据MotionEvent的Y值动态调用ActivityManagerService.resizeDockedStack()来改变DockedStack的bound大小,完成窗口大小调整。
在FreeForm模式下的窗口缩放和拖拽跟分屏模式不一样,没有SystemUI添加的bar浮窗,但是在FreeForm模式下DecorView与子ContentView间插入了一个DecorCaptionView,代码如下所示。
//FreeForm模式下config发生改变,会触发onConfigurationChanged,调用到此处
窗口调整跟拖拽便是在DecorCaptionView中根据触摸事件检测开启的,调用的开启函数便是WindowManagerService.startMovingTask(IWindowwindow, float startX, float startY)。就拿拖拽来说,用户按住FreeFormActivity的窗口边缘然后移动,此拖动过程分为三步:
step1.DecorCaptionView检测触摸事件,发现是拖拽或窗口调整,那么跨进程调用WindowManagerService.startMovingTask()开启Task移动;
step2.WMS.startMovingTask()会创建一个TaskPositioner,TaskPositioner会注册一个InputChannel到InputManager中,此后的触摸事件便会发送到TaskPositioner中来;
step3.TaskPositioner接收move事件,计算WindowRect然后调用AMS.resizeTask(inttaskId, Rect bounds, int resizeMode)来更新Activity窗口bound。
六、区别OS
os的多窗口使用的也是类似多stack方案,通过调整stack大小,同时显示多个应用。
和原生的区别在于,os在切换窗口大小的时候需要加载不同的布局,系统强制发送onConfigurationChanged给应用,应用根据config加载不同布局。原生在config变化会销毁activity,然后根据新config重新启动新页面。os修改系统逻辑不重启应用。
七、总结
Android8.0针对多窗口进一步完善,并且将taskRecord相关的切换放到reparent新方法中统一处理。os的方案在8.0上面需要针对新特新重新修改。