简介
本文主要介绍从多任务中启动自由窗口的流程。 操作手法:打开任意应用后,进入到多任务,点击应用图标,选择【自由窗口】。
层级结构
这里以后台存在dialer、messaging应用,通过多任务中启动messaging应用为自由窗口为例。
使用命令adb shell dumpsys activity containers抓取对应的层级结构,在DefaultTaskDisplayArea中找到对应的task。
启动前:
#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#3 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 Task=1155 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 ActivityRecord{6fc2a2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t1155} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 e485e74 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#2 Task=1161 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 ActivityRecord{99290ba u0 com.android.dialer/.main.impl.MainActivity t1161} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 6e78921 com.android.dialer/com.android.dialer.main.impl.MainActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#1 Task=1160 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 ActivityRecord{bfe1cf6 u0 com.android.messaging/.ui.conversationlist.ConversationListActivity t1160} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 a1f0bc7 com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#1 Task=1160这是我们要启动为自由窗口的task,我们主要观察其窗口模式和窗口大小的变化,即mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]的变化。
启动后:
#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#3 Task=1160 type=standard mode=freeform override-mode=freeform requested-bounds=[50,134][770,1614] bounds=[50,134][820,1614]
#0 ActivityRecord{bfe1cf6 u0 com.android.messaging/.ui.conversationlist.ConversationListActivity t1160} type=standard mode=freeform override-mode=undefined requested-bounds=[0,0][0,0] bounds=[50,134][820,1614]
#0 63479c4 com.android.messaging/com.android.messaging.ui.conversationlist.ConversationListActivity type=standard mode=freeform override-mode=undefined requested-bounds=[0,0][0,0] bounds=[50,134][820,1614]
#2 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 Task=1155 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 ActivityRecord{6fc2a2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t1155} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 e485e74 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#1 Task=1161 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 ActivityRecord{99290ba u0 com.android.dialer/.main.impl.MainActivity t1161} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
#0 6e78921 com.android.dialer/com.android.dialer.main.impl.MainActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
这里我们看到#3 Task=1160也就是我们启动为自由窗口的task,明显可以看到其窗口模式变为了自由窗口,即mode=freeform override-mode=freeform。窗口大小也发生了相应的变化,即requested-bounds=[50,134][770,1614] bounds=[50,134][820,1614]。
另外还有补充说明一点,切换为自由窗口前后Task=1160前面的序号 #1 变为了 #3,这个序号最大的task表示在最前台,即切换为自由窗口的Task=1160在最前台。
开启自由窗口
运行adb命令开启自由窗口选项
adb shell settings put global enable_freeform_support 1
adb shell settings put global force_resizable_activities 1
原生机在多任务中原本点击应用图标不会出现【自由窗口】的选项,通过命令开启后会显示出来。
查找相关代码
在根据
recent_task_option_freeform找到了相应的代码。
桌面侧流程
FREE_FORM创建
代码路径:packages/apps/Launcher3/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
TaskShortcutFactory FREE_FORM = new TaskShortcutFactory() {
@Override
public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
TaskIdAttributeContainer taskContainer) {
//获取任务列表中对应的task
final Task task = taskContainer.getTask();
if (!task.isDockable) {
return null;
}
if (!isAvailable(activity, task.key.displayId)) {
return null;
}
//创建了自由窗口
//这里我们主要关注FreeformSystemShortcut
return Collections.singletonList(new FreeformSystemShortcut(
R.drawable.ic_caption_desktop_button_foreground,
R.string.recent_task_option_freeform, activity, taskContainer,
LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP));
}
//判断是否支持自由窗口
//这里我们主要关注supportsFreeformMultiWindow
private boolean isAvailable(BaseDraggingActivity activity, int displayId) {
return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)
&& !SystemProperties.getBoolean("persist.wm.debug.desktop_mode", false)
&& !SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false);
}
};
点击任意任务的icon就会走这些流程,taskContainer.getTask()这里会获取对应的任务列表中的task。
创建自由窗口
new FreeformSystemShortcut(
R.drawable.ic_caption_desktop_button_foreground,
R.string.recent_task_option_freeform, activity, taskContainer,
LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP)
先看前两个参数R.drawable.ic_caption_desktop_button_foreground(资源路径:packages/apps/Launcher3/res/drawable/ic_caption_desktop_button_foreground.xml)和 R.string.recent_task_option_freeform(资源路径:packages/apps/Launcher3/quickstep/res/values-zh-rCN/strings.xml)
activity:就是桌面,通过添加log打印结果:com.android.launcher3.uioverrides.QuickstepLauncher@c88e80f。
taskContainer:最近任务列表中的View的一些属性,其为TaskView内部类的TaskIdAttributeContainer对象,常用于获取TaskThumbnailView、Task、IconView等TaskView的属性。
通过添加log打印结果:taskContainer:TaskIdAttributeContainer{mThumbnailView=com.android.quickstep.views.TaskThumbnailView{3a39e6e V.ED..... ........ 0,224-978,2234 #7f0902a7 app:id/snapshot}, mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mStagePosition=-1, mA11yNodeId=2131296947}。
从这里我们可以看到taskContainer中有 id=1171的Task ,也就是我们当前从最近任务列表中选中的task,代码中也是通过 taskContainer.getTask()获取对应的任务列表中的task。
LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP:一个log相关的常量,大概意思就是通过多任务启动浮窗。
判断是否支持自由窗口
ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity)
代码路径:frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
/**
* Returns true if the system supports freeform multi-window.
*/
public boolean supportsFreeformMultiWindow(Context context) {
final boolean freeformDevOption = Settings.Global.getInt(context.getContentResolver(),
Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
return ActivityTaskManager.supportsMultiWindow(context)
&& (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT)
|| freeformDevOption);
}
代码路径:frameworks/base/core/java/android/provider/Settings.java
/**
* Whether to enable experimental freeform support for windows.
* @hide
*/
@Readable
public static final String DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT
= "enable_freeform_support";
配置项通过adb shell settings put global enable_freeform_support 1命令打开。
后续继续分析FreeformSystemShortcut类,看看自由窗口是如何创建的。
自由窗口的创建
桌面侧自由窗口流程主要是在TaskShortcutFactory中的内部类FreeformSystemShortcut实现。 代码路径:packages/apps/Launcher3/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
class FreeformSystemShortcut extends SystemShortcut<BaseDraggingActivity> {
private static final String TAG = "FreeformSystemShortcut";
private Handler mHandler;
private final RecentsView mRecentsView;
private final TaskThumbnailView mThumbnailView;
private final TaskView mTaskView;
private final LauncherEvent mLauncherEvent;
public FreeformSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
TaskIdAttributeContainer taskContainer, LauncherEvent launcherEvent) {
super(iconRes, textRes, activity, taskContainer.getItemInfo(),
taskContainer.getTaskView());
mLauncherEvent = launcherEvent;
mHandler = new Handler(Looper.getMainLooper());
//最近任务列表中选中的应用的View
mTaskView = taskContainer.getTaskView();
//这里activity就是桌面
//通过桌面getOverviewPanel方法获取了整个最近任务View
mRecentsView = activity.getOverviewPanel();
mThumbnailView = taskContainer.getThumbnailView();
}
这里构造方法中就是从前面创建FREE_FORM时,传递过来的值复制给了FreeformSystemShortcut的成员变量。
通过taskContainer.getTaskView()获取到了当前最近任务中选中的TaskView,其打印如下:
mTaskView:TaskView{mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mActivity=com.android.launcher3.uioverrides.QuickstepLauncher@d676323, mTaskViewId=1, mTaskIdContainer=[1171, -1], mTaskIdAttributeContainer=[TaskIdAttributeContainer{mThumbnailView=com.android.quickstep.views.TaskThumbnailView{3a39e6e V.ED..... ........ 0,224-978,2234 #7f0902a7 app:id/snapshot}, mTask=[id=1171 windowingMode=1 user=0 lastActiveTime=150231] null, mIconView=com.android.quickstep.views.IconView{ff221e9 V.ED..CL. ...P.... 405,0-573,168 #7f09018f app:id/icon}, mStagePosition=-1, mA11yNodeId=2131296947}, null]}
监听onClick事件
//监听onClick事件
//响应最近任务中点击【自由窗口】的事件
@Override
public void onClick(View view) {
dismissTaskMenuView(mTarget);
//获取最近任务列表
RecentsView rv = mTarget.getOverviewPanel();
rv.switchToScreenshot(() -> {
rv.finishRecentsAnimation(true /* toHome */, () -> {
mTarget.returnToHomescreen();
//在最近任务中通过startActivity启动应用
rv.getHandler().post(this::startActivity);
});
});
}
mTarget:实际上就是桌面,其打印结果为com.android.launcher3.uioverrides.QuickstepLauncher@6da5963
rv:是RecentsView对象,也就是最近任务列表,通过 getOverviewPanel() 方法获取。其打印结果为com.android.quickstep.views.LauncherRecentsView{ade49d1 V.ED..... .......D 0,0-1440,2960 #7f09022a app:id/overview_panel},其对应的id就是 overview_panel 。
这里onClick监听的就是这个【自由窗口】的事件。
启动自由窗口
//启动需要进入自由窗口Activity
private void startActivity() {
//通过TaskView(最近任务列表中选中的应用的View)获取其TaskKey
final Task.TaskKey taskKey = mTaskView.getTask().key;
//通过TaskKey获取其taskId
final int taskId = taskKey.id;
//创建响应的options
final ActivityOptions options = makeLaunchOptions(mTarget);
if (options != null) {
options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
}
//options不为空,则从最近任务重启动activity,传递参数为taskId和options
if (options != null
&& ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
options)) {
final Runnable animStartedListener = () -> {
// Hide the task view and wait for the window to be resized
// TODO: Consider animating in launcher and do an in-place start activity
// afterwards
mRecentsView.setIgnoreResetTask(taskId);
mTaskView.setAlpha(0f);
};
//一些缩略图相关操作
......
}
}
这个方法中有两个比较关键的点
- 设置ActivityOptions,主要用来设置自由窗口相关参数。
- 通过startActivityFromRecents启动自由窗口。
makeLaunchOptions
final ActivityOptions options = makeLaunchOptions(mTarget);
//设置Options
private ActivityOptions makeLaunchOptions(Activity activity) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
//设置窗口模式,即ActivityOptions.mLaunchWindowingMode为WINDOWING_MODE_FREEFORM
activityOptions.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);
// Arbitrary bounds only because freeform is in dev mode right now
//主要设置需要启动为自由窗口的应用界面窗口相关的大小(以传递过来的activity为参照等比缩小)
//这里activity参数传递过的是桌面(QuickstepLauncher)
//其实就是以桌面的容器尺寸来等比创建一个Rect作为后续启动的自由窗口的尺寸
final View decorView = activity.getWindow().getDecorView();
final WindowInsets insets = decorView.getRootWindowInsets();
//设置浮窗初始大小
final Rect r = new Rect(0, 0, decorView.getWidth() / 2, decorView.getHeight() / 2);
//设置偏移量,避免浮窗在状态栏之上显示
//insets.getSystemWindowInsetLeft()获取左边insets相关大小(比较少见)
//insets.getSystemWindowInsetTop()获取顶部边insets相关大小(比如状态栏)
r.offsetTo(insets.getSystemWindowInsetLeft() + 50,
insets.getSystemWindowInsetTop() + 50);
activityOptions.setLaunchBounds(r);
return activityOptions;
}
这个方法主通过ActivityOptions来设置 窗口模式(mLaunchWindowingMode) 和 窗口大小(mLaunchBounds) ,后续启动自由窗口时会传递。
另外,浮窗大小最后还会在Task.computeFreeformBounds中进行校准。bounds的最终设置是在WindowConfiguration.setBounds,可以通过在这里添加堆栈来看看最终设置bounds的值以及其流程。例如:
代码路径:frameworks/base/core/java/android/app/WindowConfiguration.java
/**
* Sets the bounds to the provided {@link Rect}.
* @param rect the new bounds value.
*/
public void setBounds(Rect rect) {
//添加堆栈 start
//这里判断窗口模式为浮窗的情况打印log
if (mWindowingMode == WINDOWING_MODE_FREEFORM){
android.util.Slog.i("TAG","setBounds rect:"+rect,new Exception());
}
//end
if (rect == null) {
mBounds.setEmpty();
return;
}
mBounds.set(rect);
}
startActivityFromRecents
ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,options)
这里的传递需要进入到自由窗口的taskId,以及需要设置的options参数。后续继续分析该流程。
systemUI侧
systemUI在这里的作用是作为桌面和system_server通信的中转站。 代码路径:frameworks/base/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
/**
* Starts a task from Recents synchronously.
*/
public boolean startActivityFromRecents(int taskId, ActivityOptions options) {
try {
//这里传递过来的options不为空
Bundle optsBundle = options == null ? null : options.toBundle();
//调用startActivityFromRecents
return ActivityManager.isStartResultSuccessful(
getService().startActivityFromRecents(
taskId, optsBundle));
} catch (Exception e) {
return false;
}
}
这里就是通过获取ActivityTaskManagerService调用其startActivityFromRecents方法,并传递taskId和optsBundle。
taskId:TaskShortcutFactory中startActivity传递过来的需要启动为自由窗口的应用taskId。
optsBundle:TaskShortcutFactory中通过makeLaunchOptions创建的值。