Android T多屏多显——应用双屏间拖拽移动功能(更新中)

功能以及显示效果简介

需求:在双屏显示中,把启动的应用从其中一个屏幕中移动到另一个屏幕中。

操作:通过双指按压应用使其移动,如果移动的距离过小,我们就不移动到另一屏幕,否则移动到另一屏。

功能分析

多屏中移动应用至另一屏本质就是Task的移动。

从窗口层级结构的角度来说,就是把Display1中的DefaultTaskDisplayArea上的Task,移动到Display2中的DefaultTaskDisplayArea上。

容器结构简化树状图如下所示:

窗口层级结构简化树状图如下所示:

关键代码知识点

移动Task至另一屏幕

代码路径:frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java

java 复制代码
    /**
     * Move root task with all its existing content to specified display.
     *
     * @param rootTaskId Id of root task to move.
     * @param displayId  Id of display to move root task to.
     * @param onTop      Indicates whether container should be place on top or on bottom.
     */
    void moveRootTaskToDisplay(int rootTaskId, int displayId, boolean onTop) {
        //根据displayId获取DisplayContent
        final DisplayContent displayContent = getDisplayContentOrCreate(displayId);
        if (displayContent == null) {
            throw new IllegalArgumentException("moveRootTaskToDisplay: Unknown displayId="
                    + displayId);
        }
        //调用moveRootTaskToTaskDisplayArea方法
        moveRootTaskToTaskDisplayArea(rootTaskId, displayContent.getDefaultTaskDisplayArea(),
                onTop);
    }
    

入参说明:
rootTaskId需要移动的Task的Id。可以通过Task中getRootTaskId()方法获取。
displayId需要移动到对应屏幕的Display的Id。可以通过DisplayContent中的getDisplayId()方法获取。
onTop移动后的Task是放在容器顶部还是底部。true表示顶部,false表示底部。

代码解释:

这个方法首先通过getDisplayContentOrCreate方法根据displayId获取DisplayContent,然后调用moveRootTaskToTaskDisplayArea方法进行移动。

其中传递参数displayContent.getDefaultTaskDisplayArea(),表示获取DisplayContent下面的DefaultTaskDisplayArea。

java 复制代码
    /**
     * Move root task with all its existing content to specified task display area.
     *
     * @param rootTaskId      Id of root task to move.
     * @param taskDisplayArea The task display area to move root task to.
     * @param onTop           Indicates whether container should be place on top or on bottom.
     */
    void moveRootTaskToTaskDisplayArea(int rootTaskId, TaskDisplayArea taskDisplayArea,
            boolean onTop) {
        //获取Task
        final Task rootTask = getRootTask(rootTaskId);
        if (rootTask == null) {
            throw new IllegalArgumentException("moveRootTaskToTaskDisplayArea: Unknown rootTaskId="
                    + rootTaskId);
        }

        final TaskDisplayArea currentTaskDisplayArea = rootTask.getDisplayArea();
        if (currentTaskDisplayArea == null) {
            throw new IllegalStateException("moveRootTaskToTaskDisplayArea: rootTask=" + rootTask
                    + " is not attached to any task display area.");
        }

        if (taskDisplayArea == null) {
            throw new IllegalArgumentException(
                    "moveRootTaskToTaskDisplayArea: Unknown taskDisplayArea=" + taskDisplayArea);
        }

        if (currentTaskDisplayArea == taskDisplayArea) {
            throw new IllegalArgumentException("Trying to move rootTask=" + rootTask
                    + " to its current taskDisplayArea=" + taskDisplayArea);
        }
        //把获取到的task重新挂载到了新display的taskDisplayArea
        rootTask.reparent(taskDisplayArea, onTop);

        // Resume focusable root task after reparenting to another display area.
        //窗口或任务reparent之后,恢复焦点,激活相关任务的活动,并更新活动的可见性,以确保窗口管理器和用户界面的状态一致和正确。
        rootTask.resumeNextFocusAfterReparent();

        // TODO(multi-display): resize rootTasks properly if moved from split-screen.
    }

