Android多窗口模式 - PIP

1,144 阅读8分钟

基于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 使用

screen.png

针对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

Android_N_PIP.jpeg

  • 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 预备工作

TaskOrganizer_registerTaskOrganizer.png

在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 应用层开启

enterPictureInPictureMode.png

多窗口的工作原理,在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 状态变更

TaskOrganizer_onTaskAppeared.png

当对应的task状态变化回调时再从mTaskOrganizerStates这个HashMap取出;

更新task状态,通知task对象已经被organized,关联TaskOrganizer对象,task.setTaskOrganizer(ITaskOrganizer organizer);

PIP 响应 Task 变更

PipTaskOrganizer_onTaskAppeared.png

  • TaskOrganizer触发了PipTaskOrganizer的onTaskAppeared()回调方法,至此进入了SystemUI的PIP模式;
  • 创建WindowContainerTransaction(窗口容器事务管理器),用于响应窗口配置更新的事务处理;
  • 将创建好的WindowContainerTransaction传入到WMS进行维护,同时直接调用applySyncTransaction(),向WMS发送需要处理的事务(Transaction);
  • PipTaskOrganizer响应自己的事务,通过onTransactionReady()方法实现;
PIP Animation 响应

PipTaskOrganizer_onTransactionReady.png

  • 执行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

PipManager_onPipTransitionFinishedOrCanceled.png

PIP Animation显示完成之后,就需要开始显示PIP Menu Activity,用于向用户提供可见的、可控制的图标等内容,例如设置、关闭、退出PIP等;

参考

  1. github demo: PiP test
  2. 官方文档(PIP):对画中画 (PiP) 的支持 / 画中画
  3. 官方文档(多窗口支持):多窗口模式