Android 7.1 FreeForm 多窗口模式

平台

RK3288 + Android 7.1

关于Freeform

Android N上的多窗口功能有三种模式:(扩展-4)

  • 分屏模式
    这种模式可以在手机上使用。该模式将屏幕一分为二,同时显示两个应用的界面。
  • 画中画模式
    这种模式主要在TV上使用,在该模式下视频播放的窗口可以一直在最顶层显示。
  • Freeform模式
    这种模式类似于我们常见的桌面操作系统,应用界面的窗口可以自由拖动和修改大小。

效果图

Android 7.1 FreeForm 多窗口模式_第1张图片

切入

Android 7.1 FreeForm 多窗口模式_第2张图片
平台默认并没有打开这个模式的支持, 需要增加一个文件以打开Feeeform特性
增加 /system/etc/permissions/android.software.freeform_window_management.xml




<permissions>
    <feature name="android.software.freeform_window_management" />
permissions>

打开后:
Android 7.1 FreeForm 多窗口模式_第3张图片
看任务右上角 X 旁边的图标
然而, 当尝试点击此按键后, 预想的画面并没有出现, 费解!

排查

跟踪下源码中此界面的布局:

  • frameworks/base/packages/SystemUI/res/layout/recents_task_view_header.xml



<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" />

    
    <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" />

    
    <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, 完成!

扩展

  1. Android Freeform模式 关键最后一步
  2. How to Enable Freeform Multi-Window Mode in Android Nougat
  3. Android 7.0中的多窗口实现解析
  4. Android N 多窗口功能初探
  5. 几个关键变量:
//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;

你可能感兴趣的:(Android 7.1 FreeForm 多窗口模式)