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

相关推荐
长亭外的少年6 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿9 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
哔哥哔特商务网9 小时前
一文探究48V新型电气架构下的汽车连接器
架构·汽车
007php0079 小时前
GoZero 上传文件File到阿里云 OSS 报错及优化方案
服务器·开发语言·数据库·python·阿里云·架构·golang
1024小神10 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛10 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法11 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
码上有前11 小时前
解析后端框架学习:从单体应用到微服务架构的进阶之路
学习·微服务·架构
NotesChapter12 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快13 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android