浅谈Android多屏幕的事

努力尽今夕,少年犹可夸。这篇文章主要讲述浅谈Android多屏幕的事相关的知识,希望能为你提供帮助。
浅谈android多屏幕的事 一部手机可以同时看片、聊天, 还可以腾出一支手来撸! 这么吊的功能(非N版本, 非第三方也能实现, 你不知道吧)摆在你面前, 你不享用?不关注它是怎样实现的?你来, 我就满足你的欲望!
一部手机可以同时看片、聊天, 还可以腾出一支手来撸= = ! 就像这样:

浅谈Android多屏幕的事

文章图片

是时候告别来回切换应用屏幕的酸爽了, 还可以在分屏模式下两Activity间直接拖放数据! 好高大上的样子! 这是怎么实现的? 别急, 我们一一道来:

kitkat(4.4)版本对多任务分屏的实现 由于相关的代码和功能被封装及隐藏起来, 所以我们从dumpsys activity命令作手。
来, 直接上一条命令:
am stack boxes
NetEasedeMac-Pro-8:~ netease$ adb shell am stack boxes Box id= 0 weight= 0.0 vertical= false bounds= [0,75][1080,1776] Stack= Stack id= 0 bounds= [0,75][1080,1776] taskId= 1: com.android.launcher/com.android.launcher2.Launcher taskId= 4: com.android.systemui/com.android.systemui.recent.RecentsActivityBox id= 1 weight= 0.0 vertical= false bounds= [0,75][1080,1776] Stack= Stack id= 1 bounds= [0,75][1080,1776] taskId= 2: com.android.contacts/com.android.contacts.activities.PeopleActivity taskId= 3: com.android.email/com.android.email.activity.Welcome

以上命令大致可以看出一些简单明显的东西, 最高级为两个Box, 每个Box有一个Stack, 每个Stack下有n个taskId等等。既然提到am, 那我们就去看一看Android源码Am这个类: com.android.commands.am.Am。在里面可以看到一堆的am命令, 这些命令的执行在onRun()函数里, 我们先看runStack()-> runStackBoxes(), 一直跟踪下去, 会涉及到ActivityManagerService.getStackBoxes()-> WindowManagerService.getStackBoxInfos()-> DisplayContent.getStackBoxInfos()直接遍历DisplayContent里的mStackBoxes。
用简单的类图来表示其数据结构关系:
StackBox相关简单类图:
浅谈Android多屏幕的事

文章图片

TaskStack相关的简单类图:
浅谈Android多屏幕的事

文章图片

有这里Task的概念是什么?
官方定义: “A task (from the activity that started it to the next task activity)defines an atomic group of activities that the user can move to.”。
简单来讲: Task是为了完成一个功能的一系列相关的有序Activity集合, 可以理解为用户与App之间对于特定功能的一次会话。一个Task中的Activity可以来自不同的App, 比如在邮件App中需要看图片附件, 然后会开imageview的Activity来显示它。
以上只是在WMS里分析了StackBox、TaskStack相关的简单数据结构, 我们知道对于一个完整的窗口流程还主要涉及到AMS、SF联合管理及渲染显示。那么在AMS里是怎样进行管理的呢?不急, 我们看第二个灵魂级的am命令
灵魂级的am命令: am stack create
am stack create < TASK_ID> < RELATIVE_STACK_BOX_ID> < POSITION> < WEIGHT>
注解如下:
am stack create: create a new stack relative to an existing one. < TASK_ID> : the task to populate the new stack with. Must exist. < RELATIVE_STACK_BOX_ID> : existing stack box' s id. < POSITION> : 0: before < RELATIVE_STACK_BOX_ID> 1: after < RELATIVE_STACK_BOX_ID> 2: to left of < RELATIVE_STACK_BOX_ID> 3: to right of < RELATIVE_STACK_BOX_ID> 4: above < RELATIVE_STACK_BOX_ID> 5: below < RELATIVE_STACK_BOX_ID> < WEIGHT> : float between 0.2 and 0.8 inclusive.\\n" +

试一下:
NetEasedeMac-Pro-8:~ netease$ adb shell am stack create 3 1 4 0.7 createStack returned new stackId= 2

