Android7.0多窗口实现原理

概述

在Android N(7.0)版本开始,系统支持了多窗口功能。在有了多窗口支持之后,用户可以同时打开和看到多个应用的界面。并且系统还支持在多个应用之间进行拖拽。在大屏幕设备上,这一功能非常实用。

在Android N中多窗口有三种表现形式:

  1. 分屏模式

该模式主要将屏幕一分为二,同时出现两个应用界面。

    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事件总线

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函数显示窗口,如下图灰白色区域。

开始分屏

获取分屏stack size

当屏幕显示出来分屏区域后,抬起手指停止拖拽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。

热启动分屏

 

 

//未完待续

 

猜你喜欢

转载自blog.csdn.net/fu_kevin0606/article/details/81191078