Android T多屏多显——应用双屏间拖拽移动功能

打印 上一主题 下一主题

主题 528|帖子 528|积分 1584

功能以及表现结果简介

需求:在双屏表现中,把启动的应用从其中一个屏幕中移动到另一个屏幕中。
操纵:通过双指按压应用使其移动,如果移动的距离过小,我们就不移动到另一屏幕,否则移动到另一屏。

功能分析

多屏中移动应用至另一屏本质就是Task的移动。
从窗口层级结构的角度来说,就是把Display1中的DefaultTaskDisplayArea上的Task,移动到Display2中的DefaultTaskDisplayArea上。
容器结构简化树状图如下所示:

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

动画分析

动画图层节点


这里以从左往右移动为例,通过DisplayContent的getWindowingLayer()方法获取WindowedMagnification节点,在该节点下创建copyTaskSc节点用于动画过渡。从右往左同理。
图层移动偏移量计算

当移动的偏移量(offsetX)大于肯定的数值时向对侧移动,否则回到之前的屏幕。
为保证两屏表现过渡平滑,需要使用到镜像图层。我们拖动那个图层是镜像图层,另一屏表现的为真实图层。需要注意的是,如果真实图层(被复制的图层)发生变化(坐标、缩放等)会导致镜像图层跟着变化(坐标、缩放等)

如上图所示:图层向右移动时,镜像图层偏移量为offsetX,真实图层的偏移量为-(width-offsetX)。由于我们真实的图层发生了变化,因此当我们镜像图层偏移量为offsetX时,实在际偏移量为镜像图层偏移量加上真实图层的偏移量,即offsetX - (width-offsetX)。以是为保证镜像图层的偏移量为offsetX,需要进行额外的偏移,即offsetX - (width-offsetX) + 额外偏移量 = offsetX 。这里我们可以算出额外偏移量就是width-offsetX,最终我们得出镜像图层的实际偏移量为offsetX + (width-offsetX)。
结论:镜像图层偏移量为offsetX + (width-offsetX),真实图层的偏移量为-(width-offsetX)。

如上图所示:向右偏移时同理,镜像图层偏移量为-offsetX,真实图层的偏移量为width-offsetX。以是经过变化后的镜像图层的实际偏移量为-offset - (width-offsetX)。
关键代码知识点

移动Task至另一屏幕

代码路径:frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
  1.     /**
  2.      * Move root task with all its existing content to specified display.
  3.      *
  4.      * @param rootTaskId Id of root task to move.
  5.      * @param displayId  Id of display to move root task to.
  6.      * @param onTop      Indicates whether container should be place on top or on bottom.
  7.      */
  8.     void moveRootTaskToDisplay(int rootTaskId, int displayId, boolean onTop) {
  9.         //根据displayId获取DisplayContent
  10.         final DisplayContent displayContent = getDisplayContentOrCreate(displayId);
  11.         if (displayContent == null) {
  12.             throw new IllegalArgumentException("moveRootTaskToDisplay: Unknown displayId="
  13.                     + displayId);
  14.         }
  15.         //调用moveRootTaskToTaskDisplayArea方法
  16.         moveRootTaskToTaskDisplayArea(rootTaskId, displayContent.getDefaultTaskDisplayArea(),
  17.                 onTop);
  18.     }
  19.    
