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
private boolean startPositioningLocked(
WindowState win, boolean resize, float startX, float startY) {
// ...
Display display = displayContent.getDisplay();
mTaskPositioner = new TaskPositioner(this);
mTaskPositioner.register(display);
mInputMonitor.updateInputWindowsLw(true /*force*/);
// ...
mTaskPositioner.startDragLocked(win, resize, startX, startY);
}
流程图如下:
经过以上步骤,当我们继续滑动手指的时候,此时的响应事件会转移到WindowPositionerEventReceiver的onInputEvent方法。
在notifyMoveLocked方法里面,根据当前缩放模式以及初始化的位置和当前位置确认缩放窗口的大小。mCtrlType控制当前的缩放模式。CTRl_LEFT表示手指处于屏幕左边,其他类似。
@TaskPositioner.java
private boolean notifyMoveLocked(float x, float y) {
if (DEBUG_TASK_POSITIONING) {
Slog.d(TAG, "notifyMoveLocked: {" + x + "," + y + "}");
}
if (mCtrlType != CTRL_NONE) {
// This is a resizing operation.
final int deltaX = Math.round(x - mStartDragX);
final int deltaY = Math.round(y - mStartDragY);
int left = mWindowOriginalBounds.left;
int top = mWindowOriginalBounds.top;
int right = mWindowOriginalBounds.right;
int bottom = mWindowOriginalBounds.bottom;
if ((mCtrlType & CTRL_LEFT) != 0) {
left = Math.min(left + deltaX, right - mMinVisibleWidth);
}
if ((mCtrlType & CTRL_TOP) != 0) {
top = Math.min(top + deltaY, bottom - mMinVisibleHeight);
}
if ((mCtrlType & CTRL_RIGHT) != 0) {
right = Math.max(left + mMinVisibleWidth, right + deltaX);
}
if ((mCtrlType & CTRL_BOTTOM) != 0) {
bottom = Math.max(top + mMinVisibleHeight, bottom + deltaY);
}
mWindowDragBounds.set(left, top, right, bottom);
mTask.setDragResizing(true, DRAG_RESIZE_MODE_FREEFORM);
return false;
}
// ...
updateWindowDragBounds(nX, nY);
updateDimLayerVisibility(nX);
return dragEnded;
}
mWindowDragBounds 控制当前的Task的边界。
接下来就会不断的根据mWindowDragBounds的大小来resize我们的Task。
@TaskPointer.java
@Override
public void onInputEvent(InputEvent event) {
// ...
case MotionEvent.ACTION_MOVE: {
if (DEBUG_TASK_POSITIONING){
Slog.w(TAG, "ACTION_MOVE @ {" + newX + ", " + newY + "}");
}
synchronized (mService.mWindowMap) {
mDragEnded = notifyMoveLocked(newX, newY);
mTask.getDimBounds(mTmpRect);
}
// ...
try {
mService.mActivityManager.resizeTask(
mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER);
} catch (RemoteException e) {
}
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
}
} break;
// ...
}
mActivityManager.resizeTask的步骤和前面PIP以及分屏里面讲解的类似,不再阐述。
拖动窗口
窗体拖动注册全双工输入管道的方式和窗口缩放是一样的。唯一的区别是拖动的时候我们的手指是窗口最顶端的装饰标题视图之上。 对应DecorCaptionView.java,当我们点击移动的时候,执行的是:
@DecorCaptionView.java
@Override
public boolean onTouch(View v, MotionEvent e) {
// ...
case MotionEvent.ACTION_MOVE:
if (!mDragging && mCheckForDragging && passedSlop(x, y)) {
mCheckForDragging = false;
mDragging = true;
mLeftMouseButtonReleased = false;
startMovingTask(e.getRawX(), e.getRawY());
接下来到WMS里面的startPositonLocked,步骤和窗口缩放就一致了。
@WMS.java
boolean startMovingTask(IWindow window, float startX, float startY) {
WindowState win = null;
synchronized (mWindowMap) {
// ...
if (!startPositioningLocked(win, false /*resize*/, startX, startY)) {
return false;
}
}
// ...
}
总结
虽然说三种多窗口的表现形式不一致,但是原理大致类似。
多窗口是在不同的窗口显示不同Stack ID的Task。把不同的Task根据Stack ID来进行分类,分类主要是为了方便设置窗口的大小。
Stack的大小决定了Stack 里面的Task 的大小,最终决定了Task里面的Activity对应的窗口的大小。
标签:task,java,int,多窗口,bounds,final,Android7.0,原理,stack 来源: https://blog.csdn.net/Jason_Lee155/article/details/117265155