前言:代码基于android8的分屏源码,它的分屏操作是从recent画面中长按一个任务,然后会将该任务挪到左边一半的屏幕显示出来并且可以交互,右边仍为recent画面,此时仍可以长按recent画面中的任务再打开一个任务,然后recent画面就消失。目前最多只能支持两个任务的分屏,现在希望通过研究两个任务分屏怎么实现的,去实现三个,或者四个任务的分屏。
双屏怎么实现的:
源码中的分屏模式,思路是通过双栈(一个DOCKED stack,一个RECENT stack或者FULL_SCREEN stack,取决于右半屏的应用是什么),把它们挪到前面,对应的task则挪到stack的前面,然后设置它们的bounds(栈的显示区域),从而限制了窗口的大小,最后绘制的时候就有两个应用显示在前台了。
我们重点看startActivityFromRecent()里的逻辑,如图:
图中的函数调用关系已经清晰了,着重讲几个关键点:
我们重写了一个自己的recent activity,名叫CustomRecentActivity,它的主体是一个RecyclerView,用GirdLayoutManager实现格子布局,我们要主要看在长按一个recent的时候,它是如何打开这个recent,并且分屏的,代码在这:
@Override
public boolean onLongClickItem(int position) { //重写了onLongClickItem,长按某个recent的view的时候会调用这
if (processEvent != null && mStack != null) {
ArrayList<Task> list = filterIgnoreTask(mStack.getStackTasks());
if (list != null && list.size() > position) {
return processEvent.onLongClickItem(list.get(position), mStack); //CustomRecentActivity实现了这个接口
}
}
return false;
}
----------------------------------------------------------------------------------
@Override
public boolean onLongClickItem(Task task, TaskStack stack) {
if (isInMultiWindow) { //如果进入了分屏,这个为true
return false;
}
SystemServicesProxy ssp = Recents.getSystemServices();
ssp.startTaskInDockedMode(task.key.id, ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT); //打开选中的recent
mStack.removeTask(task, null, true /* fromDockGesture */);
ssp.overridePendingAppTransitionMultiThumbFuture(null,
null,
true /* scaleUp */);
startSelf();
if(task.key.getComponent().getPackageName().equals("display.interactive.board2")){
Intent mIntent = new Intent("com.hikvision.android.intent.action.multiwindow.showLeft");
mIntent.setFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
Context mContext = CustomRecentsActivity.this;
mContext.sendBroadcast(mIntent,"com.hikvision.receiver");
}
return true;
}
---------------------------------------------------------------------------------------
/** Docks a task to the side of the screen and starts it. */
public boolean startTaskInDockedMode(int taskId, int createMode) {
if (mIam == null) return false;
try {
final ActivityOptions options = ActivityOptions.makeBasic();
options.setDockCreateMode(createMode);
options.setLaunchStackId(DOCKED_STACK_ID); //这个stackid为3
mIam.startActivityFromRecents(taskId, options.toBundle()); //调用到AMS里的逻辑,请求打开对应recent的activity
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to dock task: " + taskId + " with createMode: " + createMode, e);
}
return false;
}
这三个函数就是处理输入事件,然后去向AMS请求打开对应的recent的activity,这里有一个点比较重要,就是DOCKED_STACK_ID,我们需要设置该stack id,AMS才会知道你要分屏,然后创建DOCKED stack。
桌面是在id为0这个ActivityStack中管理着的,其他全屏的app的activity则在1这个ActivityStack中,至于分屏的app,自然是在3(DOCKED)这个ActivityStack中了。继续看AMS怎么处理请求的,它是转给了ActivityStackSupervisor中:
final int startActivityFromRecentsInner(int taskId, Bundle bOptions) {
final TaskRecord task;
final int callingUid;
final String callingPackage;
final Intent intent;
final int userId;
final ActivityOptions activityOptions = (bOptions != null)
? new ActivityOptions(bOptions) : null;
final int launchStackId = (activityOptions != null) // DOCKED_STACK_ID = 3
? activityOptions.getLaunchStackId() : INVALID_STACK_ID;
mWindowManager.deferSurfaceLayout();
try {
if (launchStackId == DOCKED_STACK_ID) {
mWindowManager.setDockedStackCreateState(
activityOptions.getDockCreateMode(), null /* initialBounds */);
// Defer updating the stack in which recents is until the app transition is done, to
// not run into issues where we still need to draw the task in recents but the
// docked stack is already created.
deferUpdateBounds(RECENTS_STACK_ID);
mWindowManager.prepareAppTransition(TRANSIT_DOCK_TASK_FROM_RECENTS, false);
}
task = anyTaskForIdLocked(taskId, MATCH_TASK_IN_STACKS_OR_RECENT_TASKS_AND_RESTORE,
launchStackId); //step1
// Since we don't have an actual source record here, we assume that the currently
// focused activity was the source.
final ActivityStack focusedStack = getFocusedStack();
final ActivityRecord sourceRecord =
focusedStack != null ? focusedStack.topActivity() : null;
if (launchStackId != INVALID_STACK_ID) {
if (task.getStackId() != launchStackId) {
task.reparent(launchStackId, ON_TOP, REPARENT_MOVE_STACK_TO_FRONT, ANIMATE,
DEFER_RESUME, "startActivityFromRecents"); //step2
}
}
// 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, true /* fromRecents */);
mActivityMetricsLogger.notifyActivityLaunched(ActivityManager.START_TASK_TO_FRONT,
task.getTopActivity());
// If we are launching the task in the docked stack, put it into resizing mode so
// the window renders full-screen with the background filling the void. Also only
// call this at the end to make sure that tasks exists on the window manager side.
if (launchStackId == DOCKED_STACK_ID) {
setResizingDuringAnimation(task);
}
mService.mActivityStarter.postStartActivityProcessing(task.getTopActivity(),
ActivityManager.START_TASK_TO_FRONT,
sourceRecord != null
? sourceRecord.getTask().getStackId() : INVALID_STACK_ID,
sourceRecord, task.getStack());
return ActivityManager.START_TASK_TO_FRONT;
}
callingUid = task.mCallingUid;
callingPackage = task.mCallingPackage;
intent = task.intent;
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
userId = task.userId;
int result = mService.startActivityInPackage(callingUid, callingPackage, intent, null,
null, null, 0, 0, bOptions, userId, null, task, "startActivityFromRecents");
if (launchStackId == DOCKED_STACK_ID) {
setResizingDuringAnimation(task);
}
return result;
} finally {
mWindowManager.continueSurfaceLayout(); //step3
}
}
我们再看这个函数里做的事情,我们对应分屏的case,如果是简单的在recent中打开一个应用,那么下面的不会发生。 主要分三步:
Step1. 首先判断launch id是否为DOCKED stack的id,是则说明用户想分屏,然后在WMS中设置一些标志位。 接着,在DOCKED stack查找要分屏的task,第一次分屏这里都为null,所以系统会新建一个DOCKED stack,并且WMS中也会新建一个TaskStack(与ActivityStack对应)。
Step2. 有了DOCKED stack后,要做一次reparent操作,即把要分屏的task从旧栈移除,添加到DOCKED stack中,然后调用resize,让task的bounds与栈的bounds保持一致,bounds的值会影响WindowState中的mFrame的值,该值会在窗口绘制的时候生效,限制显示的区域。 由于要分屏,窗口层级的大小也做相应的调整。
Step3. 最后,就可以启动要分屏应用的Activity了,应用的task已经被挪到了栈顶,这个时候启动它的top activity,最后调用continueSurfaceLayout去触发WMS中的逻辑。