复制代码
入参阐明:
rootTaskId需要移动的Task的Id。可以通过Task中getRootTaskId()方法获取。
displayId需要移动到对应屏幕的Display的Id。可以通过DisplayContent中的getDisplayId()方法获取。
onTop移动后的Task是放在容器顶部还是底部。true表现顶部,false表现底部。
代码表明:
这个方法首先通过getDisplayContentOrCreate方法根据displayId获取DisplayContent,然后调用moveRootTaskToTaskDisplayArea方法进行移动。
其中传递参数displayContent.getDefaultTaskDisplayArea(),表现获取DisplayContent下面的DefaultTaskDisplayArea。
  1.     /**
  2.      * Move root task with all its existing content to specified task display area.
  3.      *
  4.      * @param rootTaskId      Id of root task to move.
  5.      * @param taskDisplayArea The task display area to move root task to.
  6.      * @param onTop           Indicates whether container should be place on top or on bottom.
  7.      */
  8.     void moveRootTaskToTaskDisplayArea(int rootTaskId, TaskDisplayArea taskDisplayArea,
  9.             boolean onTop) {
  10.         //获取Task
  11.         final Task rootTask = getRootTask(rootTaskId);
  12.         if (rootTask == null) {
  13.             throw new IllegalArgumentException("moveRootTaskToTaskDisplayArea: Unknown rootTaskId="
  14.                     + rootTaskId);
  15.         }
  16.         final TaskDisplayArea currentTaskDisplayArea = rootTask.getDisplayArea();
  17.         if (currentTaskDisplayArea == null) {
  18.             throw new IllegalStateException("moveRootTaskToTaskDisplayArea: rootTask=" + rootTask
  19.                     + " is not attached to any task display area.");
  20.         }
  21.         if (taskDisplayArea == null) {
  22.             throw new IllegalArgumentException(
  23.                     "moveRootTaskToTaskDisplayArea: Unknown taskDisplayArea=" + taskDisplayArea);
  24.         }
  25.         if (currentTaskDisplayArea == taskDisplayArea) {
  26.             throw new IllegalArgumentException("Trying to move rootTask=" + rootTask
  27.                     + " to its current taskDisplayArea=" + taskDisplayArea);
  28.         }
  29.         //把获取到的task重新挂载到了新display的taskDisplayArea
  30.         rootTask.reparent(taskDisplayArea, onTop);
  31.         // Resume focusable root task after reparenting to another display area.
  32.         //窗口或任务reparent之后,恢复焦点,激活相关任务的活动,并更新活动的可见性,以确保窗口管理器和用户界面的状态一致和正确。
  33.         rootTask.resumeNextFocusAfterReparent();
  34.         // TODO(multi-display): resize rootTasks properly if moved from split-screen.
  35.     }
复制代码
根据前面传递的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
    1. void reparent(TaskDisplayArea newParent, boolean onTop) {
    2.         if (newParent == null) {
    3.             throw new IllegalArgumentException("Task can't reparent to null " + this);
    4.         }
    5.         if (getParent() == newParent) {
    6.             throw new IllegalArgumentException("Task=" + this + " already child of " + newParent);
    7.         }
    8.         //通过调用 canBeLaunchedOnDisplay 方法检查任务是否可以在新父区域所在的显示设备上启动。
    9.         if (canBeLaunchedOnDisplay(newParent.getDisplayId())) {
    10.             //实际执行reparent的操作。
    11.             reparent(newParent, onTop ? POSITION_TOP : POSITION_BOTTOM);
    12.             //如果Task是一个叶子Task(即没有子Task的Task)
    13.             if (isLeafTask()) {
    14.                 //调用新父区域的 onLeafTaskMoved 方法来通知新父区域叶子Task已经移动。
    15.                 newParent.onLeafTaskMoved(this, onTop);
    16.             }
    17.         } else {
    18.             Slog.w(TAG, "Task=" + this + " can't reparent to " + newParent);
    19.         }
    20.     }
    复制代码
    其中reparent(newParent, onTop ? POSITION_TOP : POSITION_BOTTOM);实际实行reparent的操纵。这里根据 onTop 的值来决定使命应该被放置在新父区域的顶部还是底部。我们再看看这方法的具体实现。
    代码路径:frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java
    1. void reparent(WindowContainer newParent, int position) {
    2.         if (newParent == null) {
    3.             throw new IllegalArgumentException("reparent: can't reparent to null " + this);
    4.         }
    5.         if (newParent == this) {
    6.             throw new IllegalArgumentException("Can not reparent to itself " + this);
    7.         }
    8.         final WindowContainer oldParent = mParent;
    9.         if (mParent == newParent) {
    10.             throw new IllegalArgumentException("WC=" + this + " already child of " + mParent);
    11.         }
    12.         // Collect before removing child from old parent, because the old parent may be removed if
    13.         // this is the last child in it.
    14.         //记录reparent的容器(this)相关信息,这里的this指的是移动的Task,newParent是新的TaskDisplayArea
    15.         mTransitionController.collectReparentChange(this, newParent);
    16.         // The display object before reparenting as that might lead to old parent getting removed
    17.         // from the display if it no longer has any child.
    18.         //获取之前的DisplayContent和新的DisplayContent
    19.         final DisplayContent prevDc = oldParent.getDisplayContent();
    20.         final DisplayContent dc = newParent.getDisplayContent();
    21.         //设置 mReparenting 为 true,表示正在执行reparent操作。
    22.         //然后从旧父容器中移除当前容器,并将其添加到新父容器的指定位置。
    23.         //最后,将 mReparenting 设置为 false,表示reparent操作完成。
    24.         mReparenting = true;
    25.         oldParent.removeChild(this);
    26.         newParent.addChild(this, position);
    27.         mReparenting = false;
    28.         // Relayout display(s)
    29.         //标记新父容器对应的显示内容为需要布局。
    30.         //如果新父容器和旧父容器的显示内容不同,
    31.         //则触发显示内容改变的通知,并标记旧显示内容也需要布局。
    32.         //最后,调用layoutAndAssignWindowLayersIfNeeded方法确保显示内容按需进行布局和窗口层级的分配。
    33.         dc.setLayoutNeeded();
    34.         if (prevDc != dc) {
    35.             onDisplayChanged(dc);
    36.             prevDc.setLayoutNeeded();
    37.         }
    38.         getDisplayContent().layoutAndAssignWindowLayersIfNeeded();
    39.         // Send onParentChanged notification here is we disabled sending it in setParent for
    40.         // reparenting case.
    41.         //处理窗口容器在父容器变更时的各种逻辑
    42.         onParentChanged(newParent, oldParent);
    43.         //处理窗口容器在不同父容器之间同步迁移的逻辑
    44.         onSyncReparent(oldParent, newParent);
    45.     }
    复制代码
  • rootTask.resumeNextFocusAfterReparent();
    代码路径:frameworks/base/services/core/java/com/android/server/wm/Task.java
    1.     void resumeNextFocusAfterReparent() {
    2.         //调整焦点
    3.         adjustFocusToNextFocusableTask("reparent", true /* allowFocusSelf */,
    4.                 true /* moveDisplayToTop */);
    5.         //恢复当前焦点任务的顶部活动
    6.         mRootWindowContainer.resumeFocusedTasksTopActivities();
    7.         // Update visibility of activities before notifying WM. This way it won't try to resize
    8.         // windows that are no longer visible.
    9.         //更新activities的可见性
    10.         mRootWindowContainer.ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
    11.                 !PRESERVE_WINDOWS);
    12.     }
    复制代码
