平台
RK3288 + Android 7.1
关于Freeform
Android N上的多窗口功能有三种模式:(扩展-4)
分屏模式
这种模式可以在手机上使用。该模式将屏幕一分为二,同时显示两个应用的界面。
画中画模式
这种模式主要在TV上使用,在该模式下视频播放的窗口可以一直在最顶层显示。
Freeform模式
这种模式类似于我们常见的桌面操作系统,应用界面的窗口可以自由拖动和修改大小。
效果图
切入
平台默认并没有打开这个模式的支持, 需要增加一个文件以打开Feeeform特性
增加 /system/etc/permissions/android.software.freeform_window_management.xml
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2015 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <permissions> <feature name="android.software.freeform_window_management" /> </permissions>
打开后:
看任务右上角 X 旁边的图标
然而, 当尝试点击此按键后, 预想的画面并没有出现, 费解!
排查
跟踪下源码中此界面的布局:
frameworks/base/packages/SystemUI/res/layout/recents_task_view_header.xml
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2014 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <!-- The layouts params are calculated in TaskViewHeader.java --> <com.android.systemui.recents.views.TaskViewHeader xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/task_view_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="top|center_horizontal"> <com.android.systemui.recents.views.FixedSizeImageView android:id="@+id/icon" android:contentDescription="@string/recents_app_info_button_label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" android:paddingTop="8dp" android:paddingBottom="8dp" android:paddingStart="16dp" android:paddingEnd="12dp" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" android:textSize="16sp" android:textColor="#ffffffff" android:text="@string/recents_empty_message" android:fontFamily="sans-serif-medium" android:singleLine="true" android:maxLines="1" android:ellipsize="marquee" android:fadingEdge="horizontal" android:forceHasOverlappingRendering="false" /> <com.android.systemui.recents.views.FixedSizeImageView android:id="@+id/move_task" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:padding="@dimen/recents_task_view_header_button_padding" android:src="@drawable/star" android:background="?android:selectableItemBackground" android:alpha="0" android:visibility="gone" /> <com.android.systemui.recents.views.FixedSizeImageView android:id="@+id/dismiss_task" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:padding="@dimen/recents_task_view_header_button_padding" android:src="@drawable/recents_dismiss_light" android:background="?android:selectableItemBackground" android:alpha="0" android:visibility="gone" /> <!-- The progress indicator shows if auto-paging is enabled --> <ViewStub android:id="@+id/focus_timer_indicator_stub" android:inflatedId="@+id/focus_timer_indicator" android:layout="@layout/recents_task_view_header_progress_bar" android:layout_width="match_parent" android:layout_height="5dp" android:layout_gravity="bottom" /> <!-- The app overlay shows as the user long-presses on the app icon --> <ViewStub android:id="@+id/app_overlay_stub" android:inflatedId="@+id/app_overlay" android:layout="@layout/recents_task_view_header_overlay" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.android.systemui.recents.views.TaskViewHeader>
对应的自定义VIEW控件
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
/* The task bar view */ public class TaskViewHeader extends FrameLayout implements View.OnClickListener, View.OnLongClickListener { @Override protected void onFinishInflate() { SystemServicesProxy ssp = Recents.getSystemServices(); // Initialize the icon and description views mIconView = (ImageView) findViewById(R.id.icon); mIconView.setOnLongClickListener(this); mTitleView = (TextView) findViewById(R.id.title); mDismissButton = (ImageView) findViewById(R.id.dismiss_task); if (ssp.hasFreeformWorkspaceSupport()) { mMoveTaskButton = (ImageView) findViewById(R.id.move_task); } onConfigurationChanged(); } @Override public void onClick(View v) { if (v == mIconView) { //... } else if (v == mMoveTaskButton) { TaskView tv = Utilities.findParent(this, TaskView.class); EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, null, mMoveTaskTargetStackId, false)); } else if (v == mAppInfoView) { //... } } }
重点关注点击的实现的事件
关于LaunchTaskEvent
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskEvent.java
public class LaunchTaskEvent extends EventBus.Event { public final TaskView taskView; public final Task task; public final Rect targetTaskBounds; public final int targetTaskStack; public final boolean screenPinningRequested; public LaunchTaskEvent(TaskView taskView, Task task, Rect targetTaskBounds, int targetTaskStack, boolean screenPinningRequested) { this.taskView = taskView; this.task = task; this.targetTaskBounds = targetTaskBounds; this.targetTaskStack = targetTaskStack; this.screenPinningRequested = screenPinningRequested; } }
检测是否支持自由窗口模式
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
/** Private constructor */ private SystemServicesProxy(Context context) { mAccm = AccessibilityManager.getInstance(context); mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); mIam = ActivityManagerNative.getDefault(); mPm = context.getPackageManager(); mIpm = AppGlobals.getPackageManager(); mAssistUtils = new AssistUtils(context); mWm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mIwm = WindowManagerGlobal.getWindowManagerService(); mUm = UserManager.get(context); mDisplay = mWm.getDefaultDisplay(); mRecentsPackage = context.getPackageName(); mHasFreeformWorkspaceSupport = mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT) || Settings.Global.getInt(context.getContentResolver(), DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; } /** * Returns whether this device has freeform workspaces. */ public boolean hasFreeformWorkspaceSupport() { return mHasFreeformWorkspaceSupport; }
点击后, 加入事件队列
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java
/** * Sends an event to the subscribers of the given event type immediately. This can only be * called from the same thread as the EventBus's looper thread (for the default EventBus, this * is the main application thread). */ public void send(Event event) { // Fail immediately if we are being called from the non-main thread //... queueEvent(event); } /** * Processes and dispatches the given event to the given event handler, on the thread of whoever * calls this method. */ private void processEvent(final EventHandler eventHandler, final Event event) { //...反射调用. eventHandler.method.invoke(sub, event); //... }
eventHandler的由来:
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/events/EventBus.java
private static final String METHOD_PREFIX = "onBusEvent"; public void register(Object subscriber) { registerSubscriber(subscriber, DEFAULT_SUBSCRIBER_PRIORITY, null); } public void register(Object subscriber, int priority) { registerSubscriber(subscriber, priority, null); } /** * Registers a new subscriber. */ private void registerSubscriber(Object subscriber, int priority, MutableBoolean hasInterprocessEventsChangedOut) { //... // Find all the valid event bus handler methods of the subscriber MutableBoolean isInterprocessEvent = new MutableBoolean(false); Method[] methods = subscriberType.getDeclaredMethods(); for (Method m : methods) { Class<?>[] parameterTypes = m.getParameterTypes(); isInterprocessEvent.value = false; if (isValidEventBusHandlerMethod(m, parameterTypes, isInterprocessEvent)) { Class<? extends Event> eventType = (Class<? extends Event>) parameterTypes[0]; ArrayList<EventHandler> eventTypeHandlers = mEventTypeMap.get(eventType); if (eventTypeHandlers == null) { eventTypeHandlers = new ArrayList<>(); mEventTypeMap.put(eventType, eventTypeHandlers); } if (isInterprocessEvent.value) { try { // Enforce that the event must have a Bundle constructor eventType.getConstructor(Bundle.class); mInterprocessEventNameMap.put(eventType.getName(), (Class<? extends InterprocessEvent>) eventType); if (hasInterprocessEventsChangedOut != null) { hasInterprocessEventsChangedOut.value = true; } } catch (NoSuchMethodException e) { throw new RuntimeException("Expected InterprocessEvent to have a Bundle constructor"); } } EventHandlerMethod method = new EventHandlerMethod(m, eventType); EventHandler handler = new EventHandler(sub, method, priority); eventTypeHandlers.add(handler); //保存函数 subscriberMethods.add(method); sortEventHandlersByPriority(eventTypeHandlers); if (DEBUG_TRACE_ALL) { logWithPid(" * Method: " + m.getName() + " event: " + parameterTypes[0].getSimpleName() + " interprocess? " + isInterprocessEvent.value); } } } //... } //检测对应的方法 /** * @return whether {@param method} is a valid (normal or interprocess) event bus handler method */ private boolean isValidEventBusHandlerMethod(Method method, Class<?>[] parameterTypes, MutableBoolean isInterprocessEventOut) { int modifiers = method.getModifiers(); if (Modifier.isPublic(modifiers) && Modifier.isFinal(modifiers) && method.getReturnType().equals(Void.TYPE) && parameterTypes.length == 1) { if (EventBus.InterprocessEvent.class.isAssignableFrom(parameterTypes[0]) && method.getName().startsWith(INTERPROCESS_METHOD_PREFIX)) { isInterprocessEventOut.value = true; return true; } else if (EventBus.Event.class.isAssignableFrom(parameterTypes[0]) && method.getName().startsWith(METHOD_PREFIX)) { isInterprocessEventOut.value = false; return true; } else { if (DEBUG_TRACE_ALL) { if (!EventBus.Event.class.isAssignableFrom(parameterTypes[0])) { logWithPid(" Expected method take an Event-based parameter: " + method.getName()); } else if (!method.getName().startsWith(INTERPROCESS_METHOD_PREFIX) && !method.getName().startsWith(METHOD_PREFIX)) { logWithPid(" Expected method start with method prefix: " + method.getName()); } } } } else { if (DEBUG_TRACE_ALL) { if (!Modifier.isPublic(modifiers)) { logWithPid(" Expected method to be public: " + method.getName()); } else if (!Modifier.isFinal(modifiers)) { logWithPid(" Expected method to be final: " + method.getName()); } else if (!method.getReturnType().equals(Void.TYPE)) { logWithPid(" Expected method to return null: " + method.getName()); } } } return false; }
处理事件, 开始执行切换
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
public final void onBusEvent(LaunchTaskEvent event) { mLastTaskLaunchedWasFreeform = event.task.isFreeformTask(); mTransitionHelper.launchTaskFromRecents(getStack(), event.task, mTaskStackView, event.taskView, event.screenPinningRequested, event.targetTaskBounds, event.targetTaskStack); }
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
/** * Launches the specified {@link Task}. */ 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); } //... if (taskView == null) { // If there is no task view, then we do not need to worry about animating out occluding // task views, and we can launch immediately startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener); } else { LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView, screenPinningRequested); if (task.group != null && !task.group.isFrontMostTask(task)) { launchStartedEvent.addPostAnimationCallback(new Runnable() { @Override public void run() { startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener); } }); EventBus.getDefault().send(launchStartedEvent); } else { EventBus.getDefault().send(launchStartedEvent); startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener); } } Recents.getSystemServices().sendCloseSystemWindows( BaseStatusBar.SYSTEM_DIALOG_REASON_HOME_KEY); } /** * Starts the activity for the launch task. * * @param taskView this is the {@link TaskView} that we are launching from. This can be null if * we are toggling recents and the launch-to task is now offscreen. */ private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView, ActivityOptions opts, IAppTransitionAnimationSpecsFuture transitionFuture, final ActivityOptions.OnAnimationStartedListener animStartedListener) { SystemServicesProxy ssp = Recents.getSystemServices(); if (ssp.startActivityFromRecents(mContext, task.key, task.title, opts)) { // Keep track of the index of the task launch int taskIndexFromFront = 0; int taskIndex = stack.indexOfStackTask(task); if (taskIndex > -1) { taskIndexFromFront = stack.getTaskCount() - taskIndex - 1; } EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront)); } else { // Dismiss the task if we fail to launch it if (taskView != null) { taskView.dismissTask(); } // Keep track of failed launches EventBus.getDefault().send(new LaunchTaskFailedEvent()); } if (transitionFuture != null) { ssp.overridePendingAppTransitionMultiThumbFuture(transitionFuture, wrapStartedListener(animStartedListener), true /* scaleUp */); } }
frameworks/base/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
/** Starts an activity from recents. */ public boolean startActivityFromRecents(Context context, Task.TaskKey taskKey, String taskName, ActivityOptions options) { if (mIam != null) { try { if (taskKey.stackId == DOCKED_STACK_ID) { // We show non-visible docked tasks in Recents, but we always want to launch // them in the fullscreen stack. if (options == null) { options = ActivityOptions.makeBasic(); } options.setLaunchStackId(FULLSCREEN_WORKSPACE_STACK_ID); } mIam.startActivityFromRecents( taskKey.id, options == null ? null : options.toBundle()); return true; } catch (Exception e) { Log.e(TAG, context.getString(R.string.recents_launch_error_message, taskName), e); } } return false; }
进入ActivityManagerService并切换
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public final int startActivityFromRecents(int taskId, Bundle bOptions)
frameworks/base/services/core/java/com/android/server/am/ActivityStackSupervisor.java
final int startActivityFromRecentsInner(int taskId, Bundle bOptions)
解决
原因: 在 RecentsTransitionHelper.java中, 打开任务的参数缺少了ActivityOptions.setLaunchStackId的设置:
//frameworks/base/packages/SystemUI/src/com/android/systemui/recents/views/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(); //----新增代码---- opts.setLaunchStackId(destinationStack); if (bounds != null) { opts.setLaunchBounds(bounds.isEmpty() ? null : bounds); } //... }
编译并更新SystemUI, 完成!
扩展
Android Freeform模式 关键最后一步
How to Enable Freeform Multi-Window Mode in Android Nougat
Android 7.0中的多窗口实现解析
Android N 多窗口功能初探
几个关键变量:
//frameworks/base/core/java/android/app/ActivityManager.java /** 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;