根据前面传递的TaskId获取到Task,在通过rootTask.reparent(taskDisplayArea, onTop);方法,把这个Task重新挂载到了新display的taskDisplayArea上。然后使用rootTask.resumeNextFocusAfterReparent();方法更新窗口焦点显示。

  • rootTask.reparent(taskDisplayArea, onTop);

    代码路径:frameworks/base/services/core/java/com/android/server/wm/Task.java

    java 复制代码
     void reparent(TaskDisplayArea newParent, boolean onTop) {
            if (newParent == null) {
                throw new IllegalArgumentException("Task can't reparent to null " + this);
            }
    
            if (getParent() == newParent) {
                throw new IllegalArgumentException("Task=" + this + " already child of " + newParent);
            }
    
            //通过调用 canBeLaunchedOnDisplay 方法检查任务是否可以在新父区域所在的显示设备上启动。
            if (canBeLaunchedOnDisplay(newParent.getDisplayId())) {
                //实际执行reparent的操作。
                reparent(newParent, onTop ? POSITION_TOP : POSITION_BOTTOM);
                //如果Task是一个叶子Task(即没有子Task的Task)
                if (isLeafTask()) {
                    //调用新父区域的 onLeafTaskMoved 方法来通知新父区域叶子Task已经移动。
                    newParent.onLeafTaskMoved(this, onTop);
                }
            } else {
                Slog.w(TAG, "Task=" + this + " can't reparent to " + newParent);
            }
        }

    其中reparent(newParent, onTop ? POSITION_TOP : POSITION_BOTTOM);实际执行reparent的操作。这里根据 onTop 的值来决定任务应该被放置在新父区域的顶部还是底部。我们再看看这方法的具体实现。

    代码路径:frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java

    java 复制代码
    void reparent(WindowContainer newParent, int position) {
            if (newParent == null) {
                throw new IllegalArgumentException("reparent: can't reparent to null " + this);
            }
    
            if (newParent == this) {
                throw new IllegalArgumentException("Can not reparent to itself " + this);
            }
    
            final WindowContainer oldParent = mParent;
            if (mParent == newParent) {
                throw new IllegalArgumentException("WC=" + this + " already child of " + mParent);
            }
    
            // Collect before removing child from old parent, because the old parent may be removed if
            // this is the last child in it.
            //记录reparent的容器(this)相关信息,这里的this指的是移动的Task,newParent是新的TaskDisplayArea
            mTransitionController.collectReparentChange(this, newParent);
    
            // The display object before reparenting as that might lead to old parent getting removed
            // from the display if it no longer has any child.
            //获取之前的DisplayContent和新的DisplayContent
            final DisplayContent prevDc = oldParent.getDisplayContent();
            final DisplayContent dc = newParent.getDisplayContent();
    
            //设置 mReparenting 为 true,表示正在执行reparent操作。
            //然后从旧父容器中移除当前容器,并将其添加到新父容器的指定位置。
            //最后,将 mReparenting 设置为 false,表示reparent操作完成。
            mReparenting = true;
            oldParent.removeChild(this);
            newParent.addChild(this, position);
            mReparenting = false;
    
            // Relayout display(s)
            //标记新父容器对应的显示内容为需要布局。
            //如果新父容器和旧父容器的显示内容不同,
            //则触发显示内容改变的通知,并标记旧显示内容也需要布局。
            //最后,调用layoutAndAssignWindowLayersIfNeeded方法确保显示内容按需进行布局和窗口层级的分配。
            dc.setLayoutNeeded();
            if (prevDc != dc) {
                onDisplayChanged(dc);
                prevDc.setLayoutNeeded();
            }
            getDisplayContent().layoutAndAssignWindowLayersIfNeeded();
    
            // Send onParentChanged notification here is we disabled sending it in setParent for
            // reparenting case.
            //处理窗口容器在父容器变更时的各种逻辑
            onParentChanged(newParent, oldParent);
            //处理窗口容器在不同父容器之间同步迁移的逻辑
            onSyncReparent(oldParent, newParent);
        }
  • rootTask.resumeNextFocusAfterReparent();

    代码路径:frameworks/base/services/core/java/com/android/server/wm/Task.java

    java 复制代码
        void resumeNextFocusAfterReparent() {
            //调整焦点
            adjustFocusToNextFocusableTask("reparent", true /* allowFocusSelf */,
                    true /* moveDisplayToTop */);
            //恢复当前焦点任务的顶部活动
            mRootWindowContainer.resumeFocusedTasksTopActivities();
            // Update visibility of activities before notifying WM. This way it won't try to resize
            // windows that are no longer visible.
            //更新activities的可见性
            mRootWindowContainer.ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
                    !PRESERVE_WINDOWS);
        }

更新activity可见性和配置

代码路径:frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java

java 复制代码
    /**
     * Make sure that all activities that need to be visible in the system actually are and update
     * their configuration.
     */
    void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
            boolean preserveWindows) {
        ensureActivitiesVisible(starting, configChanges, preserveWindows, true /* notifyClients */);
    }

    /**
     * @see #ensureActivitiesVisible(ActivityRecord, int, boolean)
     */
    void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
            boolean preserveWindows, boolean notifyClients) {
        //检查mTaskSupervisor是否正在进行活动可见性更新或是否延迟了根可见性更新
        if (mTaskSupervisor.inActivityVisibilityUpdate()
                || mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
            // Don't do recursive work.
            return;
        }

        try {
            //开始更新
            mTaskSupervisor.beginActivityVisibilityUpdate();
            // First the front root tasks. In case any are not fullscreen and are in front of home.
            //遍历每个DisplayContent对象
            for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
                final DisplayContent display = getChildAt(displayNdx);
                //对于每个DisplayContent对象,调用其ensureActivitiesVisible方法来确保该显示内容上的活动可见并更新其配置。
                display.ensureActivitiesVisible(starting, configChanges, preserveWindows,
                        notifyClients);
            }
        } finally {
            //结束更新
            mTaskSupervisor.endActivityVisibilityUpdate();
        }
    }