更新activity可见性和配置

代码路径:frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
  1.     /**
  2.      * Make sure that all activities that need to be visible in the system actually are and update
  3.      * their configuration.
  4.      */
  5.     void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
  6.             boolean preserveWindows) {
  7.         ensureActivitiesVisible(starting, configChanges, preserveWindows, true /* notifyClients */);
  8.     }
  9.     /**
  10.      * @see #ensureActivitiesVisible(ActivityRecord, int, boolean)
  11.      */
  12.     void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
  13.             boolean preserveWindows, boolean notifyClients) {
  14.         //检查mTaskSupervisor是否正在进行活动可见性更新或是否延迟了根可见性更新
  15.         if (mTaskSupervisor.inActivityVisibilityUpdate()
  16.                 || mTaskSupervisor.isRootVisibilityUpdateDeferred()) {
  17.             // Don't do recursive work.
  18.             return;
  19.         }
  20.         try {
  21.             //开始更新
  22.             mTaskSupervisor.beginActivityVisibilityUpdate();
  23.             // First the front root tasks. In case any are not fullscreen and are in front of home.
  24.             //遍历每个DisplayContent对象
  25.             for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
  26.                 final DisplayContent display = getChildAt(displayNdx);
  27.                 //对于每个DisplayContent对象,调用其ensureActivitiesVisible方法来确保该显示内容上的活动可见并更新其配置。
  28.                 display.ensureActivitiesVisible(starting, configChanges, preserveWindows,
  29.                         notifyClients);
  30.             }
  31.         } finally {
  32.             //结束更新
  33.             mTaskSupervisor.endActivityVisibilityUpdate();
  34.         }
  35.     }
复制代码
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
  1.     /**
  2.      * The direct child layer of the display to put all non-overlay windows. This is also used for
  3.      * screen rotation animation so that there is a parent layer to put the animation leash.
  4.      */
  5.     private SurfaceControl mWindowingLayer;
  6.    
  7.     SurfaceControl getWindowingLayer() {
  8.         return mWindowingLayer;
  9.     }