直接上效果图:
浅谈Android多屏幕的事

文章图片

这里申明一下: 由于我的模拟器有问题, 上图是借用http://androidinternalsblog.blogspot.com/2014/03/split-screens-in-android-exist.html 里的, 不过效果一样的。
这就是android分屏的鼻祖, 这时候的stack boxes输出如下:
NetEasedeMac-Pro-8:~ netease$ adb shell am stack boxes Box id= 1 weight= 0.5 vertical= true bounds= [0,75][1080,1776] First child= Box id= 2 weight= 0.0 vertical= false bounds= [0,75][1080,925] Stack= Stack id= 2 bounds= [0,75][1080,925] taskId= 3: com.android.email/com.android.email.activity.Welcome Second child= Box id= 3 weight= 0.0 vertical= false bounds= [0,925][1080,1776] Stack= Stack id= 1 bounds= [0,925][1080,1776] taskId= 2: com.android.contacts/com.android.contacts.activities.PeopleActivityBox id= 0 weight= 0.0 vertical= false bounds= [0,75][1080,1776] Stack= Stack id= 0 bounds= [0,75][1080,1776] taskId= 1: com.android.launcher/com.android.launcher2.Launcher

可以看到之前的Box_id1一分为二: Box_id2和Box_id3( 充分说明StackBox的数据结构为树) , 另外stack_id3从原Box_id1( 也可以理解成现在的Box_id2) 的移到了Box_id3。
看看am stack create是怎么实现的吧, 回到Am里的runStackCreate()-> AMS.createStack()
public int createStack(int taskId, int relativeStackBoxId, int position, float weight) { enforceCallingPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS, " createStack()" ); if (DEBUG_STACK) Slog.d(TAG, " createStack: taskId= " + taskId + " relStackBoxId= " + relativeStackBoxId + " position= " + position + " weight= " + weight); synchronized (this) { long ident = Binder.clearCallingIdentity(); try { int stackId = mStackSupervisor.createStack(); mWindowManager.createStack(stackId, relativeStackBoxId, position, weight); if (taskId > 0) { moveTaskToStack(taskId, stackId, true); } return stackId; } finally { Binder.restoreCallingIdentity(ident); } } }

这里主要做了两件事, 一个是向mStackSupervisor申请一个stackId, 第二个是邮WMS去继续createStack, 首先来看看mStackSupervisor.createStack()。
ActivityStackSupervisor.createStack()
int createStack() { while (true) { if (+ + mLastStackId < = HOME_STACK_ID) { mLastStackId = HOME_STACK_ID + 1; } if (getStack(mLastStackId) = = null) { break; } } mStacks.add(new ActivityStack(mService, mContext, mLooper, mLastStackId)); return mLastStackId; }

进入ActivityStackSupervisor, 每申请一个stackId(对于AMS来说就是添加一个ActivityStack), 都会把新添加的ActivityStack add 到 mStacks; 除了createStack()之外, 还可以看到很多关于stack的操作, 不难发现ActivityStackSupervisor是stack的终级管理者, 这里先不展开, 后面再详细讨论它是怎样管理stack的, 这里继续探讨stack create的事。
WMS.createStack()
public void createStack(int stackId, int relativeStackBoxId, int position, float weight) { synchronized (mWindowMap) { if (position < = StackBox.TASK_STACK_GOES_BELOW & & (weight < STACK_WEIGHT_MIN || weight > STACK_WEIGHT_MAX)) { throw new IllegalArgumentException( " createStack: weight must be between " + STACK_WEIGHT_MIN + " and " + STACK_WEIGHT_MAX + " , weight= " + weight); } final int numDisplays = mDisplayContents.size(); for (int displayNdx = 0; displayNdx < numDisplays; + + displayNdx) { final DisplayContent displayContent = mDisplayContents.valueAt(displayNdx); TaskStack stack = displayContent.createStack(stackId, relativeStackBoxId, position, weight); if (stack != null) { mStackIdToStack.put(stackId, stack); performLayoutAndPlaceSurfacesLocked(); return; } } Slog.e(TAG, " createStack: Unable to find relativeStackBoxId= " + relativeStackBoxId); } }