starting指的是Task 中最顶端的activity,保证的正是这个activity在启动或者resume时的可见性。
configChanges评估是否被冻结的activity改变部分配置。
preserveWindows一个标志位,更新时是否保留窗口。
notifyClients一个标志位,把配置和可见性的变化通知客户端,当前固定值为true

这个方法的主要作用是确保所有需要显示的活动确实在系统中可见,并更新它们的配置。

这里的display.ensureActivitiesVisible(starting, configChanges, preserveWindows,notifyClients);是更新的核心方法,其最终会调用到EnsureActivitiesVisibleHelper中的process方法。

获取WindowedMagnification层级

代码路径:frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java

java 复制代码
    /**
     * The direct child layer of the display to put all non-overlay windows. This is also used for
     * screen rotation animation so that there is a parent layer to put the animation leash.
     */
    private SurfaceControl mWindowingLayer;
    
    SurfaceControl getWindowingLayer() {
        return mWindowingLayer;
    }

mWindowingLayer在DisplayContent的configureSurfaces方法中有进行赋值。

java 复制代码
    /**
     * Configures the surfaces hierarchy for DisplayContent
     * This method always recreates the main surface control but reparents the children
     * if they are already created.
     *
     * @param transaction as part of which to perform the configuration
     */
    private void configureSurfaces(Transaction transaction) {
        final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
                .setOpaque(true)
                .setContainerLayer()
                .setCallsite("DisplayContent");
        mSurfaceControl = b.setName(getName()).setContainerLayer().build();

        ......
        final List<DisplayArea<? extends WindowContainer>> areas =
                mDisplayAreaPolicy.getDisplayAreas(FEATURE_WINDOWED_MAGNIFICATION);
        final DisplayArea<?> area = areas.size() == 1 ? areas.get(0) : null;

        if (area != null && area.getParent() == this) {
            // The windowed magnification area should contain all non-overlay windows, so just use
            // it as the windowing layer.
            mWindowingLayer = area.mSurfaceControl;
            transaction.reparent(mWindowingLayer, mSurfaceControl);
        } else {
            ......
        }
        ......
    }

从代码中我们可以看出mWindowingLayer = area.mSurfaceControl,实际上就是FEATURE_WINDOWED_MAGNIFICATION对应的图层,即WindowedMagnification:0:31

镜像图层

代码路径:frameworks/base/core/java/android/view/SurfaceControl.java

java 复制代码
    /**
     * Creates a mirrored hierarchy for the mirrorOf {@link SurfaceControl}
     *
     * Real Hierarchy    Mirror
     *                     SC (value that's returned)
     *                      |
     *      A               A'
     *      |               |
     *      B               B'
     *
     * @param mirrorOf The root of the hierarchy that should be mirrored.
     * @return A SurfaceControl that's the parent of the root of the mirrored hierarchy.
     *
     * @hide
     */
    public static SurfaceControl mirrorSurface(SurfaceControl mirrorOf) {
        long nativeObj = nativeMirrorSurface(mirrorOf.mNativeObject);
        SurfaceControl sc = new SurfaceControl();
        sc.assignNativeObject(nativeObj, "mirrorSurface");
        return sc;
    }

把复制一个一模一样的图层,作为镜像图层。这个复制会把该图层下的所有子节点一起复制,其图层的根节点一般叫做MirrorRoot

例如 :SurfaceControl.mirrorSurface(rootTask.getSurfaceControl());

复制rootTask的图层以及其以后得节点作为镜像。

如图所示:

注:真实图层(被复制的图层mirrorOf)的坐标变化会影响镜像图层的坐标变化。

保证activity显示在最底层

代码路径:frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java

java 复制代码
    /**
     * True if this an AppWindowToken and the activity which created this was launched with
     * ActivityOptions.setLaunchTaskBehind.
     * <p>
     * TODO(b/142617871): We run a special animation when the activity was launched with that
     * flag, but it's not necessary anymore. Keep the window invisible until the task is explicitly
     * selected to suppress an animation, and remove this flag.
     */
    boolean mLaunchTaskBehind;

mLaunchTaskBehindtrue则表示当前activity显示在最下方。例如,桌面就是一直显示最下方的activity。

调用方式:ActivityRecord对象.mLaunchTaskBehind = true;

ValueAnimator的使用

java 复制代码
ValueAnimator anim = ValueAnimator.ofInt(0, 200);
anim.setDuration(3000);

anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        int currentValue = (int) animation.getAnimatedValue();
        Slog.i("TAG", "onAnimationUpdate current value is " + currentValue);
    }
});

valueAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        Slog.i("TAG", "onAnimationEnd");
});
    
anim.start();

onAnimationUpdate是动画在更新时的监听,从上面的例子上可以看出,是在3秒内平滑打印0~200之间的整数。
onAnimationEnd是动画播放结束后的监听,在结束时的操作一般放在这里面。

相关推荐
拭心11 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王13 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡13 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道14 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库14 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道15 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe15 小时前
Android Hook - 动态加载so库
android
居居飒16 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He19 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗19 小时前
Android笔试面试题AI答之Android基础(1)
android