复制代码
mWindowingLayer在DisplayContent的configureSurfaces方法中有进行赋值。
  1.     /**
  2.      * Configures the surfaces hierarchy for DisplayContent
  3.      * This method always recreates the main surface control but reparents the children
  4.      * if they are already created.
  5.      *
  6.      * @param transaction as part of which to perform the configuration
  7.      */
  8.     private void configureSurfaces(Transaction transaction) {
  9.         final SurfaceControl.Builder b = mWmService.makeSurfaceBuilder(mSession)
  10.                 .setOpaque(true)
  11.                 .setContainerLayer()
  12.                 .setCallsite("DisplayContent");
  13.         mSurfaceControl = b.setName(getName()).setContainerLayer().build();
  14.         ......
  15.         final List<DisplayArea<? extends WindowContainer>> areas =
  16.                 mDisplayAreaPolicy.getDisplayAreas(FEATURE_WINDOWED_MAGNIFICATION);
  17.         final DisplayArea<?> area = areas.size() == 1 ? areas.get(0) : null;
  18.         if (area != null && area.getParent() == this) {
  19.             // The windowed magnification area should contain all non-overlay windows, so just use
  20.             // it as the windowing layer.
  21.             mWindowingLayer = area.mSurfaceControl;
  22.             transaction.reparent(mWindowingLayer, mSurfaceControl);
  23.         } else {
  24.             ......
  25.         }
  26.         ......
  27.     }
复制代码
从代码中我们可以看出mWindowingLayer = area.mSurfaceControl,实际上就是FEATURE_WINDOWED_MAGNIFICATION对应的图层,即WindowedMagnification:0:31。
镜像图层

代码路径:frameworks/base/core/java/android/view/SurfaceControl.java
  1.     /**
  2.      * Creates a mirrored hierarchy for the mirrorOf {@link SurfaceControl}
  3.      *
  4.      * Real Hierarchy    Mirror
  5.      *                     SC (value that's returned)
  6.      *                      |
  7.      *      A               A'
  8.      *      |               |
  9.      *      B               B'
  10.      *
  11.      * @param mirrorOf The root of the hierarchy that should be mirrored.
  12.      * @return A SurfaceControl that's the parent of the root of the mirrored hierarchy.
  13.      *
  14.      * @hide
  15.      */
  16.     public static SurfaceControl mirrorSurface(SurfaceControl mirrorOf) {
  17.         long nativeObj = nativeMirrorSurface(mirrorOf.mNativeObject);
  18.         SurfaceControl sc = new SurfaceControl();
  19.         sc.assignNativeObject(nativeObj, "mirrorSurface");
  20.         return sc;
  21.     }
复制代码
把复制一个千篇一律的图层,作为镜像图层,而且该图层会随着原图层的变化而变化。这个复制会把该图层下的所有子节点一起复制,其图层的根节点一般叫做MirrorRoot
例如 :SurfaceControl.mirrorSurface(rootTask.getSurfaceControl());
复制rootTask的图层以及其以后得节点作为镜像。
如图所示:

注意:真实图层(被复制的图层mirrorOf)的变化(坐标、缩放等)会导致镜像图层跟着变化(坐标、缩放等)。
保证底部的activity表现

代码路径:frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java
  1.     /**
  2.      * True if this an AppWindowToken and the activity which created this was launched with
  3.      * ActivityOptions.setLaunchTaskBehind.
  4.      * <p>
  5.      * TODO(b/142617871): We run a special animation when the activity was launched with that
  6.      * flag, but it's not necessary anymore. Keep the window invisible until the task is explicitly
  7.      * selected to suppress an animation, and remove this flag.
  8.      */
  9.     boolean mLaunchTaskBehind;
