在Android N(7.0)版本开始,系统支持了多窗口功能。在有了多窗口支持之后,用户可以同时打开和看到多个应用的界面。并且系统还支持在多个应用之间进行拖拽。在大屏幕设备上,这一功能非常实用。
在Android N中多窗口有三种表现形式:
该模式主要将屏幕一分为二,同时出现两个应用界面。
2.画中画模式
该模式主要在视频播放中使用,可以使视频播放窗口一直处于最顶层。
3.FreeForm模式
该模式类似于我们的桌面操作系统,应用界面窗口可以自由拖动和修改大小。
而本文主要讲解多窗口分屏模式的实现方式。
分屏模式是通过长按最近任务列表(RecetsActivity)的任一个历史应用(TaskTiew)进入的,如果该应用不支持分屏就提示用户,如果可以分屏就显示可以分屏的区域。之后拖拽想要分屏的TaskView,在拖拽的过程中不断的不断进行判断touch事件移动的位置是否进入了分屏区域,如果没有继续处理touch事件,如果进入了分屏区域,就会更新屏幕区域分屏,此时结束拖拽,调用AMS为分屏区域创建stack,根据屏幕尺寸计算stack的尺寸,然后对stack中task再重新计算尺寸,最后启动分屏应用。
最近任务列表是由一个个的TaskView组成,当我们选择要分屏的TaskVIew,长按就进入了TaskVIew.java的onLongClick函数中,请求对其分屏。
TaskView发生长按事件后,获取TaskView的边界值,进行判断mDownTouchPos的x,y值是否在TaskView的范围之内,该值就是触摸事件上报的值,也就是手指按下的位置,在onInterceptTouchEvent函数中获取。
只有点击的View与当前的对象一致,并且手指按下的位置在TaskView的范围之内,并且此时并不在分屏模式下才会监听拖拽事件,注册EventBus事件,并且发送DragStartEvent事件,将当前的Task,TaskView,以及手指按下的点封装进DragStartEvent对象中。
在拖拽TaskView对应用进行分屏过程中EventBus起到了非常重要的作用,下面先对EventBus进行分析。
EventBus是一款Android下发布/订阅事件总线机制,以观察者模式实现。主要功能是替代Intent,Handler,Broadcast在Fragment,Activity,Service,线程之间传递消息,降低了发送者与接收者的耦合度。下面分析简单讲解如何使用EventBus,以后详解分析实现原理。
通过EventBus.getDefault().register(this)来对事件进行注册,注册时首先通过getDefault函数获取EventBus对象。
可以看出EventBus是通过单例模式获得的,并且使用了主线程的Looper对象。
获得EventBus对象后就可以对订阅者进行注册,来进行接收事件,接收事件是有优先级的,通过参数priority控制,priority越大优先级越高。
EventBus.getDefault().send(Event);
EventBus.getDefault().post(Event);
通过send或者post函数进行发布对应的事件。
通过重写onBusEvent来订阅事件。
言归正传,重新回到TaskView的onLongClick函数中,来分析EventBus实例。
TaskView注册了Event事件,之后发送DragStartEvent事件,DragStartEvent是EventBus.Event的子类,只有三个参数主要是用来传输数据。所有注册了Event事件的观察者,如果在onEventBus函数中的参数为DragStartEvent事件,就可以接收到发送的DragStartEvent对象。下图为EventBus类图。
在RecentsView.java中attached to Window的时候对RecentsView与RecentsViewTouchHandler注册Event事件,detached to window时注销Event事件。
RecentsViewTouchHandler的优先级要比RecentsView的高,所以接收DragStartEvent事件较早。下面分析如何对Event事件处理,进行分屏的,整体流程见下图。
在TaskView中发送DragStartEvent事件,首先在RecentsViewTouchHandler.java的onEventBus函数接收到该事件。
在RecentsViewTouchHandler中首先清理之前的信息,如果系统支持多窗口,并且此时不处于分屏模式,就会判断当前的task是否支持分屏,如果不支持分屏,就会提示用户该应用不支持分屏,如果支持分屏,根据当前系统的方向来获取分屏的状态。最后将获取的分屏状态记录在mVisibleDockStates和mDropTargets列表中。
在getDockStatesForCurrentOrientation函数中根据Configuration判断是平板电脑还是手机,还是横屏竖屏。
手机设备在横屏状态只允许左右分屏,在竖屏状态只允许上下分屏,由于设备太小了。
平板电脑在横屏状态可以往左边也可以往右边分屏,而在竖屏状态就和手机一样只能往上边分。
总之,接收到事件后从event中获取对应的Task以及TaskView,清理mDropTargets与mVisibleDockStates列表内容。只有系统支持多窗口模式,并且此时不处于分屏模式,并且分屏后的窗口大小要大于等于最小的窗口尺寸才可以进行分屏。如果应用不支持分屏就会发送ShowIncompatibleAppOverlayEvent事件,提示用户应用不支持分屏。如果支持分屏就调用getDockStatesForCurrentOrientation函数获取当前的设备是平板还是手机,是要上下分屏还是左右分屏。
最后将获得分屏状态保存在mDropTargets与mVisibleDockStates列表中。RecentsViewTouchHandler处理完DragStartEvent事件后,分发给RecentsView.java,由RecentsView进行处理。
首先调用updateVisibleDockRegions来更新可见的分屏区域,根据当前屏幕方向获取分屏状态(左右,上下分屏),此时为默认分屏状态,dockAreaAlpha为80背景区域为灰白色,hintTextAlpha为255字体不透明。
将newDockStates转变为ArraySet,获取mVisibleDockStates对象,此时mVisibleDockStates有两个对象与newDockStates中的对象一致。遍历mVisibleDockStates中两个对象,获得areaAlpha为80,hintAlpha为255,下面主要计算bounds的值。
由于isDefaultDockState的值为true,所以调用getPreDockedBounds函数,获得准备分屏模式的边界区。
获取bounds后使用动画显示对应区域,如下图所示。
当屏幕显示出来分屏区域后,抬起手指停止拖拽TaskView,在RecentsView的onTouchEvent函数中接收touch事件,
之后调用RecentsViewTouchHandler的handleTouchEvent处理触摸事件。最后调用handleTouchEvent处理。
在手指移动过程中获取坐标点evX,evY,在进行分屏时mLastDropTarget的值为null,并且currentDropTarget也为null,所以遍历mDropTargets列表,根据前面DragStartEvent事件可以知道mDropTargets列表中是左右分屏或者上下分屏的DockState对象,就会调用DockState的acceptDrop函数来判断手指移动的位置是否可以进行分屏。
TaskStack.java中的DockState实现了DropTarget接口,所以调用的为DockState的acceptsDrop函数,
isCurrentTarget传入的为false,使用touchArea矩形区来根据设备屏幕尺寸来重新计算矩形面积,根据系统边衬区重新计算矩形边界。最后判断手指移动的位置是否在矩形区域内,如果在矩形范围内返回true。由于touchArea与上面提到的dockedArea相同,所以两者最后的矩形大小相同,也就是手指移动到上面截图灰白矩形区,就返回true。
回到handleTouchEvent函数中,target赋值给currentDropTarget。跳出循环,将currentDropTarget记录在mLastDropTarget中,将mDragTask与currentDropTarget封装进Event事件中,发送DragDropTargetChangedEvent给接收者。
在RecentsView中接收DragDropTargetChangedEvent事件。
根据上边流程知道event.dropTarget为之前获得的currentDropTarget对象,并且该对象属于DockState类型,所以进入else语句。调用updateVisibleDockRegions函数更新分屏区域。该函数之前分析过主要获得areaAlpha,hintAlpha与bounds的值,由于overrideAreaAlpha与overrideHintAlpha的值都设为-1,所以areaAlpha,hintAlpha都使用ViewState的默认值,areaAlpha为80仍为灰白色,hintAlpha为0透明。而isDefaultDockState为false调用getDockedBounds函数获得bounds值。
首先根据系统配置判断是否是水平分割屏幕,okay pad为垂直分割屏幕isHorizontalDivision为false,insets为Rect(0, 36 - 0, 0),也就是insets.left=0,inset.top=36,inset.right=0,inset.bottom=0。dividerSize为分屏后两个屏幕之间分割条的大小,首先计算屏幕中间位置。根据分屏方向,屏幕的一半减去分割线二分之一。
根据获得的中间位置计算新窗口的边界。首先将newWindowBounds设置为整个窗口大小,之后再根据分屏在哪边进行设置边界。最后通过sanitizeStackBounds函数判断边界的有效性.
通过计算获得一半的矩形窗口,使用ViewState的startAnimation函数显示窗口,如下图灰白色区域。
当屏幕显示出来分屏区域后,抬起手指停止拖拽TaskView,在RecentsViewTouchHandler的handleTouchEvent中处理抬起事件。
将mDragTask,mTaskView,mLastDropTarget封装进DragEndEvent事件中,发送给注册该事件的对象接收。由于RecentsView注册了该事件,所以会对其进行处理。
结束拖拽后会对之前显示的分屏区域进行隐藏,获取SystemServiceProxy对象,调用startTaskInDockedMode函数,这就进入了多窗口分屏的核心代码中了。
在SystemServiceProxy中将创建stack的模式,以及stack的ID封装进ActivityOptions对象中,之后调用AMS启动Activity。主要在ActivityStackSupervisor对象中处理启动应用事件,在函数startActivityFromRecentsInner中首先将传递的参数获取出来记录在activityOptions,以及launchStackId中。如果此时启动的stack id为分屏的stack,将stack类型以及初始化边界记录在WMS中,延迟更新Home stack,由于需要根据分屏 Windows的边界重新更新home stack的大小,当分屏activity启动完成后才会对home stack重新更新计算边界。
下面调用anyTaskForIdLocked函数,根据TaskId为对应任务创建stack对象。首先需要获取此时系统中的display数量,遍历所有display,获取每一个display上面的stack对象,判断一下是否对应的task已经存在某一个stack中了,如果存在就将获取到的TaskRecord返回。
当没有在的stack中找到对应的task,说明应用未启动属于冷启动分屏,就会创建分屏stack,并将task放入stack中,启动分屏应用。
如果已经在现有stack中获取到的对应task,就属于热启动分屏,就会将task从之前的stack中移到分屏stack中,启动分屏应用。
在冷启动分屏时,此时的task不存在active list,就需要从最近任务列表中寻找,在最近任务列表中找到后就调用restoreRecentTaskLocked函数将该task restore到active list中。根据对应的条件重新获得stackId,有了id后通过getStack函数获得所需要的stack,最后将Task加入新获得的stack中就结束了,下面主要分析getStack函数,获取stack过程。
在getStack函数中首先根据stackId从mActivityContainers列表中获取对象,如果activityContainer对象不为空说明需要的stack已经存在,直接返回。如果对象为空,就会将stackId,设备号等作为参数传递给createStackOnDisplay函数,在特定设备上创建stack。
在createStackOnDisplay函数创建stack过程,主要创建ActivityContainer对象,然后以stackId为key,activityContainer为value放入mActivityContainers中,通过ActivityContainer对象的attachToDisplayLocked将stack与设备关联,最后将mStack对象返回。在ActivityContainer的构造函数中创建ActivityStack对象mStack,这就是我们需要的stack对象。
在ActivityContainer的构造函数中主要创建对象。
获取到stack后,调用attachToDisplayLocked函数将stack attach到对应的display上去。
在attachToDisplayLocked中主要做的事情为:
在ActivityStack的attachDisplay函数中,核心代码为:
获取分屏stack size是通过WindowManagerService的attachStack来获得的,在该函数中首先通过displayId获取到displayContent对象,在mStackIdToStack与displayContent中判断是否已经存在了TaskStack对象,如果不存在就会创建一个TaskStack对象,并加入mStackIdToStack与displayContent中,之后调用TaskStack的attachDisplayContent函数关联设备,计算stack size,最后通过TaskStack的getRawBounds函数获得stack size对象 bounds,将bounds返回给ActivityStack的mBounds对象。下面主要分析计算stack size过程。
在TaskStack对象中调用attachDisplayContent函数,由于此时是多窗口分屏模式,mStackId为DOCKED_STACK_ID,之后通过displayContent的内部函数获得设备的逻辑尺寸,并记录在mTmpRect对象中,如果设备没有转屏,屏幕尺寸为1200x825,此时mTmpRect对象的值为(0, 0, 1200, 825),由于此时dockedStack还没有值,所以mTmpRect2为(0, 0, 0, 0),dockedOnTopOrLeft的值在SystemServiceProxy中传入,分屏的位置。
通过getStackDockedModeBounds函数获得dockedStack的bounds。
outBounds就为传入的bounds对象,首先将displayRect即获得的mTmpRect(0, 0, 1200, 825),设备的最大尺寸赋值给bounds,如果此时是分屏的stack,再重新根据设备尺寸,分割线divider尺寸来计算分屏后的中间位置,如果此时分屏位置在左边,并且屏幕的宽大于高,position为600,那么outBound.right=600, 而outBounds的值为(0,0,600,825),矩形面积为屏幕的一半。获取到分屏后的矩形面积后,在attachDisplayContent函数中调用updateDisplayInfo函数更新设备信息。最后调用setBounds函数将获得的bounds保存在mBounds中。
TaskStack的getRawBounds函数获得mBounds对象,最后返回到ActivityStack的mBounds中。获取到分屏窗口的size后,就可以根据size,resize stack的大小了。
当获得分屏的stack size后,如果当前的stack为分屏的stack,就调用resizeDockedStackLocked函数来对分屏stack resize。首先通过getStack函数从mActivityContainers列表中取出之前创建的DOCKED STACK,也就是分屏的stack,获取分屏stack中最顶端正在运行的ActivityRecord。之后调用resizeStackUncheckedLocked函数进行对分屏stack进行resize。
在resizeStackUncheckedLocked函数中如果允许更新bounds就继续更新bounds,将DOCKED STACK中的所有task取出来,如果task可以resize的话就更新Configuration,将新的Task bounds设置给对应task,将所有task进行冻结。
最后,调用WMS的resizeStack函数最终对stack里面所有的task重新计算一下尺寸,将最终获取到的分屏size bounds设置给DOCKED STACK。
在WindowManagerService的resizeStack函数中,根据 stackId从mStackIdToStack中获取到对应的TaskStack,然后调用TaskStack的setBounds来设置边界。当设置边界成功后,并且该stack可以被看到就进行layout。最后将stack是否全屏返回。
在TaskStack的setBounds的函数中,首先调用setBounds函数来为stack设置边界,最后遍历mTasks列表中的task对象,并且取出对应的config,对每一个task进行设置边界。
将分屏stack的size重新计算,并且将stack中的所有task重新计算边界后。重新回到resizeDockedStackLocked函数中继续往下执行第九步,getStackDockedModeBounds通过分屏stack的边界,获取到home stack的边界大小。
在WMS中根据stackId获取到mStackIdToStack列表中保存的TaskStack对象,调用TaskStack的getStackDockedModeBoundsLocked函数来获得home stack边界。
在getStackDockedModeBoundsLocked函数中根据DOCKED_STACK_ID获取到mStackIdToStack保存的对应dockedStack对象。
在获取到分屏stack后,如果不忽视stack的可视性,并且分屏stack不可见,就将设备的尺寸返回给home stack,全屏显示。否则获取分屏在那一侧dockedSide。将Home stack所在设备的屏幕尺寸保存在mTmpRect中,将dockedStack的矩形尺寸保存在mTmpRect2中,获取此时分屏在屏幕的位置dockedOnTopOrLeft。根据getStackDockedModeBounds函数获得分屏区之外HOME stack的矩形尺寸。
由于stackId为为HOME_STACK_ID,dockedStack为false最后根据分屏是在左侧还是右侧,来重新计算,上,下,左,右,边界位置。如果分屏stack在屏幕左侧占屏幕一半,那么Home stack就在屏幕右侧占屏幕一半。
获取到分屏后Home Stack的尺寸后,回到第12步,遍历系统中的所有stack,如果该stack可以被分屏stack resize,并且该stack存在,就调用resizeStackLocked函数。
从mActivityContainers中获取Home Stack,在resizeStackLocked函数中调用resizeStackUncheckedLocked函数,然后调用updateBoundsAllowed函数来判断是否需要更新边界,由于在开始分屏时调用ActivityStackSupervisor的startActivityFromRecentsInner函数中,将Home stack的边界延迟更新,设置mUpdateBoundsDeferred为true,所以在调用updateBoundsAllowed时就会将Home Stack边界,记录在mDeferredBounds中。
对分屏的stack size获取完之后,就可以启动Activity了。在ActivityStackSupervisor的startActivityFromRecentsInner函数的最后调用AMS的startActivityInPackage函数进行启动Activity。
当分屏的ActivityTransition完成后,就会调用ActivityStackSupervisor的notifyAppTransitionDone函数,这时就会继续更新HOME_STACK的边界
根据stackid获取Home stack,进行更新Home stack的边界。
获取Home stack中的mDeferredBounds,来对home stack进行更新。