Android多窗口模式-SplitScreen

基于Android R版本分析

分屏

和PIP模块一样,Split Screen也属于多窗口机制中的一种场景,在SystemUI模块中的Divider管理着所有关于分屏的对象;

  • DividerView:分屏分割线,分屏显示界面;
  • SplitScreenTaskOrganizer:分屏Task组织者,分屏逻辑;

在Android原生系统中,分屏功能是通过Recents(近期任务)功能进入:

Stack & Task 层次接口

bash 复制代码
nobo@nobo-System-Product-Name:~/dupz1019/SystemUI/log$ adb shell am stack list
Stack id=4 bounds=[0,1498][1440,2960] displayId=0 userId=0
 configuration={1.0 ?mcc?mnc [zh_CN_#Hans,en_US] ldltr sw369dp w411dp h369dp 560dpi smll hdr widecg land finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 1498 - 1440, 2960) mAppBounds=Rect(0, 1498 - 1440, 2792) mWindowingMode=split-screen-secondary mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.10}
  taskId=1085: com.android.gallery3d/com.android.gallery3d.app.GalleryActivity bounds=[0,1498][1440,2960] userId=0 visible=true topActivity=ComponentInfo{com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity}
  taskId=1083: com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher bounds=[0,485][1440,2960] userId=0 visible=true topActivity=ComponentInfo{com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity}
  taskId=1084: com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity bounds=[0,1498][1440,2960] userId=0 visible=true topActivity=ComponentInfo{com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity}
​
Stack id=3 bounds=[0,0][1440,1464] displayId=0 userId=0
 configuration={1.0 ?mcc?mnc [zh_CN_#Hans,en_US] ldltr sw369dp w411dp h369dp 560dpi smll hdr widecg land finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1440, 1464) mAppBounds=Rect(0, 171 - 1440, 1464) mWindowingMode=split-screen-primary mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.10}
  taskId=1086: com.android.dialer/com.android.dialer.main.impl.MainActivity bounds=[0,0][1440,1464] userId=0 visible=true topActivity=ComponentInfo{com.android.dialer/com.android.dialer.main.impl.MainActivity}

ConfigurationContainer

scala 复制代码
public abstract class ConfigurationContainer<E extends ConfigurationContainer> {}

ConfigurationContainer包含了具有覆盖configuration并以层次结构组织的类的通用逻辑;

ConfigurationContainer是一个Container,类似WindowContainer容器,ConfigurationContainer容器用于存放Configuration对象;

ConfigurationContainer有两个子类:

  • WindowContainer
  • WindowProcessController:保存一些进程相关的信息,是WMS和AMS沟通的媒介;

ConfigurationContainer虽然没有参与层次结构的构建,但是却借助了WindowContainer构建起来的层次结构,从上到下进行了Configuration的分发;

adb shell dumpsys activity a:打印全屏状态下的ActivityRecord的Configuration信息;

Configuration
kotlin 复制代码
public final class Configuration implements Parcelable, Comparable<Configuration> {}

这个类描述了所有可能影响应用程序检索的资源的设备配置信息,包括用于指定的配置选项(语言区域列表和缩放)以及设备的配置(如输入模式、屏幕大小和屏幕方向);

Configuration中包含了一个变量:WindowConfiguration,WindowConfiguration持有了窗口状态相关的配置信息,作为Configuration的一部分,跟随Configuration一起进行分发;

WindowConfiguration
kotlin 复制代码
public class WindowConfiguration implements Parcelable, Comparable<WindowConfiguration> {}

这个类用于描述窗口配置或者状态信息的类,用于那些直接或间接包含窗口的容器;

WindowConfiguration Param
yaml 复制代码
configuration={1.0 ?mcc?mnc [zh_CN_#Hans,en_US] ldltr sw369dp w411dp h369dp 560dpi smll hdr widecg land finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 1498 - 1440, 2960) mAppBounds=Rect(0, 1498 - 1440, 2792) mWindowingMode=split-screen-secondary mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.10}

mBounds

java 复制代码
private Rect mBounds = new Rect();  // mBounds=Rect(0, 1498 - 1440, 2960)

代表了Container的边界。例如Task,调用WindowConfiguration的getBounds()方法,返回的就是这个bounds,这个bounds代表的就是Task的边界;

mAppBounds

yaml 复制代码
private Rect mAppBounds;  // mAppBounds=Rect(0, 1498 - 1440, 2792)

代表的是App的可用边界,对比mBounds,区别在于mAppBounds的计算考虑到了insets,需要考虑StatusBar和NavigationBar的高度,而mBounds的边界不考虑insets;

同时,mAppBounds一个比较重要的作用就是,计算Configuration的screenWidthDp和screenHeightDp;

  • screenWidthDp = w 411dp
  • screenHeightDp = h 369dp
  • densityDpi = 560dpi

mWindowingMode & mDisplayWindowingMode

arduino 复制代码
/** The current windowing mode of the configuration. */
private @WindowingMode int mWindowingMode;  // split-screen-secondary
​
/** The display windowing mode of the configuration */
private @WindowingMode int mDisplayWindowingMode;  // fullscreen
  • WindowingMode:代表了当前container处于哪一种多窗口模式;
  • DisplayWindowingMode:代表了当前container所在的Display处于哪一种多窗口模式里;

mActivityType

arduino 复制代码
/** The current activity type of the configuration. */
private @ActivityType int mActivityType;  // undefined

代表了当前container的Activity类型;

Activity Type Desc Value
ACTIVITY_TYPE_UNDEFINED 一般container刚刚创建时使用的模式,在后续使用中需要选择一种非ACTIVITY_TYPE_UNDEFINED的Activity类型 0
ACTIVITY_TYPE_STANDARD 标准模式,大部分App都是类型 1
ACTIVITY_TYPE_HOME Launcher类型的App,如google原生Launcher中的ActivityRecord都是这种类型 2
ACTIVITY_TYPE_RECENTS Recent或者Overview类型,系统中只有一个Activity是这个类型:RecentsActivity - Android Q 是通过判断packageName = com.android.systemui.recents来进行赋值的 - Android R 是通过判断对应Component是否为Recent组件且是否与Recent组件共享相同的uid 3
ACTIVITY_TYPE_ASSISTANT Assistant类型,目前接触的不多,多和语音助手服务有关 4
ACTIVITY_TYPE_DREAM Dream类型,壁纸类型相关的Activity 5

mAlwaysOnTop

arduino 复制代码
/** The current always on top status of the configuration. */
private @AlwaysOnTop int mAlwaysOnTop;  // undefined

用来声明当前container是否总是处于顶层;

ALWAYS Type Desc Value
ALWAYS_ON_TOP_UNDEFINED 当前没有定义Always on top 0
ALWAYS_ON_TOP_ON 当前在此配置中处于开启状态 1
ALWAYS_ON_TOP_OFF 当前该配置为关闭状态 2

我们看一下ALWAYS_ON_TOP的判断条件:

kotlin 复制代码
/**
  * Returns true if the container associated with this window configuration is always-on-top of
  * its siblings.
  * @hide
  */
public boolean isAlwaysOnTop() {
    if (mWindowingMode == WINDOWING_MODE_PINNED) return true;  // PIP模式
    if (mActivityType == ACTIVITY_TYPE_DREAM) return true;  // 壁纸场景
    if (mAlwaysOnTop != ALWAYS_ON_TOP_ON) return false;  // 没有开启置顶开关
    return mWindowingMode == WINDOWING_MODE_FREEFORM
        || mWindowingMode == WINDOWING_MODE_MULTI_WINDOW;
}

这几种场景下,需要有优先级的排序,PIP优先级最高,其次Dream。针对除这两种以外的场景,需要满足两个条件:

  • mAlwaysOnTop = ALWAYS_ON_TOP_ON
  • mWindowingMode = WINDOWING_MODE_FREEFORM || WINDOWING_MODE_MULTI_WINDOW

mRotation

arduino 复制代码
private int mRotation = ROTATION_UNDEFINED;  // ROTATION_0

当前container的旋转角度,只和当前container所在的display相关,和container所在层级结构无关;

一般情况下,各级的container都是直接继承Display的rotation,但是ActivityRecord中针对尺寸兼容模式有额外的逻辑;

流程分析

Divider init & start

  • 创建SplitScreenTaskOrganizer实例,SplitScreenTaskOrganizer继承自TaskOrganizer,TaskOrganizer:接收ActivityTaskManager/WindowManager委派任务控制的接口,SplitScreenTaskOrganizer扩展了TaskOrganizer,实现分屏功能的任务控制逻辑;
  • 创建DisplayController实例,DisplayController用于处理WMS的显示变化,通过调用WMS的registerDisplayWindowListener()方法向底层注册WMS 监听窗口变化的事件;
  • 创建SystemWindows实例和WindowManagerProxy实例,WindowManagerProxy中创建了SyncTransactionQueue实例,SyncTransactionQueue用于序列化同步WindowContainerTransaction和相应Callback的助手类;
  • 创建DividerWindowManager实例,用于管理Docked Stack;
  • 调用addDisplayWindowListener()方法,监听Window的变化,这个listener是SystemUI内部的Listener,IDisplayWindowListener接收到回调之后,会调用该Listener向SystemUI上报;

在init过程中,通过DisplayController的registerDisplayWindowListener()方法向底层注册了IDisplayWindowListener监听器, 注册完成之后,就会直接进行遍历Listener集合,遍历RootWindowContainer下的所有child,将对应的child的displayId通知给listener;

  • SystemUI应用通过IDisplayWindowListener的onDisplayAdded()回调方法,获取到的对应的displayId值,然后根据对应的DisplayId获取一些对应的参数值(context、DisplayLayout等),然后通过OnDisplaysChangedListener类型的监听器向SystemUI上报;
  • 在OnDisplaysChangedListener的onDisplayAdded()方法中,创建SplitDisplayLayout实例,SplitDisplayLayout用于处理分屏相关的内部显示布局;
  • 然后判断该版本是否支持分屏功能,不支持则直接退出;
  • 然后调用SplitScreenTaskOrganizer的init()方法,初始化SplitScreenTaskOrganizer实例;
  • 然后通过SplitDisplayLayout的resizeSplits()方法设置了Primary Screen和Secondary Screen的Bound和Smallest(最小的) ScreenWidthDp属性值,通过WindowContainerTransaction的applyTransaction方式进行处理这些操作;
  • 注册任务堆栈侦听器,该侦听器在任务堆栈更改时获取回调;
registerOrganizer

我们知道,SplitScreenTaskOrganizer的过程分为两部分:创建和初始化。在初始化过程中,向底层注册了TaskOrganizer,即SplitScreenTaskOrganizer,SplitScreenTaskOrganizer绑定了两种WindowingMode:

  • WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
  • WINDOWING_MODE_SPLIT_SCREEN_SECONDARY

然后通过TaskOrganizerController机制,通过onTaskAppeared()方式为Primary Stack和Secondary Stack创建对应的SurfaceControl实例,用于显示图像内容;不过onTaskAppeared()方法是在Task创建成功之后,在addTask()的时候才会被回调;

createRootTask

在执行完registerOrganizer之后,会等待Task的创建,只有在创建Task完成之后,才会创建对应的SurfaceControl实例;

相同的,调用两次createRootTask()方法分别创建Primary类型和Secondary类型的Task;

  • 根据默认的DisplayId来获取对应的DisplayContent,然后再根据获取到的DisplayContent实例获取默认的TaskDisplayArea实例,DefaultTaskDisplayArea指在专用于应用程序窗口的显示器上获取默认显示区域;
  • 调用DefaultTaskDisplayArea的createStack()方法,在该方法中首先会针对ActivityType以及WindowingMode的有效性进行判断;
  • 调用getNextStackId()获取新建Stack的Id值,然后调用createStackUnchecked()继续执行;
  • 然后创建ActivityStack实例,用于承载后续分屏过程中的Task;
  • 调用ActivityStack的setWindowingMode()方法,将创建好的ActivityStack和WindowingMode进行了绑定,然后触发onConfigurationChanged机制;
  • 最后通过getTaskInfo()方式从ActivityStack中获取到RunningTaskInfo实例,然后将RunningTaskInfo实例和ActivityStack实例以键值对的方式保存到mLastSentTaskInfos集合中进行维护;
SplitScreenTaskOrganizer # onTaskAppeared

当Primary Stack 和 Secondary Stack创建成功之后,就会调用TaskOrganizer的onTaskAppeared()回调方法,上报ActivityStack创建完成的结果,而TaskOrganizer对应的就是SplitScreenTaskOrganizer;

这个过程比较简单,为创建好的Primary和Secondary类型的RunningTaskInfo实例创建对应的SurfaceControl,然后调用SurfaceControl.Transaction中的apply()清除应用事务状态,并使其可作为新事务使用;

stack list
scss 复制代码
Stack id=3 bounds=[0,0][1440,1464] displayId=0 userId=0
 configuration={1.0 ?mcc?mnc [zh_CN_#Hans,en_US] ldltr sw369dp w411dp h369dp 560dpi smll hdr widecg land finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 0 - 1440, 1464) mAppBounds=Rect(0, 171 - 1440, 1464) mWindowingMode=split-screen-primary mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.10}
  taskId=3: unknown bounds=[0,0][1440,1464] userId=0 visible=false
​
Stack id=4 bounds=[0,1498][1440,2960] displayId=0 userId=0
 configuration={1.0 ?mcc?mnc [zh_CN_#Hans,en_US] ldltr sw369dp w411dp h369dp 560dpi smll hdr widecg land finger -keyb/v/h -nav/h winConfig={ mBounds=Rect(0, 1498 - 1440, 2960) mAppBounds=Rect(0, 1498 - 1440, 2792) mWindowingMode=split-screen-secondary mDisplayWindowingMode=fullscreen mActivityType=undefined mAlwaysOnTop=undefined mRotation=ROTATION_0} s.10}
  taskId=4: unknown bounds=[0,1498][1440,2960] userId=0 visible=false

这个就是Divider init&start完成之后,stack list的情况,其中包括了两个StackId = 3StackId = 4的ActivityStack,这个就是通过createRootStack创建的;

start SplitScreen

Divider和SplitScreenTaskOrganizer创建和初始化完成之后,就等待Android设备通过Recents来开启分屏功能;

start Primary SplitScreen

在点击分屏之后,会将当前对应的应用进程Task移动到Stack = 3(Primary Screen)中;

  • TaskShortcutFactory响应分屏功能的点击事件;

  • 首先先创建ActivityOptions实例,ActivityOptions是用于Activity转场动画,在其中配置了WindowingMode属性和SplitScreenCreateMode属性;

    • WindowingMode = WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
    • SplitScreenCreateMode = SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT
  • 然后就是调用ActivityManagerWrapper的startActivityFromRecents()方法进行分屏开启;

    在开启主分屏时,主屏一般是已经存在的Activity的Task,不需要重新启动,可以通过RootWindowContainer的anyTaskForId()方法找到对应的task;

    • 通过开启分屏的应用的taskId获取到对应的Task实例,同时也通过getLaunchStack()方法寻找对应的ActivityStack,而这个Stack的类型应该为SPLIT_SCREEN_PRIMARY类型的Stack;
    • 通过Task的reparent()方法重置应用Task所属的Stack,其中传入的Stack就是通过getLaunchStack()获取到的ActivityStack;
  • 针对Primary Stack,会调用ActivityStartController的postStartActivityProcessingForLastStarter()方法记录的最后一个起始者,重新评估是否可以移除;

  • 最终继续执行ActivityTaskManagerService的continueWindowLayout()方法;

continueWindowLayout

当上面的所有工作执行完成之后,调用ATMS的continueWindowLayout()继续窗口系统的绘制和显示逻辑;

我们知道,在上述过程中,主副屏task的状态变化和子Task操作(reparent、addChild)都会回调通知SplitScreenTaskOrganizer,回调SplitScreenTaskOrganizer的onTaskInfoChanged()方法;

handleTaskInfoChanged

这个过程也比较简单,主要涉及两组变量:

  • Changed before

    • primaryWasEmpty:用于描述changed之前primary screen topActivity的情况
    • secondaryWasEmpty:用于描述changed之前secondary screen topActivity的情况
  • Changed after

    • primaryIsEmpty:用于描述changed之后primary screen topActivity的情况
    • secondaryIsEmpty:用于描述changed之后secondary screen topActivity的情况

然后通过这些状态信息判断分屏状态;

startDismissSplit
startEnterSplit
ensureMinimizedSplit / ensureNormalSplit

start SecondaryScreen

相关推荐
Henry_He32 分钟前
桌面列表小部件不能点击的问题分析
android
工程师老罗1 小时前
Android笔试面试题AI答之Android基础(1)
android
qq_397562312 小时前
android studio更改应用图片,和应用名字。
android·ide·android studio
峥嵘life2 小时前
Android Studio版本升级那些事
android·ide·android studio
新手上路狂踩坑2 小时前
Android Studio的笔记--BusyBox相关
android·linux·笔记·android studio·busybox
一个儒雅随和的男子4 小时前
微服务详细教程之nacos和sentinel实战
微服务·架构·sentinel
腾讯云开发者5 小时前
AI时代,需要怎样的架构师?腾讯云架构师峰会来了!
架构
TroubleMaker5 小时前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西6 小时前
Android Studio IDE环境配置
android·ide·android studio
发飙的蜗牛'7 小时前
23种设计模式
android·java·设计模式