Android7.0多窗口实现原理
Posted Jason_Lee155
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android7.0多窗口实现原理相关的知识,希望对你有一定的参考价值。
本文基于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.java
frameworks/base/core/java/com/android/internal/policy/DividerSnapAlgorithm.java
frameworks/base/packages/SystemUI/src/com/android/systemui/stackdivider/
画中画模式
画中画模式(PIP)是最简单的多窗口模式,进入Android画中画模式的Activity会在当前屏幕上显示一个小的窗口,如图所示。
进入画中画模式很简单,直接在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
// of the stack, but we do try to make sure the tasks are still contained
// with the bounds of the stack.
tempRect2.set(task.mBounds);
fitWithinBounds(tempRect2, bounds);
task.updateOverrideConfiguration(tempRect2);
} else {
task.updateOverrideConfiguration(taskBounds, insetBounds);
}
}
mTmpConfigs.put(task.taskId, task.mOverrideConfig);
mTmpBounds.put(task.taskId, task.mBounds);
if (tempTaskInsetBounds != null) {
mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
}
}
// We might trigger a configuration change. Save the current task bounds for freezing.
mWindowManager.prepareFreezingTaskBounds(stack.mStackId);
stack.mFullscreen = mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,
mTmpBounds, mTmpInsetBounds);
stack.setBounds(bounds);
}
task.updateOverrideConfiguration
mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,mTmpBounds, mTmpInsetBounds);
接下来我们会进入到设置Stack大小变化的最后一步。设置当前Stack的大小。
@WindowManagerService.java
public boolean resizeStack(int stackId, Rect bounds,
SparseArray<Configuration> configs, SparseArray<Rect> taskBounds,
SparseArray<Rect> taskTempInsetBounds) {
synchronized (mWindowMap) {
final TaskStack stack = mStackIdToStack.get(stackId);
if (stack == null) {
throw new IllegalArgumentException("resizeStack: stackId " + stackId
+ " not found.");
}
if (stack.setBounds(bounds, configs, taskBounds, taskTempInsetBounds)
&& stack.isVisibleLocked()) {
stack.getDisplayContent().layoutNeeded = true;
mWindowPlacerLocked.performSurfacePlacement();
}
return stack.getRawFullscreen();
}
}
可以看到当前设置的是TaskStack的边界。
@TaskStack.java
boolean setBounds(
Rect stackBounds, SparseArray<Configuration> configs, SparseArray<Rect> taskBounds,
SparseArray<Rect> taskTempInsetBounds) {
setBounds(stackBounds);
// Update bounds of containing tasks.
for (int taskNdx = mTasks.size() - 1; taskNdx >= 0; --taskNdx) {
final Task task = mTasks.get(taskNdx);
Configuration config = configs.get(task.mTaskId);
if (config != null) {
Rect bounds = taskBounds.get(task.mTaskId);
if (task.isTwoFingerScrollMode()) {
// This is a non-resizeable task that's docked (or side-by-side to the docked
// stack). It might have been scrolled previously, and after the stack resizing,
// it might no longer fully cover the stack area.
// Save the old bounds and re-apply the scroll. This adjusts the bounds to
// fit the new stack bounds.
task.resizeLocked(bounds, config, false /* forced */);
task.getBounds(mTmpRect);
task.scrollLocked(mTmpRect);
} else {
task.resizeLocked(bounds, config, false /* forced */);
task.setTempInsetBounds(
taskTempInsetBounds != null ? taskTempInsetBounds.get(task.mTaskId)
: null);
}
} else {
Slog.wtf(TAG_WM, "No config for task: " + task + ", is there a mismatch with AM?");
}
}
return true;
}
在setBounds里面会更新当前TaskStack的bounds,接下来会更新TaskStack里面所有的Task的边界。
在task.resizeLocked里面,会最终设置Task的mBounds变量。也就是我们本文介绍的Task边界。至此,Task的边界bounds已经设置完毕。
显示在窗口顶端
系统所有的Window在屏幕显示的层级是按照Z轴进行排序的,当窗口发生改变的时候,系统会不断的调整Window在整个Window队列里面的层级,除了通过assignLayersLocked对正常窗口的层级进行调整之外。针对多窗口的特殊窗口,WindowLayersController.java还会进行特殊调整。位于adjustSpecialWindows,其中一个回调流程如下:
在adjustSpecialWindows里面,会对分屏模式下DockedWindow以及DockDivider的顺序调整。还有我们正在登陆的窗口mReplaceingWindows的顺序调整。把PIP模式对应的mPinnedWindows放到最后进行调整,这样对应的layer值就最大,那么从Z轴方向来看离屏幕最近。故就会显示到屏幕最顶端。
private void adjustSpecialWindows() {
// 省略
// 将PIP模式的窗口Layer设置到最顶层
while (!mPinnedWindows.isEmpty()) {
layer = assignAndIncreaseLayerIfNeeded(mPinnedWindows.remove(), layer);
}
}
分屏模式
首先直接给出两张图示。
如图,对于分屏模式而言,当长按预览键的时候,屏幕会分成上下两个窗口,不同的窗口对应不同的Stack,上面的窗口此时对应的是Docked Stack,底部窗口是Home Stack(处于多任务界面)或者FullStack(正常界面)。如图所示,手机被分成上下两块区域,也就是两个Stack,每个stack里面会包含很多的Task,而每个Task里面又包含了多个Activity,最终系统通过控制每个Stack的大小,来控制每个Task的大小,然后控制了Task里面的Activity的窗口的大小,所以最终控制了用户肉眼看到的每个小屏幕窗口大小。
接下来我们就开始分析整个分屏的流程。
创建Divider
如图4-1所示,中间黑色的分割线是DividerView,系统在开机过程中,会将这个DividerView通过WindowManager添加到系统的窗口中,默认影藏。以下是代码逻辑。
@Divider.java
private void addDivider(Configuration configuration) {
mView = (DividerView)
LayoutInflater.from(mContext).inflate(R.layout.docked_stack_divider, null);
mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
final int size = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.docked_stack_divider_thickness);
final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE;
final int width = landscape ? size : MATCH_PARENT;
final int height = landscape ? MATCH_PARENT : size;
mWindowManager.add(mView, width, height);
mView.injectDependencies(mWindowManager, mDividerState);
}
中间的白色小点是DividerHandleView.
@docked_stack_divider.xml
<com.android.systemui.stackdivider.DividerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent">
<View
style="@style/DockedDividerBackground"
android:id="@+id/docked_divider_background"
android:background="@color/docked_divider_background"/>
<com.android.systemui.stackdivider.MinimizedDockShadow
style="@style/DockedDividerMinimizedShadow"
android:id="@+id/minimized_dock_shadow"
android:alpha="0"/>">
<com.android.systemui.stackdivider.DividerHandleView
style="@style/DockedDividerHandle"
android:id="@+id/docked_divider_handle"
android:contentDescription="@string/accessibility_divider"
android:background="@null"/>
</com.android.systemui.stackdivider.DividerView>
代码里面具体的细节此处不做详细介绍。
初始化SnapTarget
为了确定上下两个stack的大小,设计了SnapTarget的概念,每个SnapTarget相当于一块区域,中间的分割线可以停留在每个区域的底部。在开机过程中,系统会根据屏幕的分辨率来创建不同个数的SnapTarget.
计算SnapTarget位置的代码如下:
@DividerSnapAlgorithm.java
private void calculateTargets(boolean isHorizontalDivision) {
mTargets.clear();
int dividerMax = isHorizontalDivision
? mDisplayHeight
: mDisplayWidth;
mTargets.add(new SnapTarget(-mDividerSize, -mDividerSize, SnapTarget.FLAG_DISMISS_START,
0.35f));
switch (mSnapMode) {
case SNAP_MODE_16_9:
addRatio16_9Targets(isHorizontalDivision, dividerMax);
break;
case SNAP_FIXED_RATIO:
addFixedDivisionTargets(isHorizontalDivision, dividerMax);
break;
case SNAP_ONLY_1_1:
addMiddleTarget(isHorizontalDivision);
break;
}
int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
SnapTarget.FLAG_DISMISS_END, 0.35f));
}
简单介绍一下SnapTarget,看构造方法。
public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
this.position = position;
this.taskPosition = taskPosition;
this.flag = flag;
this.distanceMultiplier = distanceMultiplier;
}
- position:离屏幕顶部的位置,最终决定了分割线停住的位置。
- taskPostion: 和postion差不多,主要是用来计算每个Task边界的位置。
- flag: 控制滑动到某个位置的时候是否退出分屏模式,比如我们将分割线滑动到靠近屏幕底部或者屏幕顶部的时候,会退出分屏。
- distanceMultiplier:退出分屏模式的距离因子,值越大表示越不容易退出。假设总共高度是1000px,我们需要滑到900px的地方则退出分屏模式。如果distanceMultiplier是1,相当于没有起作用,还是900px退出。如果是0.5,那么1000-900/0.5=200,相当于我们滑动800的地方就会退出了。
SnapTarget的作用主要是用来确认上下小屏的大小以及中间分割线的位置。后面还会继续介绍。
如果屏幕高度足够,上下小屏则可以调整大小。那么会创建5个SnapTargets,相当于是在手机屏幕上下方向确定了5个位置。中间的分割线根据这五个位置来确认自己的位置。举例说明:假设屏幕的分辨率是1440x2560,density是560。那么Android定义的状态栏的高度是84,导航栏的高度是168,默认小屏是按照16:9来分配大小。分割线本身的大小是 34,以上单位都是px。
那么上半部分的高度是(1440-0)X 9/16 = 810。 topPosition是810+84 = 894; bottomPostion是:2560 - 810 - 34. = 1548.
在Android系统中,默认配置小屏的大小是220dp。
<dimenname="default_minimal_size_resizable_task">220dp</dimen>
mMinimalSizeResizableTask = res.getDimensionPixelSize(
com.android.internal.R.dimen.default_minimal_size_resizable_task);
mMinimaSizeResizableTask = 220dp,560/160 * 220 = 770px
如果小屏的高度大于770px,才会添加5个SnapTarget,否则就只添加3个,由于我们上半屏的高度是810px,所以就会添加5个SnapTarget。当添加3个Target的时候,中间的分割线相当于只能停留在中间Target的postion处。也就是不能改变上下小屏幕的大小。
我们列出5个Target的postion和TaskPostion,单位是px,分别如下:
- mDismissStartTarget : -34px,分割线滑到此处,会退出分屏模式。实际上由于distanceMultiplier 的作用,分割线不需要滑动到这个位置则会退出。
- mFirstSplitTarget: 894 894
- mMiddleTarget: 1221 1221 分割线默认位置
- mLastSplitTarget:1548 1548
- mDismissEndTarget : 2392 2560
黑色的分割线DividerView默认位于1221处,可以停在894px,1221px和1548px的位置,也就是可以调整上下小屏的大小。当我们滑动中间分割线的时候,分割线会停到离滑动位置最近的postion,比如手指现在滑动的位置是900,那么分割线就会位于894px的地方。
接下来我们分析是如何分栈和设置栈边界
首先附上一张流程图,分为三个颜色,分别表示分栈,设置上半部分的栈边界,设置下半部分的栈边界。
分屏模式分栈
step1. 当我们长按多任务按键之后,系统判断当前是否支持分屏模式,如果手机屏幕过小或者没有配置支持分屏,那么直接返回,否则触发分屏模式。
@PhoneStatusBar.java
private View.OnLongClickListener mRecentsLongClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (mRecents == null || !ActivityManager.supportsMultiWindow()
|| !getComponent(Divider.class).getView().getSnapAlgorithm()
.isSplitScreenFeasible()) {
return false;
}
toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
return true;
}
};
step2-3. 其他代码细节此处不做详表,在RecentsImpl里面,会涉及到我们上面提到的两步核心。分栈和设置栈边界。分栈是通过moveTaskToDockedStack来实现。将当前的Task移动到对应的Docked Stack里面。设置栈边界是通过EventBus.getDefault().send(new DockedTopTaskEvent
(dragMode, initialBounds)来实现。
@RecentsImpl.java
public void dockTopTask(int topTaskId, int dragMode,
int stackCreateMode, Rect initialBounds) {
SystemServicesProxy ssp = Recents.getSystemServices();
// Make sure we inform DividerView before we actually start the activity so we can change
// the resize mode already.
if (ssp.moveTaskToDockedStack(topTaskId, stackCreateMode, initialBounds)) {
EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds));
showRecents(
false /* triggeredFromAltTab */,
dragMode == NavigationBarGestureHelper.DRAG_MODE_RECENTS,
false /* animate */,
true /* launchedWhileDockingTask*/,
false /* fromHome */,
DividerView.INVALID_RECENTS_GROW_TARGET);
}
}
Step4-9 .通过SystemServiceProxy代理,直接调用到ActivityManagerService.java的moveTaskToDockedStack,首先,系统会调用mWindowManager.setDockedStackCreateState,为了方便后面在WindowManager里面计算stack的大小。接下来调用moveTaskToStackLocked将当前的Task移动到Docked Stack里面。
@Override
public boolean moveTaskToDockedStack(int taskId, int createMode, boolean toTop, boolean animate,
Rect initialBounds, boolean moveHomeStackFront) {
enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "moveTaskToDockedStack()");
synchronized (this) {
long ident = Binder.clearCallingIdentity();
try {
if (DEBUG_STACK) Slog.d(TAG_STACK, "moveTaskToDockedStack: moving task=" + taskId
+ " to createMode=" + createMode + " toTop=" + toTop);
mWindowManager.setDockedStackCreateState(createMode, initialBounds);
final boolean moved = mStackSupervisor.moveTaskToStackLocked(
taskId, DOCKED_STACK_ID, toTop, !FORCE_FOCUS, "moveTaskToDockedStack",
animate, DEFER_RESUME);
if (moved) {
if (moveHomeStackFront) {
mStackSupervisor.moveHomeStackToFront("moveTaskToDockedStack");
}
mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
}
return moved;
} finally {
Binder.restoreCallingIdentity(ident);
}
}
}
mStackSupervisor.moveTaskToStackLocked
在ActivityStackSupervisor.java里面会调用WindowManager的moveTaskToStack方法,然后通过TaskStack的addTask方法,最终将当前的Task添加进mStack变量里面。在ActivityStackSupervisor.java里面,如红线所示,如果当前的task在移动之前有焦点。那么就会将当前的task移动到栈的最前面,而且会重新更新所有的windows,这样当系统在后面重新请求绘制Window的时候,Window在Z轴上的位置是正确的。
@ActivityStackSupervisor.java
ActivityStack moveTaskToStackUncheckedLocked(
TaskRecord task, int stackId, boolean toTop, boolean forceFocus, String reason) {
// omitted code
final ActivityStack stack = getStack(stackId, CREATE_IF_NEEDED, toTop);
task.mTemporarilyUnresizable = false;
mWindowManager.moveTaskToStack(task.taskId, stack.mStackId, toTop);
stack.addTask(task, toTop, reason);
// If the task had focus before (or we're requested to move focus),
// move focus to the new stack by moving the stack to the front.
stack.moveToFrontAndResumeStateIfNeeded(
r, forceFocus || wasFocused || wasFront, wasResumed, reason);
return stack;
}
分栈的整个流程我们就介绍到这里了。具体的细节不妨碍我们了解整个架构,不在详细描述。接下来我们看是如何设置栈边界的。
分屏模式设置栈边界
step11-13
接下来我们看EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds)方法。EventBus是SystemUI里面用来发送消息的一个机制,通过反射来实现相关功能。使用方法大致如下:
- 首先在需要订阅的类里面通过EventBus.getDefault().register(this)注册监听,表示订阅了某一种详细。
- 然后定制public final void onBusEvent()方法。
- 最后调用send或者post方法,将消息发布出去,所有订阅了此消息的类都会收到此消息。
在DividerView里面,注册了EventBus事件。
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
EventBus.getDefault().register(this);
mSurfaceFlingerOffsetMs = calculateAppSurfaceFlingerVsyncOffsetMs();
}
当我们执行了send之后,由于参数是DockedTopTaskEvent,那么会执行参数是DockedTopTaskEvent的onBusEvent方法。
@DividerView
public final void onBusEvent(DockedTopTaskEvent event) {
if (event.dragMode == NavigationBarGestureHelper.DRAG_MODE_NONE) {
mState.growAfterRecentsDrawn = false;
mState.animateAfterRecentsDrawn = true;
startDragging(false /* animate */, false /* touching */);
}
updateDockSide();
int position = DockedDividerUtils.calculatePositionForBounds(event.initialRect,
mDockSide, mDividerSize);
mEntranceAnimationRunning = true;
// Insets might not have been fetched yet, so fetch manually if needed.
if (mStableInsets.isEmpty()) {
SystemServicesProxy.getInstance(mContext).getStableInsets(mStableInsets);
mSnapAlgorithm = null;
initializeSnapAlgorithm();
}
resizeStack(position, mSnapAlgorithm.getMiddleTarget().position,
mSnapAlgorithm.getMiddleTarget());
}
接下来根据事件的初始化边界position和middle target的位置来对栈进行resize操作。注意此时并没有完全确认上下屏的大小。
由于RecentsActivity.java也注册了订阅者。当我们调用了RecentsImpl.java里面的showRecents方法的时候,同时也会调用RecentsActivity.java里面的onBusEvent方法。
@RecentsActivity.java
public final void onBusEvent(final DockedTopTaskEvent event) {
mRecentsView.getViewTreeObserver().addOnPreDrawListener(mRecentsDrawnEventListener);
mRecentsView.invalidate();
}
当前View进行PreDraw的时候,就会调用回调onPreDraw()方法。
public boolean onPreDraw() {
mRecentsView.getViewTreeObserver().removeOnPreDrawListener(this);
// We post to make sure that this information is delivered after this traversals is
// finished.
mRecentsView.post(new Runnable() {
@Override
public void run() {
Recents.getSystemServices().endProlongedAnimations();
}
});
return true;
}
接下来会根据中间分割线的位置,以及最终分割停留的位置(middle snap target的位置),在stopDragging里面,不断的通过动画,最终将当前Task的边界的底部设置成middle snaptarget的postion.然后请求ActivityManagerService.java进行resize的动作。
@DividerView.java
public final void onBusEvent(RecentsDrawnEvent drawnEvent) {
if (mState.animateAfterRecentsDrawn) {
mState.animateAfterRecentsDrawn = false;
updateDockSide();
mHandler.post(() -> {
// Delay switching resizing mode because this might cause jank in recents animation
// that's longer than this animation.
stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(),
mLongPressEntraceAnimDuration, Interpolators.FAST_OUT_SLOW_IN,
200 /* endDelay */);
});
}
if (mState.growAfterRecentsDrawn) {
mState.growAfterRecentsDrawn = false;
updateDockSide();
EventBus.getDefault().send(new RecentsGrowingEvent());
stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(), 336,
Interpolators.FAST_OUT_SLOW_IN);
}
}
step 15.
ActivityManagerService.java里面会直接请求ActivityStackSupervisor.java 重新设置上下屏的边界。如下代码所示,1表示设置DockedStack的bounds,2表示获取下屏的Bounds,3表示根据第2步获取的bounds设置当前stack的bounds。
@ActivityStackSupervisor.java
void resizeDockedStackLocked(Rect dockedBounds, Rect tempDockedTaskBounds,
Rect tempDockedTaskInsetBounds, Rect tempOtherTaskBounds, Rect tempOtherTaskInsetBounds,
boolean preserveWindows, boolean deferResume) {
// 1...
resizeStackUncheckedLocked(stack, dockedBounds, tempDockedTaskBounds,
tempDockedTaskInsetBounds);
// 2 ...
mWindowManager.getStackDockedModeBounds(
HOME_STACK_ID, tempRect, true /* ignoreVisibility */);
for (int i = FIRST_STATIC_STACK_ID; i <= LAST_STATIC_STACK_ID; i++) {
if (StackId.isResizeableByDockedStack(i) && getStack(i) != null) {
// 3
resizeStackLocked(i, tempRect, tempOtherTaskBounds,
tempOtherTaskInsetBounds, preserveWindows,
true /* allowResizeInDockedMode */, deferResume);
}
}
}
//...
}
在ActivityStackSupervisor.java里面,由于上下小屏的最小宽度以及屏幕高度和宽度等发生了变化。故会对当前的Task的config进行重新配置.
void resizeStackUncheckedLocked(ActivityStack stack, Rect bounds, Rect tempTaskBounds,
Rect tempTaskInsetBounds) {
//...
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
// of the stack, but we do try to make sure the tasks are still contained
// with the bounds of the stack.
tempRect2.set(task.mBounds);
fitWithinBounds(tempRect2, bounds);
task.updateOverrideConfiguration(tempRect2);
} else {
task.updateOverrideConfiguration(taskBounds, insetBounds);
}
}
//...
stack.mFullscreen = mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,
mTmpBounds, mTmpInsetBounds);
stack.setBounds(bounds);
}
task.updateOverrideConfiguration
mWindowManager.resizeStack
step16-22.
在WindowManagerService.java里面设置当前Stack的bounds。接下来的步骤和画中画模式里面介绍的一样,故不再解释。
@WindowManagerService.java
public void resizeTask(int taskId, Rect bounds, Configuration configuration,
boolean relayout, boolean forced) {
synchronized (mWindowMap) {
Task task = mTaskIdToTask.get(taskId);
if (task == null) {
throw new IllegalArgumentException("resizeTask: taskId " + taskId
+ " not found.");
}
if (task.resizeLocked(bounds, configuration, forced) && relayout) {
task.getDisplayContent().layoutNeeded = true;
mWindowPlacerLocked.performSurfacePlacement();
}
}
}
至此,PIP模式的分栈和设置栈边界就分析完了。接下来我们分析自由模式。
自由模式
自由模式默认情况下是关闭的。如果需要打开,以下两种方式选择其一即可。
- adb shell settings put globalenable_freeform_supporttrue
- 给系统添加feature:android.software.freeform_window_management
如果打开了Freeform模式,当我们按了多任务按键之后,在每个任务View的标题栏上会多出一个方框,如图所示,当我们点击了方框之后,会进入FreeForm模式。如图,我们可以看到Settings窗口的顶部多出了一部分,并且右边还有两个操作按钮,表示放大当前窗口,相当于是“”全屏”,关掉当前的窗口。
SystemUI和ActivityManagerService里面有一个值控制是否支持freeform格式。
@ActivityManagerService.java
// ...
final boolean freeformWindowManagement =
mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
|| Settings.Global.getInt(
resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
final boolean supportsPictureInPicture =
mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
// ...
}
@SystemServiceProxy.java
private SystemServicesProxy(Context context) {
// ...
mHasFreeformWorkspaceSupport =
mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT) ||
Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
自由模式UI
Freeform模式对比PIP模式和分屏模式,除了窗口大小有区别之外,顶部还多出了几个可控图标。通过Android Layout Inspector查看,和正常的View对比,DecoreView下面多了一个DecorCaptionView,我们看到的按钮就是Maximize和Close。
DecorCaptionView的创建过程和正常setContentView类似,当我们点击顶部方框进入freeform模式,会创建当前界面的Activity,而且会创建DecorCaptionView。
在创建的过程中,我们会判断当前的Stack id是不是freeform_stack_id。如果不是,那么就不创建顶部标题栏,具体创建过程此处不再描述。
@DecorView.java
private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
// ...
if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) {
// Dependent on the brightness of the used title we either use the
// dark or the light button frame.
if (decorCaptionView == null) {
decorCaptionView = inflateDecorCaptionView(inflater);
}
decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
} else {
decorCaptionView = null;
}
// Tell the decor if it has a visible caption.
enableCaption(decorCaptionView != null);
return decorCaptionView;
}
我们还是按照分栈和设置栈边界来分析自由模式。
自由模式分栈
当我们在Recent界面点击了进入freedom模式的按钮之后,当前界面会进入自由模式。首先贴出时序图。
step1-3
可以看到,当我们点击按钮之后,还是基于EventBus机制,发送LauncherTaskEvent。Freeform模式下,Stack的初始化大小的bounds,可以在TaskViewHeader.java里面进行配置。代码如下:
@TaskViewHeader.java
@Override
public void onClick(View v) {
// ...
} else if (v == mMoveTaskButton) {
TaskView tv = Utilities.findParent(this, TaskView.class);
Rect bounds = mMoveTaskTargetStackId == FREEFORM_WORKSPACE_STACK_ID
? new Rect(mTaskViewRect)
: new Rect();
EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
mMoveTaskTargetStackId, false));
}
// ...
}
基于EventBus机制,接下来会调用RecentsView的onBusEvent方法。 然后调用launchTaskFromRecents开始启动Task。在freeForm模式中,有一个很重要的特性就是ActivityOptions,系统根据ActivityOptions来控制启动的Task的栈边界的大小。
@RecentsTransitionHelper.java
public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
final TaskStackView stackView, final TaskView taskView,
final boolean screenPinningRequested, final Rect bounds, final int destinationStack) {
final ActivityOptions opts = ActivityOptions.makeBasic();
if (bounds != null) {
opts.setLaunchBounds(bounds.isEmpty() ? null : bounds);
}
// ...
}
step4-10
省略掉其他的细节,直接进入ActivityManagerService的startActivityFromRecents方法。会获取在前面设置的stack id,在step 5中,如果当前Task的stack id是docked_stack_id,才会设置LauncherStackID,正常情况下是不会进行设置的,也就是说从DockedStack进入Freeform才会设置bounds。
@AMS.java
final int startActivityFromRecentsInner(int taskId, Bundle bOptions) {
// ...
final ActivityOptions activityOptions = (bOptions != null)
? new ActivityOptions(bOptions) : null;
final int launchStackId = (activityOptions != null)
? activityOptions.getLaunchStackId() : INVALID_STACK_ID;
// ...
}
所以在接下来的判断中launcherStackId等于INVALID_STACK_ID,也就不会走moveTask的操作。如下:
if (launchStackId != INVALID_STACK_ID) {
if (task.stack.mStackId != launchStackId) {
moveTaskToStackLocked(
taskId, launchStackId, ON_TOP, FORCE_FOCUS, "startActivityFromRecents",
ANIMATE);
}
}
那move操作时在哪儿进行的呢?我们接着往后看,会有个mService.moveTaskToFrontLocked方法。
// If the user must confirm credentials (e.g. when first launching a work app and the
// Work Challenge is present) let startActivityInPackage handle the intercepting.
if (!mService.mUserController.shouldConfirmCredentials(task.userId)
&& task.getRootActivity() != null) {
mService.mActivityStarter.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */);
mActivityMetricsLogger.notifyActivityLaunching();
mService.moveTaskToFrontLocked(task.taskId, 0, bOptions);
// ...
mService.mActivityStarter.postStartActivityUncheckedProcessing(task.getTopActivity(),
ActivityManager.START_TASK_TO_FRONT,
sourceRecord != null ? sourceRecord.task.stack.mStackId : INVALID_STACK_ID,
sourceRecord, task.stack);
return ActivityManager.START_TASK_TO_FRONT;
}
mService.moveTaskToFrontLocked
直接进入到ActivityStackSupervisor里面的moveTaskToFrontLocked方法,如果当前的Task支持多窗口、options不会null而且当前是PIP或者自由模式,会更新当前Task的bounds。由于当前的stackId是不合法,那么就会重新后去获取stackID。
@ActivityStackSupervisor.java
void findTaskToMoveToFrontLocked(TaskRecord task, int flags, ActivityOptions options,
String reason, boolean forceNonResizeable) {
//...
if (task.isResizeable() && options != null) {
int stackId = options.getLaunchStackId();
//1. 判断是否使用ActivityOptions
if (canUseActivityOptionsLaunchBounds(options, stackId)) {
final Rect bounds = TaskRecord.validateBounds(options.getLaunchBounds());
task.updateOverrideConfiguration(bounds);
if (stackId == INVALID_STACK_ID) {
stackId = task.getLaunchStackId();
}
if (stackId != task.stack.mStackId) {
// 2. 移动
final ActivityStack stack = moveTaskToStackUncheckedLocked(
task, stackId, ON_TOP, !FORCE_FOCUS, reason);
stackId = stack.mStackId;
// moveTaskToStackUncheckedLocked() should already placed the task on top,
// still need moveTaskToFrontLocked() below for any transition settings.
}
if (StackId.resizeStackWithLaunchBounds(stackId)) {
//3. resize栈
resizeStackLocked(stackId, bounds,
null /* tempTaskBounds */, null /* tempTaskInsetBounds */,
!PRESERVE_WINDOWS, true /* allowResizeInDockedMode */, !DEFER_RESUME);
} else {
// WM resizeTask must be done after the task is moved to the correct stack,
// because Task's setBounds() also updates dim layer's bounds, but that has
// dependency on the stack.
mWindowManager.resizeTask(task.taskId, task.mBounds, task.mOverrideConfig,
false /* relayout */, false /* forced */);
}
}
}
//...
}
根据getLaunchStackID获取当前的STACK_ID,在LaunchTaskFromRecentss里面我们设置了mBounds,所以最终返回FREEFORM_WORKSPACE_STACK_ID。
@ActivityRecord.java
int getLaunchStackId() {
if (!isApplicationTask()) {
return HOME_STACK_ID;
}
if (mBounds != null) {
return FREEFORM_WORKSPACE_STACK_ID;
}
return FULLSCREEN_WORKSPACE_STACK_ID;
}
后面调用moveTaskToStackUncheckedLocked移动Stack,调用resizeTask设置栈边界大小,步骤和PIP模式的类似,不在描述。
自由模式设置栈边界
在ActivityStackSupervisor.java里面进行相应的判断,如果stack支持分屏,且ActiviyOptions不为null,那么就会根据我们的stackid和bounds对当前的Stack进行resize的动作。rezise逻辑和PIP一致,此处不再描述。
缩放窗口
自由模式里面,窗口支持放大缩小以及移动位置。原理是不断的更改Task的边界(用Rect表示),然后根据Task的边界来重新缩放Task,从而达到窗口缩放和拖动的作用。对于拖动来说,边界的宽和高保持不变,变的是坐标的位置。缩放来说,坐标改变的同时,边界宽和高也发生了变化。
对于每一个窗口来说,如果我们的触摸事件要能够正常响应,我们必须注册全双工的输入管道。一个用来分发事件给对应的窗口,一个用来接受事件并进行处理。
首先我们分析缩放窗口的原理,在Android系统启动WMS之后,会在InputManagerService的monitorInput方法创建一个可以用来接受所有输入事件的输入管道。先上一张流程图。
客户端的输入管道会接受服务端发送过来的输入事件,系统所有的事件都会先分发到PointerEventDispatcher.java的onInputEvent方法。由于TaskTapPointerEventListener加入了监听,最终会执行TaskTapPointerEventListener的onPointerEvent方法。
@PointerEventDispatcher.java
@Override
public void onInputEvent(InputEvent event) {
try {
if (event instanceof MotionEvent
&& (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
final MotionEvent motionEvent = (MotionEvent)event;
PointerEventListener[] listeners;
synchronized (mListeners) {
if (mListenersArray == null) {
mListenersArray = new PointerEventListener[mListeners.size()];
mListeners.toArray(mListenersArray);
}
listeners = mListenersArray;
}
for (int i = 0; i < listeners.length; ++i) {
listeners[i].onPointerEvent(motionEvent);
}
}
} finally {
finishInputEvent(event, false);
}
}
接下来
@TaskTapPointerEventListener.java
@Override
public void onPointerEvent(MotionEvent motionEvent) {
doGestureDetection(motionEvent);
final int action = motionEvent.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final int x = (int) motionEvent.getX();
final int y = (int) motionEvent.getY();
synchronized (this) {
if (!mTouchExcludeRegion.contains(x, y)) {
mService.mH.obtainMessage(H.TAP_OUTSIDE_TASK,
x, y, mDisplayContent).sendToTarget();
}
}
break;
}
// ...
}
mTouchExcludeRegion: 表示当前获取焦点的栈的区域。
根据native层输入的event里面的x和y坐标可以判断我们当前点击的位置,如果点击在栈的外面,那么就会发送TAP_OUTSICE_TASK的消息。 然后会执行findTaskForControlPoint,请注意此时会判断我们点击的位置是不是在当前窗口周围RESIZE_HANDLE_WIDTH_IN_DP=30dp以内,如果是才会进行缩放操作。
@WindowManagerService.java
private void handleTapOutsideTask(DisplayContent displayContent, int x, int y) {
int taskId = -1;
synchronized (mWindowMap) {
final Task task = displayContent.findTaskForControlPoint(x, y);
if (task != null) {
if (!startPositioningLocked(
task.getTopVisibleAppMainWindow(), true /*resize*/, x, y)) {
return;
}
//...
}
Task findTaskForControlPoint(int x, int y) {
final int delta = mService.dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
TaskStack stack = mStacks.get(stackNdx);
if (!StackId.isTaskResizeAllowed(stack.mStackId)) {
break;
}
final ArrayList<Task> tasks = stack.getTasks();
for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
final Task task = tasks.get(taskNdx);
if (task.isFullscreen()) {
return null;
}
// We need to use the task's dim bounds (which is derived from the visible
// bounds of its apps windows) for any touch-related tests. Can't use
// the task's original bounds because it might be adjusted to fit the
// content frame. One example is when the task is put to top-left quadrant,
// the actual visible area would not start at (0,0) after it's adjusted
// for the status bar.
task.getDimBounds(mTmpRect);
//判断我们账号点击在栈边缘30dp的区域
mTmpRect.inset(-delta, -delta);
if (mTmpRect.contains(x, y)) {
mTmpRect.inset(delta, delta);
if (!mTmpRect.contains(x, y)) {
return task;
}
// User touched inside the task. No need to look further,
// focus transfer will be handled in ACTION_UP.
return null;
}
}
}
接下来会进入到WindowManagerService的startPositioningLocked方法,有如下两个动作。
- 注册新的专门用于处于移动或者缩放的全双工输入管道,用来处理在当前Task边界周围30px以内的触摸事件。
- 告诉TaskPointer,当前的缩放模式以及首次点击的坐标位置。
@WindowManagerService.java
以上是关于Android7.0多窗口实现原理的主要内容,如果未能解决你的问题,请参考以下文章 WmS详解之如何理解Window和窗口的关系?基于Android7.0源码