在WMS.createStack()里可以看到两个重要的信息, 首先是遍历 SparseArray < DisplayContent> mDisplayContents,每一个DisplayContent再执行createStack(), 然后把(stackId, stack)存储到mStackIdToStack里( 从第一个命令里查stackBoxes的时候是从DisplayContent时获取的, 那在WMS里存储mStackIdToStack很明显是为了更方便的管理stack信息) 。
DisplayContent.createStack()
TaskStack createStack(int stackId, int relativeStackBoxId, int position, float weight) { TaskStack newStack = null; if (DEBUG_STACK) Slog.d(TAG, " createStack: stackId= " + stackId + " relativeStackBoxId= " + relativeStackBoxId + " position= " + position + " weight= " + weight); if (stackId = = HOME_STACK_ID) { if (mStackBoxes.size() != 1) { throw new IllegalArgumentException(" createStack: HOME_STACK_ID (0) not first." ); } newStack = mHomeStack; } else { int stackBoxNdx; for (stackBoxNdx = mStackBoxes.size() - 1; stackBoxNdx > = 0; --stackBoxNdx) { final StackBox box = mStackBoxes.get(stackBoxNdx); if (position = = StackBox.TASK_STACK_GOES_OVER || position = = StackBox.TASK_STACK_GOES_UNDER) { // Position indicates a new box is added at top level only. if (box.contains(relativeStackBoxId)) { StackBox newBox = new StackBox(mService, this, null); newStack = new TaskStack(mService, stackId, this); newStack.mStackBox = newBox; newBox.mStack = newStack; final int offset = position = = StackBox.TASK_STACK_GOES_OVER ? 1 : 0; if (DEBUG_STACK) Slog.d(TAG, " createStack: inserting stack at " + (stackBoxNdx + offset)); mStackBoxes.add(stackBoxNdx + offset, newBox); break; } } else { // Remaining position values indicate a box must be split. newStack = box.split(stackId, relativeStackBoxId, position, weight); if (newStack != null) { break; } } } if (stackBoxNdx < 0) { throw new IllegalArgumentException(" createStack: stackBoxId " + relativeStackBoxId + " not found." ); } } if (newStack != null) { layoutNeeded = true; } EventLog.writeEvent(EventLogTags.WM_STACK_CREATED, stackId, relativeStackBoxId, position, (int)(weight * 100 + 0.5)); return newStack; }

DisplayContent.createStack()方法代码这么长, 其最有价值的就是“灵魂事件—分屏”: box.split, box.split的第一层if判断是: 新增的stack是否在同一layer( 参数position如果是4或者5那么就不会执行下面的split) 。那我们来看看TaskStack split里做了些什么。
TaskStack split
/** * Create a new TaskStack relative to a specified one by splitting the StackBox containing * the specified TaskStack into two children. The size and position each of the new StackBoxes * is determined by the passed parameters. * @ param stackId The id of the new TaskStack to create. * @ param relativeStackBoxId The id of the StackBox to place the new TaskStack next to. * @ param position One of the static TASK_STACK_GOES_xxx positions defined in this class. * @ param weight The percentage size of the parent StackBox to devote to the new TaskStack. * @ return The new TaskStack. */ TaskStack split(int stackId, int relativeStackBoxId, int position, float weight) { if (mStackBoxId != relativeStackBoxId) { // This is not the targeted StackBox. if (mStack != null) { return null; } // Propagate the split to see if the targeted StackBox is in either sub box. TaskStack stack = mFirst.split(stackId, relativeStackBoxId, position, weight); if (stack != null) { return stack; } return mSecond.split(stackId, relativeStackBoxId, position, weight); }// Found it! TaskStack stack = new TaskStack(mService, stackId, mDisplayContent); TaskStack firstStack; TaskStack secondStack; if (position = = TASK_STACK_GOES_BEFORE) { // TODO: Test Configuration here for LTR/RTL. position = TASK_STACK_TO_LEFT_OF; } else if (position = = TASK_STACK_GOES_AFTER) { // TODO: Test Configuration here for LTR/RTL. position = TASK_STACK_TO_RIGHT_OF; } switch (position) { default: case TASK_STACK_TO_LEFT_OF: case TASK_STACK_TO_RIGHT_OF: mVertical = false; if (position = = TASK_STACK_TO_LEFT_OF) { mWeight = weight; firstStack = stack; secondStack = mStack; } else { mWeight = 1.0f - weight; firstStack = mStack; secondStack = stack; } break; case TASK_STACK_GOES_ABOVE: case TASK_STACK_GOES_BELOW: mVertical = true; if (position = = TASK_STACK_GOES_ABOVE) { mWeight = weight; firstStack = stack; secondStack = mStack; } else { mWeight = 1.0f - weight; firstStack = mStack; secondStack = stack; } break; }mFirst = new StackBox(mService, mDisplayContent, this); firstStack.mStackBox = mFirst; mFirst.mStack = firstStack; mSecond = new StackBox(mService, mDisplayContent, this); secondStack.mStackBox = mSecond; mSecond.mStack = secondStack; mStack = null; return stack; }

TaskStack split在维护stackBox树结构的同时实现了分屏, 我还是少说话, 大家多多品味吧。
到此, 对于stack create已说完, 回应前面提到的ActivityStackSupervisor, 这里就来谈谈它是怎样管理stack的, 首先看看里面引入了ActivityStack的两个重要成员:
/** The stack containing the launcher app */ private ActivityStack mHomeStack; /** All the non-launcher stacks */ private ArrayList< ActivityStack> mStacks = new ArrayList< ActivityStack> ();

同样, 用简单的类关系来说明stack在AMS里的层次结构:
浅谈Android多屏幕的事

文章图片

与前面WMS里的stack结构相比较:
浅谈Android多屏幕的事

文章图片

两相比较, 其结构一样, 但是AMS与WMS两者的职能不一样, 前者主要管理Activity, Service和Process等信息, WMS管理应用和系统窗口, 维护窗口的布局, z _order的管理, 发生变化时通知app作出调整, 同时为SF提供layery计算等等, 因此两个成为了不同的模块, 固然在stack _task的管理上有了相同的结构不同的“view”。
继续看看分屏相关其它的am命令
am stack move task
  • 语法: am stack movetask < TASK_ID> < STACK_ID> [true|false]
  • 注解: move < TASK_ID> from its current stack to the top (true) or bottom (false) of < STACK_ID> .
    由参数及注解可以看出, 该命令不是单一的删除一个task, 而是把一个task remove to a pointed stack。
  • 实现:
    Am.runStackMoveTask()-> Ams.moveTaskToStack(int taskId, int stackId, boolean toTop)-> ActivityStackSupervisor.moveTaskToStack(int taskId, int stackId, boolean toTop)
ActivityStackSupervisor.moveTaskToStack
void moveTaskToStack(int taskId, int stackId, boolean toTop) { final TaskRecord task = anyTaskForIdLocked(taskId); if (task = = null) { return; } final ActivityStack stack = getStack(stackId); if (stack = = null) { Slog.w(TAG, " moveTaskToStack: no stack for id= " + stackId); return; } removeTask(task); stack.addTask(task, toTop); mWindowManager.addTask(taskId, stackId, toTop); resumeTopActivitiesLocked(); }

void removeTask(TaskRecord task) { mWindowManager.removeTask(task.taskId); final ActivityStack stack = task.stack; final ActivityRecord r = stack.mResumedActivity; if (r != null & & r.task = = task) { stack.mResumedActivity = null; } if (stack.removeTask(task) & & !stack.isHomeStack()) { if (DEBUG_STACK) Slog.i(TAG, " removeTask: removing stack " + stack); mStacks.remove(stack); final int stackId = stack.mStackId; final int nextStackId = mWindowManager.removeStack(stackId); // TODO: Perhaps we need to let the ActivityManager determine the next focus... if (mFocusedStack = = null || mFocusedStack.mStackId = = stackId) { // If this is the last app stack, set mFocusedStack to null. mFocusedStack = nextStackId = = HOME_STACK_ID ? null : getStack(nextStackId); } } }

ActivityStackSupervisor里先从历史数据中remove掉task, 再把task加到目标stack中, 不管是remove还是add在stackSupervisor里进行相关操作的时候, 都会通知WMS作相应的处理。不过这里有一个细节, 就是如果一个非homeStack删除掉最后一个task的时候, 该stack也会被干掉, 同样除了在ActivityStackSupervisor里的mStacks里remove外, 也会通知WMS remove掉该stack, 在WMS里的remove操作其实就是对stack数据的destory, 在这里不再展示。
WMS的removeTask和addTask, 同样遵守“先下后上”原则:
public void removeTask(int taskId) { synchronized (mWindowMap) { Task task = mTaskIdToTask.get(taskId); if (task = = null) { if (DEBUG_STACK) Slog.i(TAG, " removeTask: could not find taskId= " + taskId); return; } final TaskStack stack = task.mStack; EventLog.writeEvent(EventLogTags.WM_TASK_REMOVED, taskId, " removeTask" ); stack.removeTask(task); stack.getDisplayContent().layoutNeeded = true; } }

public void addTask(int taskId, int stackId, boolean toTop) { synchronized (mWindowMap) { Task task = mTaskIdToTask.get(taskId); if (task = = null) { return; } TaskStack stack = mStackIdToStack.get(stackId); stack.addTask(task, toTop); final DisplayContent displayContent = stack.getDisplayContent(); displayContent.layoutNeeded = true; performLayoutAndPlaceSurfacesLocked(); } }

在WMS里taks迁移成功后, 调用performLayoutAndPlaceSurfacesLocked()进行窗口绘。
am stack resize
  • 语法 : am stack resize < STACK_ID> < WEIGHT>
  • 注解: am stack resize: change < STACK_ID> relative size to new < WEIGHT>
  • 实现: AM.runStackBoxResize()-> AMS.resizeStackBox(int stackBoxId, float weight)-> WMS.resizeStackBox(int stackBoxId, float weight)
WMS.resizeStackBox
public void resizeStackBox(int stackBoxId, float weight) { if (weight < STACK_WEIGHT_MIN || weight > STACK_WEIGHT_MAX) { throw new IllegalArgumentException( " resizeStack: weight must be between " + STACK_WEIGHT_MIN + " and " + STACK_WEIGHT_MAX + " , weight= " + weight); } synchronized (mWindowMap) { final int numDisplays = mDisplayContents.size(); for (int displayNdx = 0; displayNdx < numDisplays; + + displayNdx) { if (mDisplayContents.valueAt(displayNdx).resizeStack(stackBoxId, weight)) { performLayoutAndPlaceSurfacesLocked(); return; } } } throw new IllegalArgumentException(" resizeStack: stackBoxId " + stackBoxId + " not found." ); }

【浅谈Android多屏幕的事】遍历mDisplayContents, 每一个DisplayContent都调用resizeStack, 然后进行重绘。
DisplayContent.resizeStack
/** Refer to {@ link WindowManagerService#resizeStackBox(int, float)} */ boolean resizeStack(int stackBoxId, float weight) { for (int stackBoxNdx = mStackBoxes.size() - 1; stackBoxNdx > = 0; --stackBoxNdx) { final StackBox box = mStackBoxes.get(stackBoxNdx); if (box.resize(stackBoxId, weight)) { layoutNeeded = true; return true; } } return false; }

DisplayContent.resizeStack里, 实际是相应的StackBox进行了resize,继续查看StackBox.resize。
StackBox.resize
boolean resize(int stackBoxId, float weight) { if (mStackBoxId != stackBoxId) { return mStack = = null & & (mFirst.resize(stackBoxId, weight) || mSecond.resize(stackBoxId, weight)); } // Don' t change weight on topmost stack. if (mParent != null) { mParent.mWeight = isFirstChild() ? weight : 1.0f - weight; } return true; }

之前讲过StackBox是一个树结构, StackBox.resize的时候它会一直遍历下去, 直到匹配到stackBoxId为至。别外注意的是这里的weight是0?1的float值, 它在屏幕上体现是一个相对值。
am stack box
  • 语法 : am stack box
  • 注解: am stack box: list the hierarchy of stack boxes rooted at
  • 实现: am stack box 的流程现在理解就非常的简单了, 与之前的stack boxes流程一样, 只不过boxes是取all boxInfo, box只取 pointed stack_box_id的bixInfo。
总结
综合以上命令的实现( 主要就是分屏的管理与实现) , 其涉及到相关的主要类及关系如下图:
浅谈Android多屏幕的事

文章图片

正常情况下, 一条实线走到底的啊, 这里的三条虚线是特殊情况, 从上至下我们把三条虚线分别标号为1、2、3。
  • 第1条虚线是在create stack 的时候stack_id需要由ActivityStackSuperviosr生成, 有了这个stack_id后再交给WMS进行create;
  • 第2条虚线, 在处理moveTaskToStack的流程是先由ActivityStackSuperviosr进行相关的remove and add 处理, 再由 ActivityStackSuperviosr去通知WMS进行remove and add 处理。
  • 第3条虚线, 就是如果一个非homeStack删除掉最后一个task的时候, 该stack也会被干掉, 同样除了在ActivityStackSupervisor里的mStacks里remove外, 也会通知WMS remove掉该stack, 在WMS里的remove操作其实就是对stack数据的destory, 在这里不再展示。
对于APP、AMS、WMS及SF之间的协作关系, 其它网站、博客、书籍已有大量的介绍, 这里不再赘述。
M (6.0)版本对多任务分屏的实现 这里主要谈谈与4.4kitkat的不同之处, 核心内容其实在4.4kitkat已经出现, 为什么不谈5.0版本, 因为5.0与6.0之间在多任务分屏模块变化不大, 理东西我们只需抓住头和尾就行了, 那为什么不直接谈N版本, 我也想, 可是源码还没有出来。。。
Am与分屏相关主要命令的变化 这一小节主要介绍各版本中am命令的变化, 来分析及概括分屏路的前世今生。
kikat(4.4)M(6.0)N
stack createstack startstack start
stack move taskstack move taskstack move task
stack resizestack resizestack resize
stack boxesstack resize-animated
stack boxstack resize-docked-stack
stack size-docked-stack-test
stack splitstack move-top-activity-to-pinned-stack
stack positiontask
stack liststack list
stack infostack info
stack remove
task locktask lock
task lock stop
task resizeabletask resizeable
task resizetask resize
task drag-task-test
task size-task-test
N版本分屏, 我们要做的事 以上内容都是系统在做, 那我们该做些什么、注意些什么呢?
1、 如何分屏
通过以下方式切换到多窗口模式:
  • 若打开 Overview 屏幕并长按 Activity 标题, 则可以拖动该 Activity 至屏幕突出显示的区域, 使 Activity 进入多窗口模式。
  • 若长按 Overview 按钮, 设备上的当前 Activity 将进入多窗口模式, 同时将打开 Overview 屏幕, 用户可在该屏幕中选择要共享屏幕的另一个 Activity。
用户可以在两个 Activity 共享屏幕的同时在这两个 Activity 之间拖放数据 ( 在此之前, 用户只能在一个 Activity 内部拖放数据) 。
2、 多窗口的配置应用
在清单的 或 节点中设置该属性, 启用或禁用多窗口显示( 在N版本里默认为true) :
android:resizeableActivity= [" true" | " false" ]

若为false相应的Activity不支持多窗口模式, 且用户尝试在多窗口模式下启动 Activity, 该 Activity 将全屏显示。
对于 Android N, 清单文件元素支持以下几种属性, 这些属性影响 Activity 在多窗口模式中的行为:
< activity android:name= " .MyActivity" > < layout android:defaultHeight= " 500dp" android:defaultWidth= " 600dp" android:gravity= " top|end" android:minimalHeight= " 450dp" android:minimalWidth= " 300dp" /> < /activity>

defaultWidth、defaultHeight分别是以自由形状模式启动时 Activity 的默认宽度和高度。。
gravity 以自由形状模式启动时 Activity 的初始位置。
min_size分屏和自由形状模式中 Activity 的最小高度和最小宽度。 如果用户在分屏模式中移动分界线, 使 Activity 尺寸低于指定的最小值, 系统会将 Activity 裁剪为用户请求的尺寸。
3、 多窗口的生命周期
多窗口模式不会更改 Activity 生命周期。但是要注意以下几点:
  • Activity状态的不同, 活动状态( 顶级) > 暂停状态(可见)> 暂停状态(不可见)。用户与其中一个暂停的 Activity 交互, 该 Activity 将恢复, 而之前的顶级 Activity 将暂停。
  • 用户仍可以看到处于暂停状态的应用, 因此在暂停状态下可能仍需要继续其操作。如视频播放应用在 onPause() 中不应该暂停播放, 应在 onStop() 中暂停, onStart() 中恢复。
    • Activity配置变更: 分屏与全屏的相互切换时触发, 与纵、横变换时Activity生命周期相同, 但设备不但交换尺寸, 还会变理尺寸; Activity可以自行处理配置变更, 或允许系统销毁 Activity, 并以新的尺寸重新创建该 Activity。
4、 在多窗口模式中运行应用
处于多窗口模式中时, 某些功能会被禁用或忽略:
  • 某些系统 UI 自定义选项将被禁用; 例如, 在非全屏模式中, 应用无法隐藏状态栏。
  • 系统将忽略对 android:screenOrientation 属性所作的更改。
    多窗口模式状态的监控:
  • Activity.isInMultiWindowMode(): Activity 是否处于多窗口模式。
  • Activity.onMultiWindowModeChanged(): Activity 进入或退出多窗口模式时系统将调用此方法。 在 Activity 进入多窗口模式时, 系统向该方法传递 true 值, 在退出多窗口模式时, 则传递 false 值。
    每个方法对于 Fragment 也支持, 例如 Fragment.isInMultiWindowMode()。
5、 多窗口模式中启动新的Activity
在启动新 Activity 时, 用户可以提示系统如果可能, 应将新 Activity 显示在当前 Activity 旁边。 要执行此操作, 可使用标志 Intent.FLAG_ACTIVITY_LAUNCH_TO_ADJACENT。 传递此标志将请求以下行为:
如果设备处于分屏模式, 系统会尝试在启动系统的 Activity 旁创建新 Activity, 这样两个 Activity 将共享屏幕。 系统并不一定能实现此操作, 但如果可以, 系统将使两个 Activity 处于相邻的位置。
如果设备不处于分屏模式, 则该标志无效。
如果设备处于自由形状模式, 则在启动新 Activity 时, 用户可通过调用 ActivityOptions.setLaunchBounds() 指定新 Activity 的尺寸和屏幕位置。 如果设备不处于多窗口模式, 则该方法无效。
注: 如果您在任务栈中启动 Activity, 该 Activity 将替换屏幕上的 Activity, 并继承其所有的多窗口属性。 如果要在多窗口模式中以单独的窗口启动新 Activity, 则必须在新的任务栈中启动此 Activity。
6、 拖放功能的实现
用户可以在两个 Activity 共享屏幕的同时在这两个 Activity 之间拖放数据 ( 在此之前, 用户只能在一个 Activity 内部拖放数据) 。
android.view.DropPermissions: 令牌对象, 负责指定对接收拖放数据的应用授予的权限。
View.startDragAndDrop() : View.startDrag() 的新别名。要启用跨 Activity 拖放, 请传递新标志 View.DRAG_FLAG_GLOBAL。 如需对接收拖放数据的 Activity 授予 URI 权限, 可根据情况传递新标志 View.DRAG_FLAG_GLOBAL_URI_READ 或 View.DRAG_FLAG_GLOBAL_URI_WRITE。
View.cancelDragAndDrop(): 取消当前正在进行的拖动操作。只能由发起拖动操作的应用调用。
View.updateDragShadow(): 替换当前正在进行的拖动操作的拖动阴影。只能由发起拖动操作的应用调用。
Activity.requestDropPermissions(): 请求使用 DragEvent 中包含的 ClipData 传递的内容 URI 的权限。
在android4.0中应用View.startDrag()实现View的拖动:
http://blog.csdn.net/krislight/article/details/12250039
7、 官网上也提到如何测试分屏应用, 这里不在赘述!
https://developer.android.com/preview/features/multi-window.html#running

    推荐阅读