复制代码
mLaunchTaskBehind为true则表现当前允许activity表现在最下方。例如,桌面就是一直表现最下方的activity。
调用方式:ActivityRecord对象.mLaunchTaskBehind = true;
在双屏拖拽时大概会出现另一屏桌面大概其他顶层Activity界面黑屏的征象,因此需要通过该配置使其保持表现。
ValueAnimator的使用

  1. ValueAnimator anim = ValueAnimator.ofInt(0, 200);
  2. anim.setDuration(3000);
  3. anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  4.     @Override
  5.     public void onAnimationUpdate(ValueAnimator animation) {
  6.         int currentValue = (int) animation.getAnimatedValue();
  7.         Slog.i("TAG", "onAnimationUpdate current value is " + currentValue);
  8.     }
  9. });
  10. valueAnimator.addListener(new AnimatorListenerAdapter() {
  11.     @Override
  12.     public void onAnimationEnd(Animator animation) {
  13.         super.onAnimationEnd(animation);
  14.         Slog.i("TAG", "onAnimationEnd");
  15. });
  16.    
  17. anim.start();
复制代码
onAnimationUpdate是动画在更新时的监听,从上面的例子上可以看出,是在3秒内平滑打印0~200之间的整数。
onAnimationEnd是动画播放结束后的监听,在结束时的操纵一般放在这里面。
全局触摸接口

功能接口

代码路径:frameworks/base/core/java/android/view/WindowManagerPolicyConstants.java
  1.     interface PointerEventListener {
  2.         /**
  3.          * 1. onPointerEvent will be called on the service.UiThread.
  4.          * 2. motionEvent will be recycled after onPointerEvent returns so if it is needed later a
  5.          * copy() must be made and the copy must be recycled.
  6.          **/
  7.         void onPointerEvent(MotionEvent motionEvent);
  8.     }
复制代码
我们触摸相关的操纵调用此接口,然后实现。
监听接口

frameworks/base/services/core/java/com/android/server/policy/WindowManagerPolicy.java
  1.     public interface WindowManagerFuncs {
  2.         /** Register a system listener for touch events */
  3.         void registerPointerEventListener(PointerEventListener listener, int displayId);
  4.         /** Unregister a system listener for touch events */
  5.         void unregisterPointerEventListener(PointerEventListener listener, int displayId);
  6.     }
复制代码
监听接口AOSP内部有实现,我们根据需要调用对应的监听方式接口。


  • WindowManagerService中重写监听方法
    代码路径:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
    1.     @Override
    2.     public void registerPointerEventListener(PointerEventListener listener, int displayId) {
    3.         synchronized (mGlobalLock) {
    4.             final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
    5.             if (displayContent != null) {
    6.                 displayContent.registerPointerEventListener(listener);
    7.             }
    8.         }
    9.     }
    10.     @Override
    11.     public void unregisterPointerEventListener(PointerEventListener listener, int displayId) {
    12.         synchronized (mGlobalLock) {
    13.             final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
    14.             if (displayContent != null) {
    15.                 displayContent.unregisterPointerEventListener(listener);
    16.             }
    17.         }
    18.     }
    复制代码
    WindowManagerService中对多屏多显状态的屏幕做了处理,除了传递PointerEventListener对象以外,还需要传递需要监听的DisplayContent。最终调用的是DisplayContent中的监听方法。
  • DisplayContent中的监听方法
    代码路径:frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
    1.     void registerPointerEventListener(@NonNull PointerEventListener listener) {
    2.         mPointerEventDispatcher.registerInputEventListener(listener);
    3.     }
    4.     void unregisterPointerEventListener(@NonNull PointerEventListener listener) {
    5.         mPointerEventDispatcher.unregisterInputEventListener(listener);
    6.     }
    复制代码
    在DisplayContent中调用registerPointerEventListener监听,仅传递PointerEventListener对象即可。其本质的实现在PointerEventDispatcher中。
  • PointerEventDispatcher中实现监听方法
    代码路径:frameworks/base/services/core/java/com/android/server/wm/PointerEventDispatcher.java
    1.     /**
    2.      * Add the specified listener to the list.
    3.      * @param listener The listener to add.
    4.      */
    5.     public void registerInputEventListener(PointerEventListener listener) {
    6.         synchronized (mListeners) {
    7.             if (mListeners.contains(listener)) {
    8.                 throw new IllegalStateException("registerInputEventListener: trying to register" +
    9.                         listener + " twice.");
    10.             }
    11.             mListeners.add(listener);
    12.             mListenersArray = null;
    13.         }
    14.     }
    15.    
    16.     /**
    17.      * Remove the specified listener from the list.
    18.      * @param listener The listener to remove.
    19.      */
    20.     public void unregisterInputEventListener(PointerEventListener listener) {
    21.         synchronized (mListeners) {
    22.             if (!mListeners.contains(listener)) {
    23.                 throw new IllegalStateException("registerInputEventListener: " + listener +
    24.                         " not registered.");
    25.             }
    26.             mListeners.remove(listener);
    27.             mListenersArray = null;
    28.         }
    29.     }
    复制代码
    创建PointerEventDispatcher对象调用registerInputEventListener监听即可。
代码

本地使用android-13.0.0_r43版本
  1. repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-13.0.0_r43
复制代码
android T 应用双屏间拖拽移动功能
相关技术文章参考

操纵视频链接:https://www.bilibili.com/video/BV1Tv4y1J7eb/
多屏互动非动画版本:
https://blog.csdn.net/learnframework/article/details/130461689
https://blog.csdn.net/learnframework/article/details/130463995
动画相关设计方案:
https://blog.csdn.net/learnframework/article/details/130507022
https://blog.csdn.net/learnframework/article/details/130522955

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

用户国营

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表