本文基于AOSP Android-7.1.1-R9代码进行分析。
Android N的的多窗口框架中,总共包含了三种模式。
- Split-Screen Mode: 分屏模式。
- Freeform Mode 自由模式:类似于Windows的窗口模式。
- Picture In Picture Mode:画中画模式(PIP)
经过一段时间的研究,总结一句话:多窗口框架的核心思想是分栈和设置栈边界。本文会从系统源码角度分析分栈以及设置栈边界的步骤和原理,从而解析多窗口三种模式的实现方式。
栈
既然提到了分栈,那我们首先要了解这个栈是什么?在Android系统中,启动一个Activity之后,必定会将此Activity存放于某一个Stack,在Android N中,系统定义了5种Stack ID,系统所有Stack的ID属于这5种里面的一种。不同的Activity可能归属于不同的Stack,但是具有相同的Stack ID。StackID如下图所示:
/** 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;
正常情况下,Launcher和SystemUI进程里面的Activity所在的Stack的id是HOME_STACK_ID, 普通的Activity所在的Stack的id是FULLSCREEN_WORKSPACE_STACK_ID,自由模式下对应的栈ID是FREEFORM_WORKSPACE_STACK_ID;分屏模式下,上半部分窗口里面的Activity所处的栈ID是DOCKED_STACK_ID;画中画模式中,位于小窗口里面的Activity所在的栈的ID是PINNED_STACK_ID;
栈边界
在多窗口框架中,通过设置Stack的边界(Bounds)来控制里面每个Task的大小,最终Task的大小决定了窗口的大小。栈边界通过Rect(left,top,right,bottom)来表示,存储了四个值,分别表示矩形的4条边离坐标轴的位置,最终显示在屏幕上窗口的大小是根据Stack边界的大小来决定的。
如图1-1所示,为分屏模式下的Activity的状态。整个屏幕被分成了两个Stack,一个DockedStack,一个FullScreenStack。每个Stack里面有多个Task,每个Task里面又有多个Activity。当我们设置了Stack的大小之后,Stack里面的所有的Task的大小以及Task里面所有的Activity的窗口大小都确定了。假设屏幕的大小是1440x2560,整个屏幕的栈边界就是(0,0,1440,2560)。
多窗口涉及到几大核心服务,WindowManagerService级相关类、ActivityManagerService和相关类、以及SystemUI里面的核心类,代码主要位于如下:
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
frameworks/base/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
frameworks/base/services/core/java/com/android/server/wm/TaskGroup.java
frameworks/base/services/core/java/com/android/server/wm/Task.java
frameworks/base/services/core/java/com/android/server/wm/TaskStack.java
frameworks/base/services/core/java/com/android/server/wm/TaskPositioner.java
frameworks/base/services/core/java/com/android/server/am/TaskPersister.java
frameworks/base/services/core/java/com/android/server/am/TaskRecord.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/services/core/java/com/android/server/am/ActivityStackSupervisor.java
frameworks/base/services/core/java/com/android/server/am/ActivityStack.javaframeworks/base/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
frameworks/base/packages/SystemUI/src/com/android/systemui/stackdivider/
画中画模式
画中画模式(PIP)是最简单的多窗口模式,进入Android画中画模式的Activity会在当前屏幕上显示一个小的窗口,如图2-1所示。
进入画中画模式很简单,直接在Activity里面调用enterPictureInPicture方法进入PIP模式。上面说到多窗口模式的核心是分栈和设置栈边界,接下来我们将一步步来分析画中画模式的框架原理,首先给出一张图说明下相关流程。
本文将根据分栈和设置栈边界两个核心来进行相关代码梳理。
PIP模式分栈
Step1-5
PIP模式下分栈核心代码,后面的步骤是设置栈边界的核心代码。
如前面所说,系统有5种Stack ID,PIP模式中的Activity所在的stack id是PINNED_STACK_ID。普通Activity位于id是FULLSCREEN_WORKSPACE_STACK_ID的stack里面。因此画中画模式分栈的核心工作是把activity从id是FULLSCREEN_WORKSPACE_STACK_ID的栈移动到id是PINNED_STACK_ID的stack里面。
本文会贴出部分代码加以分析,首先,Activity直接调用enterPictureInPictureMod进入画中画模式。
@Activity.java
public void enterPictureInPictureMode() {
try {
ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken);
} catch (RemoteException e) {
}
}
紧接着在ActivityManagerService的enterPictureInPictureMode方法中,会获取PIP窗口的默认大小。窗口的默认大小是mDefaultPinnedStackBounds来控制的。如果我们想定制此窗口大小,更改config_defaultPictureInPictureBounds即可。
@ActivityManagerService.java
public void enterPictureInPictureMode(IBinder token) {
final long origId = Binder.clearCallingIdentity();
try {
...
// Use the default launch bounds for pinned stack if it doesn't exist yet or use the
// current bounds.
final ActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);
final Rect bounds = (pinnedStack != null)
? pinnedStack.mBounds : mDefaultPinnedStackBounds;
mStackSupervisor.moveActivityToPinnedStackLocked(
r, "enterPictureInPictureMode", bounds);
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
核心代码
- mStackSupervisor.moveActivityToPinnedStackLocked(r, “enterPictureInPictureMode”, bounds);
多窗口的核心是分stack,以上方法的最后一句话会把当前Activity移动到系统为PIP分配的stack。接下来到moveActivityToPinnedStackLocked里面,默认情况下PinnedStack不存在,系统会创建这个stack,然后会根据当前Activity(正常窗口)所在的task的边界来设置PinnedStack的边界,注意此时还没有用到我们默认为PIP指定的bounds,当前activity的边界就是屏幕的可视区域,最终在WindowManagerService.java里面我们会把当前的task添加到PIP模式所在的Stack里面。
@ActivityStackSupervisor
void moveActivityToPinnedStackLocked(ActivityRecord r, String reason, Rect bounds) {
mWindowManager.deferSurfaceLayout();
try {
final TaskRecord task = r.task;
if (r == task.stack.getVisibleBehindActivity()) {
// An activity can't be pinned and visible behind at the same time. Go ahead and
// release it from been visible behind before pinning.
requestVisibleBehindLocked(r, false);
}
// Need to make sure the pinned stack exist so we can resize it below...
final ActivityStack stack = getStack(PINNED_STACK_ID, CREATE_IF_NEEDED, ON_TOP);
// Resize the pinned stack to match the current size of the task the activity we are
// going to be moving is currently contained in. We do this to have the right starting
// animation bounds for the pinned stack to the desired bounds the caller wants.
resizeStackLocked(PINNED_STACK_ID, task.mBounds, null /* tempTaskBounds */,
null /* tempTaskInsetBounds */, !PRESERVE_WINDOWS,
true /* allowResizeInDockedMode */, !DEFER_RESUME);
if (task.mActivities.size() == 1) {
// There is only one activity in the task. So, we can just move the task over to
// the stack without re-parenting the activity in a different task.
if (task.getTaskToReturnTo() == HOME_ACTIVITY_TYPE) {
// Move the home stack forward if the task we just moved to the pinned stack
// was launched from home so home should be visible behind it.
moveHomeStackToFront(reason);
}
moveTaskToStackLocked(
task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, reason, !ANIMATE);
} else {
stack.moveActivityToStack(r);
}
} finally {
mWindowManager.continueSurfaceLayout();
}
// The task might have already been running and its visibility needs to be synchronized
// with the visibility of the stack / windows.
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
resumeFocusedStackTopActivityLocked();
mWindowManager.animateResizePinnedStack(bounds, -1);
mService.notifyActivityPinnedLocked();
}
核心方法
- moveTaskToStackLocked(task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, reason, !ANIMATE);
- mWindowManager.animateResizePinnedStack(bounds, -1);
至此分栈的过程就完成了。
PIP模式设置栈边界
接下来我们分析一下设置栈边界的过程。
接着分栈的分析,最后会调用WindowManager的animateResizePinnedStack(bounds, -1)方法,根据当前Stack的大小和指定的PIP窗口的边界,通过动画慢慢更改当前窗口的大小,直到最后显示画中画模式的窗口。
@BoundsAnimationController.java
public void animateResizePinnedStack(final Rect bounds, final int animationDuration) {
synchronized (mWindowMap) {
...
UiThread.getHandler().post(new Runnable() {
@Override
public void run() {
mBoundsAnimationController.animateBounds(
stack, originalBounds, bounds, animationDuration);
}
});
}
}
mBoundsAnimationController.animateBounds
的from
和to
参数,分别表示在全屏stack id下的栈边界和指定的PIP模式的栈边界。
void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration) {
...
final BoundsAnimator animator =
new BoundsAnimator(target, from, to, moveToFullscreen, replacing);
mRunningAnimations.put(target, animator);
animator.setFloatValues(0f, 1f);
animator.setDuration((animationDuration != -1 ? animationDuration
: DEFAULT_APP_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
animator.setInterpolator(new LinearInterpolator());
animator.start();
}
在动画的执行过程中,不断的去更改当前stack的大小。
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// ...
if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
// ...
}
}
省略掉中间的一些步骤。直接到ActivityStackSupervisor.java的resizeStackUncheckedLocked。由于我们的Stack将要发生变化,所以会更新当前stack里面的所有task的相关配置。且会通知应用当前的多窗口状态发生了变化,此时会更新Task对应的最小宽度和最小高度等config信息。
@ActivityStackSupervisor.java
void resizeStackUncheckedLocked(ActivityStack stack, Rect bounds, Rect tempTaskBounds,
Rect tempTaskInsetBounds) {
bounds = TaskRecord.validateBounds(bounds);
if (!stack.updateBoundsAllowed(bounds, tempTaskBounds, tempTaskInsetBounds)) {
return;
}
mTmpBounds.clear();
mTmpConfigs.clear();
mTmpInsetBounds.clear();
final ArrayList<TaskRecord> tasks = stack.getAllTasks();
final Rect taskBounds = tempTaskBounds != null ? tempTaskBounds : bounds;
final Rect insetBounds = tempTaskInsetBounds != null ? tempTaskInsetBounds : taskBounds;
for (int i = tasks.size() - 1; i >= 0; i--) {
final TaskRecord task = tasks.get(i);
if (task.isResizeable()) {
if (stack.mStackId == FREEFORM_WORKSPACE_STACK_ID) {
// For freeform stack we don't adjust the size of the tasks to match that