基于Android R版本分析
多窗口模式
在Android 7.0中引入了一个新的多任务处理功能:多窗口支持,其核心思想:
- 分栈
- 设置栈边界
多窗口框架中,总共包含三种模式:
- Split-Screen Mode:分屏模式;
- Freeform Mode:自由模式,类似于Windows的窗口模式;
- Picture In Picture Mode:画中画模式;
多窗口模式迭代
Android 7.0
Android N在手持设备上支持分屏模式,在电视上支持画中画模式;
- 分屏模式:分屏模式会以左右并排或者上下并排的方式分屏显示两个应用,用户可以拖动两个应用之间的分割线,放大其中一个应用,同时缩小另一个应用;
- 画中画模式:可以让用户在与另一个应用互动的同时继续播放视频;
如果不需要使用多窗口模式,可以通过设置:resizeableActivity="false"
来停用多窗口模式;
Andorid 8.0
将PIP模式扩展到了手持设备中;
Android 12
将多窗口模式作为标准行为;
多窗口模式配置
如果应用以 Android 7.0(API 级别 24)或更高版本为目标平台,就可以配置应用的 activity 是否支持多窗口模式以及如何支持;
resizeableActivity
在AndroidManifest.xml的 或 元素中设置此属性,即可针对 API 级别 30 及更低级别启用或停用多窗口模式:
<application
android:name=".MyActivity"
android:resizeableActivity=["true" | "false"] />
- true:activity 能以分屏模式和自由窗口模式启动;
- false:activity 不支持多窗口模式,如果用户尝试在该多窗口模式下启动 activity,则 activity 会全屏显示;
supportsPictureInPicture
在AndroidManifest.xml的 节点中设置此属性,代表Activity是否支持画中画:
<activity
android:name=".MyActivity"
android:supportsPictureInPicture=["true" | "false"] />
configChanges
如需自行处理多窗口配置更改(例如当用户调整窗口大小时),请将至少指定以下值的 android:configChanges
属性添加到应用清单的节点中:
<activity
android:name=".MyActivity"
android:configChanges="screenSize | smallestScreenSize
| screenLayout | orientation" />
添加 android:configChanges
后,activity 和 fragment 会收到对 onConfigurationChanged()
的回调,而不是被销毁并重新创建。然后可以根据需要手动更新视图、重新加载资源以及执行其他操作;
PIP
从 Android 8.0(API 级别 26)开始,Android 允许以画中画 (PiP) 模式启动 activity。画中画是一种特殊类型的多窗口模式,最常用于视频播放。使用该模式,用户可以通过固定到屏幕一角的小窗口观看视频,同时在应用之间进行导航或浏览主屏幕上的内容;
PIP 使用
针对SystemUI模块:
<activity
android:name=".pip.phone.PipMenuActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:excludeFromRecents="true"
android:exported="false"
android:launchMode="singleTop"
android:permission="com.android.systemui.permission.SELF"
android:resizeableActivity="true"
android:stateNotNeeded="true"
android:supportsPictureInPicture="true"
android:taskAffinity=""
android:theme="@style/PipPhoneOverlayControlTheme"
androidprv:alwaysFocusable="true" />
PictureInPictureParams 配置
PictureInPictureParams:表示一组参数,用于以图中图模式初始化和更新Activity;
/**
* The expected aspect ratio of the picture-in-picture.
* 画中画的预期宽高比
*/
@Nullable
private Rational mAspectRatio;
/**
* The set of actions that are associated with this activity when in picture-in-picture.
* 在画中画中与此活动相关联的一组操作
*/
@Nullable
private List<RemoteAction> mUserActions;
/**
* The source bounds hint used when entering picture-in-picture, relative to the window bounds.
* We can use this internally for the transition into picture-in-picture to ensure that a
* particular source rect is visible throughout the whole transition.
* 输入画中画相对于窗口边界时使用的源边界提示
* 我们可以在内部将其用于画中画的转换,以确保特定的源矩形在整个转换过程中都是可见的
*/
@Nullable
private Rect mSourceRectHint;
RemoteAction:表示可以从另一个进程调用的远程操作。动作可以有一个相关的可视化,包括像图标或标题这样的元数据;
public final class RemoteAction implements Parcelable {
private final Icon mIcon;
private final CharSequence mTitle;
private final CharSequence mContentDescription;
private final PendingIntent mActionIntent;
private boolean mEnabled;
private boolean mShouldShowIcon;
……………………
}
PictureInPictureParams 创建 & 配置
/**
* The arguments to be used for Picture-in-Picture mode.
*/
private final PictureInPictureParams.Builder mPictureInPictureParamsBuilder =
new PictureInPictureParams.Builder();
// 配置画中画宽高比
Rational aspectRatio = new Rational(mMovieView.getWidth(), mMovieView.getHeight());
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
开启 PIP
PictureInPictureParams配置完成之后,直接调用Activity的enterPictureInPictureMode方法就可以直接进行画中画显示了;
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
PIP 流程分析
Android N
- PIP模式分栈
- PIP模式设置栈边界
在Android N中,我们通过stack ID来区分是普通stack还是画中画stack,但是在后续的版本中,我们通过getWindowMode来判断当前Activity的类型;
stack ID
/** First static stack ID. */
public static final int FIRST_STATIC_STACK_ID = 0;
/** Home activity stack ID. */
public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;
/** ID of stack where fullscreen activities are normally launched into. */
public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;
/** ID of stack where freeform/resized activities are normally launched into. */
public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;
/** ID of stack that occupies a dedicated region of the screen. */
public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;
/** ID of stack that always on top (always visible) when it exist. */
public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;
Android R
WindowingMode
/** Windowing mode is currently not defined. */
public static final int WINDOWING_MODE_UNDEFINED = 0;
/** Occupies the full area of the screen or the parent container. */
// 普通全屏窗口
public static final int WINDOWING_MODE_FULLSCREEN = 1;
/** Always on-top (always visible). of other siblings in its parent container. */
// 画中画
public static final int WINDOWING_MODE_PINNED = 2;
/** The primary container driving the screen to be in split-screen mode. */
// TODO: Remove once split-screen is migrated to wm-shell.
// 分屏主窗口
public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3;
/**
* The containers adjacent to the {@link #WINDOWING_MODE_SPLIT_SCREEN_PRIMARY} container in
* split-screen mode.
* NOTE: Containers launched with the windowing mode with APIs like
* {@link ActivityOptions#setLaunchWindowingMode(int)} will be launched in
* {@link #WINDOWING_MODE_FULLSCREEN} if the display isn't currently in split-screen windowing
* mode
* @see #WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY
*/
// TODO: Remove once split-screen is migrated to wm-shell.
// 分屏副窗口
public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4;
/**
* Alias for {@link #WINDOWING_MODE_SPLIT_SCREEN_SECONDARY} that makes it clear that the usage
* points for APIs like {@link ActivityOptions#setLaunchWindowingMode(int)} that the container
* will launch into fullscreen or split-screen secondary depending on if the device is currently
* in fullscreen mode or split-screen mode.
*/
// TODO: Remove once split-screen is migrated to wm-shell.
public static final int WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY =
WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
/** Can be freely resized within its parent container. */
// TODO: Remove once freeform is migrated to wm-shell.
// 自有窗口
public static final int WINDOWING_MODE_FREEFORM = 5;
/** Generic multi-window with no presentation attribution from the window manager. */
// 没有来自窗口管理器的表示属性的通用多窗口
public static final int WINDOWING_MODE_MULTI_WINDOW = 6;
在Android R中使用了新的mode值;
相关类介绍
- ActivityRecord:跟踪每个 Activity 的画中画状态;
- ActivityManagerService:主Activity来请求进入PIP,以及从WindowManager和SystemUI更改画中画Activity的状态;
- ActivityStackSupervisor:从ActivityManagerService调用以将任务移入出固定的堆栈,并根据需要更新WindowManager;
- PipManager:支持PIP模式功能的核心类,由PipUI组件创建;
- PipTouchHandler:触摸处理程序,用于控制操控画中画的手势。只有在画中画的处于活动状态的用户处于此状态时,才使用该类;
- PipMotionHelper:辅助类,用于跟踪画中画的位置和屏幕上允许的区域。通过调用ActivityManagerService来更新画中画,或调整画中画的位置和大小;
- PipMenuActivityController:启动一个Activity,以显示当前画中画中的Activity提供的操作,此Activity是任务叠加层Activity,并移除叠加输入使用方,使其处于互动状态;
- PipMenuActivity:菜单Activity的实现;
- PipMediaController:当媒体会话以可能影响PIP的默认操作的方式发生变化时更新SystemUI的监听器;
PIP 显示位置
- config_defaultPictureInPictureGravity:重力整数,用于控制放置PIP的角落,例如:BOTTOM或者RIGHT;
- config_defaultPictureInPictureScreenEdgeInsets:放置画中画的位置与屏幕侧边的偏移量;
- config_pictureInPictureDefaultSizePercent & config_pictureInPictureDefaultAspectRatio:屏幕宽度的百分比和宽高比的组合控制PIP的大小;
PIP 启动
PIP 预备工作
在SystemUI模块启动的时候,会将各个组件都启动,其中就有一个组件:PipUI;
在PipUI组件启动的过程中会创建PipManager,PipManager类用于管理SystemUI模块中整个PIP模块的所有功能;
- 在PipManager的构造过程中,首先先创建PipTaskOrganizer实例,即PIP任务管理器,用于处理后续PIP Task变更的事务;
- 将该TaskOrganizer向WMS中的TaskOrganizerController中进行保存维护,待后续使用;
TaskOrganizerController将ITaskOrganizer对象添加到mTaskOrganizerStates管理,mTaskOrganizerStates是一个HashMap,key是Binder对象,value则是内部的代理类TaskOrganizerState;
PIP 应用层开启
多窗口的工作原理,在Android R也是一致的;
-
创建PictureInPictureParams实例,用于配置PIP使用到的参数配置信息;
-
调用enterPictureInPictureMode()方法开启PIP模式;
-
判断设备是否支持以及PictureInPictureParams的有效性,判断当前Activity是否处于PIP模式;
-
将PictureInPictureParams保存到ActivityRecord进行维护;
-
调用moveActivityToPinnedStack()方法执行分栈操作和resize bound操作;
- 判断当前是否存在Pinned类型stack,如果存在,需要将Pinned Stack中的Activity清空,即dismissPip()方法;
- 如果不存在对应的Pinned Stack,则需要创建Pinned Stack;
- resize bound,重新计算PIP bound值;
-
最后调用ensureActivityVisible()方法显示Activity;
-
Task 状态变更
当对应的task状态变化回调时再从mTaskOrganizerStates这个HashMap取出;
更新task状态,通知task对象已经被organized,关联TaskOrganizer对象,task.setTaskOrganizer(ITaskOrganizer organizer);
PIP 响应 Task 变更
- TaskOrganizer触发了PipTaskOrganizer的onTaskAppeared()回调方法,至此进入了SystemUI的PIP模式;
- 创建WindowContainerTransaction(窗口容器事务管理器),用于响应窗口配置更新的事务处理;
- 将创建好的WindowContainerTransaction传入到WMS进行维护,同时直接调用applySyncTransaction(),向WMS发送需要处理的事务(Transaction);
- PipTaskOrganizer响应自己的事务,通过onTransactionReady()方法实现;
PIP Animation 响应
-
执行SurfaceControl.Transaction的apply();
-
创建PipTransactionAnimator和PipAnimationController,用于处理PIP Animator事务,其中核心为三个部分:
- Animation start:开启动画的显示;
- Animation update:因为PIP为小窗口显示,有从大到小的显示动效,所以需要连续性的更新PIP bound的值;
- Animation end:当PIP bound更新到指定bound后,开始显示PIP Menu Activity;
PIP Menu Activity
PIP Animation显示完成之后,就需要开始显示PIP Menu Activity,用于向用户提供可见的、可控制的图标等内容,例如设置、关闭、退出PIP等;
参考
- github demo: PiP test;
- 官方文档(PIP):对画中画 (PiP) 的支持 / 画中画;
- 官方文档(多窗口支持):多窗口模式;