Android 7.1 FreeForm 多窗口模式

简介: Android 7.1 FreeForm 多窗口模式

平台


RK3288 + Android 7.1


关于Freeform


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


分屏模式

这种模式可以在手机上使用。该模式将屏幕一分为二,同时显示两个应用的界面。

画中画模式

这种模式主要在TV上使用,在该模式下视频播放的窗口可以一直在最顶层显示。

Freeform模式

这种模式类似于我们常见的桌面操作系统,应用界面的窗口可以自由拖动和修改大小。


效果图


image.png


切入


image.png

平台默认并没有打开这个模式的支持, 需要增加一个文件以打开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>


打开后:

image.png

看任务右上角 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;
相关文章
|
7月前
|
Android开发
Android Mediatek 增加Recovery模式下读cmdline的强制工厂重置选项
Android Mediatek 增加Recovery模式下读cmdline的强制工厂重置选项
66 0
|
7月前
|
XML 前端开发 测试技术
Android基础知识:解释Android的MVC和MVP模式。
Android基础知识:解释Android的MVC和MVP模式。
71 0
|
4月前
|
Android开发
Android Stadio Build 窗口字符串乱码问题
在使用Android Studio过程中,如果遇到Build窗口字符串乱码问题,可以通过编辑`studio.vmoptions`文件添加`-Dfile.encoding=UTF-8`配置并重启Android Studio来解决。
187 1
Android Stadio Build 窗口字符串乱码问题
|
4月前
|
设计模式 Android开发 Kotlin
Android经典实战之Kotlin委托模式和by关键字
本文介绍了Kotlin中`by`关键字在类及属性委托中的运用,通过实例展示了如何利用类委托简化接口实现,以及如何借助标准与自定义属性委托管理属性的读写操作。通过`by`关键字的支持,Kotlin使得委托模式的实现更为直观且高效。
99 4
|
3月前
|
API Android开发 数据安全/隐私保护
Android经典实战之窗口和WindowManager
本文介绍了Android开发中“窗口”的基本概念及其重要性。窗口是承载用户界面的基础单位,而`WindowManager`系统服务则负责窗口的创建、更新和移除等操作。了解这些概念有助于开发复杂且用户体验良好的应用。
72 2
|
5月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
207 2
|
6月前
|
Android开发 开发者
Android UI设计中,Theme定义了Activity的视觉风格,包括颜色、字体、窗口样式等,定义在`styles.xml`。
【6月更文挑战第26天】Android UI设计中,Theme定义了Activity的视觉风格,包括颜色、字体、窗口样式等,定义在`styles.xml`。要更改主题,首先在该文件中创建新主题,如`MyAppTheme`,覆盖所需属性。然后,在`AndroidManifest.xml`中应用主题至应用或特定Activity。运行时切换主题可通过重新设置并重启Activity实现,或使用`setTheme`和`recreate()`方法。这允许开发者定制界面并与品牌指南匹配,或提供多主题选项。
98 6
|
6月前
|
Android开发 开发者
Android UI中的Theme定义了Activity的视觉风格,包括颜色、字体、窗口样式等。要更改主题
【6月更文挑战第25天】Android UI中的Theme定义了Activity的视觉风格,包括颜色、字体、窗口样式等。要更改主题,首先在`styles.xml`中定义新主题,如`MyAppTheme`,然后在`AndroidManifest.xml`中设置`android:theme`。可应用于全局或特定Activity。运行时切换主题需重置Activity,如通过`setTheme()`和`recreate()`方法。这允许开发者定制界面以匹配品牌或用户偏好。
63 2
|
7月前
|
传感器 小程序 Java
Java+saas模式 智慧校园系统源码Java Android +MySQL+ IDEA 多校运营数字化校园云平台源码
Java+saas模式 智慧校园系统源码Java Android +MySQL+ IDEA 多校运营数字化校园云平台源码 智慧校园即智慧化的校园,也指按智慧化标准进行的校园建设,按标准《智慧校园总体框架》中对智慧校园的标准定义是:物理空间和信息空间的有机衔接,使任何人、任何时间、任何地点都能便捷的获取资源和服务。
81 1
|
7月前
|
设计模式 前端开发 数据库
构建高效Android应用:使用Jetpack架构组件实现MVVM模式
【4月更文挑战第21天】 在移动开发领域,构建一个既健壮又易于维护的Android应用是每个开发者的目标。随着项目复杂度的增加,传统的MVP或MVC架构往往难以应对快速变化的市场需求和复杂的业务逻辑。本文将探讨如何利用Android Jetpack中的架构组件来实施MVVM(Model-View-ViewModel)设计模式,旨在提供一个更加模块化、可测试且易于管理的代码结构。通过具体案例分析,我们将展示如何使用LiveData, ViewModel, 和Repository来实现界面与业务逻辑的分离,以及如何利用Room数据库进行持久化存储。最终,你将获得一个响应迅速、可扩展且符合现代软件工
103 0