本文基于android9.0来写的。
一、产品需求:如下图,类似mac的桌面系统
(1)区域1是系统标题栏。
(2)区域2是长显示的窗口。
(3)区域3 普通app显示的窗口。
(4) 区域4 也是一个上显示的窗口,主要用于应用的点击启动。
二、下图是根据Android系统特性画的草图:
(1)区域1是系统statusBar。
(2)区域2是长显示的窗口,用WindowMananger添加的窗口。
(3)区域3 普通app显示的窗口,定制系统默认窗口大小,定位至区域3。
(4) 区域4 也是一个长显示的窗口,同样是用WindowMananger添加的窗口。
三、主要实现思路:通过freefrom 和添加悬浮window来实现。因为app只有在freeform模式才能任意定制多窗口大小和位置。
1、区域1无需改动,还是采用系统默认stausBar。
2、修改famework AMS ,以freeform模式启动Launcher。修改HOME的的区域为上图区域3。
3、Lancher启动时,同时启动两个两个service, 然后通过WindowManger来添加区域2和区域4。
4、区域4 的Icon点击以freeform模式启动其他APP。在framework拦截启动APP的入口,以freeform模式启动,同时设置窗口大小和位置(区域3)。
四、最终实现的demo如下:
五、具体实现方法如下:
1、开启freeform
google 默认freeform是关闭的,如果要测试需要手动开启。
配置
config_freeformWindowManagement
WORKING_DIRECTORY/frameworks/base/core/res/res/values/config.xml
如下,将config_freeformWindowManagement配置为true
WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
private void retrieveSettings() {
final ContentResolver resolver = mContext.getContentResolver();
//获取是否支持freeform
final boolean freeformWindowManagement =
mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
|| Settings.Global.getInt(
resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
由上面代码可以看出有两种方法可以开启freeform
(1) Settings 设置开启的属性
WORKING_DIRECTORY/frameworks/base/core/java/android/provider/Settings.java
/**
* Whether any activity can be resized. When this is true, any
* activity, regardless of manifest values, can be resized for multi-window.
* (0 = false, 1 = true)
* @hide
*/
public static final String DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES
= "force_resizable_activities";
/**
* Whether to enable experimental freeform support for windows.
* @hide
*/
public static final String DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT
= "enable_freeform_support";
这个用命令就可以开启:
adb shell settings put global enable_freeform_support 1
adb shell settings put global force_resizable_activities 1
如果要在framework中默认开启,可以这样:
private void retrieveSettings() {
final ContentResolver resolver = mContext.getContentResolver();
//简单粗暴开启,哈哈
Settings.Global.putInt(resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 1);
Settings.Global.putInt(resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 1);
final boolean freeformWindowManagement =
mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
|| Settings.Global.getInt(
resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
(2)给系统添加feature:android.software.freeform_window_management
其实android系统默认已经写好,这里可以找到:
WORKING_DIRECTORY/frameworks/native/data/etc
可以将android.software.freeform_window_management.xml 拷贝到WORKING_DIRECTORY/frameworks/base/data/etc
里面有个Android.mk在这里面添加如下:
########################
include $(CLEAR_VARS)
LOCAL_MODULE := android.software.freeform_window_management.xml
LOCAL_MODULE_CLASS := ETC
LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions
LOCAL_SRC_FILES := $(LOCAL_MODULE)
include $(BUILD_PREBUILT)
它最终会被拷贝到手机的system/etc/permissions/目录下,开机时PMS会通过的读取此目录下xml配置开启freeform这个feature.
但是经过测试这个还是不够,还需要开启
Settings.Global.putInt(resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 1);
2、freeform模式启动app
Android为了支持多窗口,在运行时创建了多个Stack,Stack就是类似这里虚拟桌面的作用。
每个Stack会有一个唯一的Id,在WindowConfiguration.java中定义了这些Stack的Id:如下:
public class WindowConfiguration
implements android.os.Parcelable, java.lang.Comparable<android.app.WindowConfiguration>
{
public WindowConfiguration() { throw new RuntimeException("Stub!"); }
public void writeToParcel(android.os.Parcel dest, int flags) { throw new RuntimeException("Stub!"); }
public int describeContents() { throw new RuntimeException("Stub!"); }
public void setBounds(android.graphics.Rect rect) { throw new RuntimeException("Stub!"); }
public void setAppBounds(android.graphics.Rect rect) { throw new RuntimeException("Stub!"); }
public android.graphics.Rect getAppBounds() { throw new RuntimeException("Stub!"); }
public android.graphics.Rect getBounds() { throw new RuntimeException("Stub!"); }
public void setWindowingMode(int windowingMode) { throw new RuntimeException("Stub!"); }
public int getWindowingMode() { throw new RuntimeException("Stub!"); }
public void setActivityType(int activityType) { throw new RuntimeException("Stub!"); }
public int getActivityType() { throw new RuntimeException("Stub!"); }
public void setTo(android.app.WindowConfiguration other) { throw new RuntimeException("Stub!"); }
public int compareTo(android.app.WindowConfiguration that) { throw new RuntimeException("Stub!"); }
public static final int ACTIVITY_TYPE_ASSISTANT = 4;
public static final int ACTIVITY_TYPE_HOME = 2;
public static final int ACTIVITY_TYPE_RECENTS = 3;
public static final int ACTIVITY_TYPE_STANDARD = 1;
public static final int ACTIVITY_TYPE_UNDEFINED = 0;
public static final int WINDOWING_MODE_FREEFORM = 5;
public static final int WINDOWING_MODE_FULLSCREEN = 1;
public static final int WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY = 4;
public static final int WINDOWING_MODE_PINNED = 2;
public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3;
public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4;
public static final int WINDOWING_MODE_UNDEFINED = 0;
}
例如以freeform启动activity
Intent intent = new Intent(this, AdjacentActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT | Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setLaunchWindowingMode(5);
int left = 100;
int top = 0;
int right = 720;
int bottom = 2280;
activityOptions.setLaunchBounds(new Rect(left,top,right,bottom));
Bundle bundle = activityOptions.toBundle();
startActivity(intent,bundle)
setLaunchWindowingMode 是个testApi,我们开发的时候获取不到,可以通过反射来调用
public static ActivityOptions getActivityOptions(Context context) {
ActivityOptions options = ActivityOptions.makeBasic();
int freeform_stackId = 5;
try {
Method method = ActivityOptions.class.getMethod("setLaunchWindowingMode", int.class);
method.invoke(options, freeform_stackId);
} catch (Exception e) { /* Gracefully fail */ }
return options;
}
具体详细用法可以参考
https://github.com/farmerbb/Taskbar
3、如何以freeform 启动launcher
/WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
boolean startHomeActivityLocked(int userId, String reason) {
if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
&& mTopAction == null) {
// We are running in factory test mode, but unable to find
// the factory test app, so just sit around displaying the
// error message and don't try to start anything.
return false;
}
Intent intent = getHomeIntent();
ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);
if (aInfo != null) {
intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));
// Don't do this if the home app is currently being
// instrumented.
aInfo = new ActivityInfo(aInfo);
aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);
ProcessRecord app = getProcessRecordLocked(aInfo.processName,
aInfo.applicationInfo.uid, true);
if (app == null || app.instr == null) {
intent.setFlags(intent.getFlags() | FLAG_ACTIVITY_NEW_TASK);
final int resolvedUserId = UserHandle.getUserId(aInfo.applicationInfo.uid);
// For ANR debugging to verify if the user activity is the one that actually
// launched.
final String myReason = reason + ":" + userId + ":" + resolvedUserId;
//这里调用启动homeActivity
mActivityStartController.startHomeActivity(intent, aInfo, myReason);
}
} else {
Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());
}
return true;
}
/WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityStartController.java
void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason) {
mSupervisor.moveHomeStackTaskToTop(reason);
// 这里我设置了freefrom启动参数。
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setLaunchWindowingMode(5);
Point size = new Point();
mService.mWindowManager.getInitialDisplaySize(0,size);
int left = 100;
int top = 0;
int right = size.x;
int bottom = size.y*6/7;
activityOptions.setLaunchBounds(new Rect(left,top,right,bottom));
Bundle bundle = activityOptions.toBundle();
mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
.setOutActivity(tmpOutRecord)
.setCallingUid(0)
.setActivityInfo(aInfo)
.setActivityOptions(bundle)
.execute();
mLastHomeActivityStartRecord = tmpOutRecord[0];
if (mSupervisor.inResumeTopActivity) {
// If we are in resume section already, home activity will be initialized, but not
// resumed (to avoid recursive resume) and will stay that way until something pokes it
// again. We need to schedule another resume.
mSupervisor.scheduleResumeTopActivities();
}
}
WORKING_DIRECTORY/frameworks/base/services/core/java/com/android/server/am/ActivityStarter.java
作如下修改:
// Note: This method should only be called from {@link startActivity}.
private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
ActivityRecord[] outActivity) {
setInitialState(r, options, inTask, doResume, startFlags, sourceRecord, voiceSession,
voiceInteractor);
computeLaunchingTaskFlags();
computeSourceStack();
mIntent.setFlags(mLaunchFlags);
ActivityRecord reusedActivity = getReusableIntentActivity();
int preferredWindowingMode = WINDOWING_MODE_UNDEFINED;
int preferredLaunchDisplayId = DEFAULT_DISPLAY;
//在这里对启动的activity作拦截,用freeform启动,设置setLaunchWindowingMode 为5
Point size = new Point();
Display display = mSupervisor.mDisplayManager.getDisplay(0);
DisplayMetrics displayMetrics = new DisplayMetrics();
display.getMetrics(displayMetrics);
size.x = displayMetrics.widthPixels;
size.y = displayMetrics.heightPixels;
Slog.e("ZyTest", "myTestZize displayMetrics w = " + displayMetrics.widthPixels + " h = " + displayMetrics.heightPixels);
if (mOptions != null) {
preferredWindowingMode = mOptions.getLaunchWindowingMode();
preferredLaunchDisplayId = mOptions.getLaunchDisplayId();
Slog.e("ZyTest", "preferredWindowingMode = " + preferredWindowingMode );
int left = size.x /4;
int top = 0;
int right = size.x;
int bottom = size.y*6/7;;
mOptions.setLaunchBounds(new Rect(left,top,right,bottom));
mOptions.setLaunchWindowingMode(5);
}else {
mOptions = ActivityOptions.makeBasic();
int left = size.x /4;
int top = 0;
int right = size.x;
int bottom = size.y*6/7;;
mOptions.setLaunchBounds(new Rect(left,top,right,bottom));
mOptions.setLaunchWindowingMode(5);
Slog.e("ZyTest", "22222 preferredWindowingMode = " + preferredWindowingMode );
}
preferredWindowingMode = 5;
// windowing mode and preferred launch display values from {@link LaunchParams} take
// priority over those specified in {@link ActivityOptions}.
if (!mLaunchParams.isEmpty()) {
if (mLaunchParams.hasPreferredDisplay()) {
preferredLaunchDisplayId = mLaunchParams.mPreferredDisplayId;
}
if (mLaunchParams.hasWindowingMode()) {
preferredWindowingMode = mLaunchParams.mWindowingMode;
}
}
设置启动的目标栈的setWindowingMode为freeform,位置和大小自定义
private int setTaskFromReuseOrCreateNewTask(
TaskRecord taskToAffiliate, ActivityStack topStack) {
mTargetStack = computeStackFocus(mStartActivity, true, mLaunchFlags, mOptions);
boolean ishome = mStartActivity.isActivityTypeHome();
Slog.e("ZyTest", "is home llllllllll = " + ishome);
// if(ishome){
Point size = new Point();
// mService.mWindowManager.getInitialDisplaySize(0,size);
//设置目标栈的setWindowingMode、位置 、大小
Display display = mSupervisor.mDisplayManager.getDisplay(0);
DisplayMetrics displayMetrics = new DisplayMetrics();
display.getMetrics(displayMetrics);
size.x = displayMetrics.widthPixels;
size.y = displayMetrics.heightPixels;
int left = size.x /4;
int top = 0;
int right = size.x;
int bottom = size.y*6/7;
mTargetStack.setBounds(left,top,right,bottom);
mTargetStack.setWindowingMode(5);
// }
// Do no move the target stack to front yet, as we might bail if
// isLockTaskModeViolation fails below.
if (mReuseTask == null) {
final TaskRecord task = mTargetStack.createTaskRecord(
mSupervisor.getNextTaskIdForUserLocked(mStartActivity.userId),
mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession,
mVoiceInteractor, !mLaunchTaskBehind /* toTop */, mStartActivity, mSourceRecord,
mOptions);
addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask - mReuseTask");
updateBounds(mStartActivity.getTask(), mLaunchParams.mBounds);
if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + mStartActivity
+ " in new task " + mStartActivity.getTask());
} else {
addOrReparentStartingActivity(mReuseTask, "setTaskFromReuseOrCreateNewTask");
}
if (taskToAffiliate != null) {
mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
}
if (mService.getLockTaskController().isLockTaskModeViolation(mStartActivity.getTask())) {
Slog.e(TAG, "Attempted Lock Task Mode violation mStartActivity=" + mStartActivity);
return START_RETURN_LOCK_TASK_MODE_VIOLATION;
}
if (mDoResume) {
mTargetStack.moveToFront("reuseOrNewTask");
}
return START_SUCCESS;
}