Android 7.1 FreeForm 多窗口模式

平臺

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

擴展

  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;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章