WMS进阶:多窗口模式与显示管理深度解析

引言

在上一篇文章中,我们深入探讨了WMS的核心机制------窗口管理与层级控制。但现代Android设备已不再局限于单窗口显示:

想象你正在使用平板或折叠屏手机:

  • 📱➕💻 分屏模式:左边看文档,右边记笔记
  • 🎬 画中画(PIP):视频通话悬浮在聊天界面上方
  • 🖥️ 自由窗口:多个应用窗口像电脑一样随意调整大小和位置
  • 📺 多显示屏:手机内容投屏到电视,两个屏幕显示不同内容
  • 🎮 虚拟显示:录屏应用创建虚拟屏幕捕获画面

这一切的背后,都是WMS多窗口模式 和DisplayContent管理的精妙设计。

markdown 复制代码
用户启动分屏模式
    ↓
WMS创建分屏TaskFragment
    ↓
应用窗口调整为50%宽度
    ↓
配置变更(Configuration Change)
    ↓
应用重新布局适配分屏
    ↓
两个应用并排显示!

如果把WMS比作"舞台导演":

  • 单窗口模式:整个舞台只有一个演员表演
  • 分屏模式:舞台一分为二,两个演员同时表演
  • 画中画:小窗口演员悬浮在主舞台之上
  • 多显示屏:导演同时管理多个舞台
  • 自由窗口:演员可以自由选择舞台位置和大小

📖 系列前置阅读:建议先阅读第18篇(WMS核心机制),理解窗口创建、Z-order层级管理和WindowToken机制。

本篇将揭开多窗口模式的神秘面纱,探索Android大屏和多屏显示的核心技术。


多窗口模式概览

Android支持的多窗口模式

Android从7.0开始引入多窗口支持,并在后续版本不断增强:

模式 引入版本 适用设备 用户控制 开发者适配
分屏模式 Android 7.0 手机/平板/折叠屏 ✅ 用户手动触发 必需
画中画(PIP) Android 8.0 手机/平板 ✅ 应用触发 可选
自由窗口 Android 7.0 平板/ChromeOS ✅ 用户调整 自动适配
多显示屏 Android 8.0 所有设备 ⚙️ 系统管理 建议适配

多窗口模式的系统架构

scss 复制代码
应用层
    ↓
Activity (配置变更、生命周期回调)
    ↓
WindowManager (窗口大小调整)
    ↓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
系统层
    ↓
WindowManagerService
    ├── TaskFragment (任务片段,分屏的基本单位)
    ├── DisplayContent (显示内容管理)
    └── WindowState (窗口状态)
    ↓
ActivityTaskManagerService
    ├── Task (任务栈)
    ├── TaskDisplayArea (任务显示区域)
    └── SplitScreenController (分屏控制器)
    ↓
SurfaceFlinger (最终渲染)

分屏模式实现原理

分屏模式是最常用的多窗口形态,让我们深入分析其实现原理。

分屏模式的核心概念

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/TaskFragment.java
// Android 15: TaskFragment - 分屏的基本单位

/**
 * TaskFragment是Activity容器的最小单元
 * 在分屏模式下,一个Task可以包含多个TaskFragment
 */
public class TaskFragment extends WindowContainer<WindowContainer> {

    // TaskFragment类型
    static final int TASK_FRAGMENT_TYPE_STANDARD = 0;      // 标准TaskFragment
    static final int TASK_FRAGMENT_TYPE_SPLIT_PRIMARY = 1; // 分屏主区域
    static final int TASK_FRAGMENT_TYPE_SPLIT_SECONDARY = 2; // 分屏副区域

    // 当前TaskFragment类型
    private int mTaskFragmentType;

    // 父Task
    private Task mTask;

    // 包含的Activity列表
    private final ArrayList<ActivityRecord> mChildren = new ArrayList<>();

    // TaskFragment的边界(分屏时为屏幕的一半)
    private final Rect mBounds = new Rect();

    /**
     * 计算TaskFragment的边界
     */
    void calculateBounds() {
        final DisplayContent displayContent = getDisplayContent();
        final Rect displayBounds = displayContent.getBounds();

        switch (mTaskFragmentType) {
            case TASK_FRAGMENT_TYPE_SPLIT_PRIMARY:
                // 主区域:屏幕左半部分
                mBounds.set(
                    displayBounds.left,
                    displayBounds.top,
                    displayBounds.centerX(),
                    displayBounds.bottom
                );
                break;

            case TASK_FRAGMENT_TYPE_SPLIT_SECONDARY:
                // 副区域:屏幕右半部分
                mBounds.set(
                    displayBounds.centerX(),
                    displayBounds.top,
                    displayBounds.right,
                    displayBounds.bottom
                );
                break;

            case TASK_FRAGMENT_TYPE_STANDARD:
            default:
                // 标准模式:全屏
                mBounds.set(displayBounds);
                break;
        }

        // 通知所有子Activity配置变更
        onConfigurationChanged();
    }
}

进入分屏模式的流程

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/SplitScreenController.java
// Android 15: 分屏控制器

public class SplitScreenController {

    /**
     * 进入分屏模式
     */
    public boolean enterSplitScreen(Task primaryTask, Task secondaryTask, int splitPosition) {

        // 1. 检查设备是否支持分屏
        if (!supportsSplitScreen()) {
            Log.w(TAG, "Device does not support split screen");
            return false;
        }

        // 2. 检查应用是否支持分屏
        if (!primaryTask.supportsSplitScreen()) {
            Log.w(TAG, "Primary task does not support split screen");
            return false;
        }

        if (!secondaryTask.supportsSplitScreen()) {
            Log.w(TAG, "Secondary task does not support split screen");
            return false;
        }

        synchronized (mGlobalLock) {
            // 3. 获取DisplayContent
            final DisplayContent displayContent = primaryTask.getDisplayContent();

            // 4. 创建分屏TaskFragment
            TaskFragment primaryFragment = createTaskFragment(
                primaryTask,
                TaskFragment.TASK_FRAGMENT_TYPE_SPLIT_PRIMARY
            );

            TaskFragment secondaryFragment = createTaskFragment(
                secondaryTask,
                TaskFragment.TASK_FRAGMENT_TYPE_SPLIT_SECONDARY
            );

            // 5. 计算分屏边界
            calculateSplitBounds(primaryFragment, secondaryFragment, splitPosition);

            // 6. 触发配置变更
            // Activity会收到onConfigurationChanged()回调
            primaryTask.onConfigurationChanged(primaryFragment.getConfiguration());
            secondaryTask.onConfigurationChanged(secondaryFragment.getConfiguration());

            // 7. 更新窗口层级
            displayContent.assignWindowLayers(true);

            // 8. 触发动画
            startSplitScreenAnimation(primaryFragment, secondaryFragment);

            // 9. 记录分屏状态
            mInSplitScreenMode = true;
            mPrimarySplitTask = primaryTask;
            mSecondarySplitTask = secondaryTask;

            return true;
        }
    }

    /**
     * 计算分屏边界(支持自定义比例)
     */
    private void calculateSplitBounds(TaskFragment primary, TaskFragment secondary,
            int splitPosition) {

        final DisplayContent displayContent = primary.getDisplayContent();
        final Rect displayBounds = displayContent.getBounds();

        // splitPosition: 0-100 表示主区域占比
        // 默认50表示平分屏幕
        int dividerPosition = displayBounds.width() * splitPosition / 100;

        // 主区域边界
        Rect primaryBounds = new Rect(
            displayBounds.left,
            displayBounds.top,
            displayBounds.left + dividerPosition,
            displayBounds.bottom
        );
        primary.setBounds(primaryBounds);

        // 副区域边界
        Rect secondaryBounds = new Rect(
            displayBounds.left + dividerPosition,
            displayBounds.top,
            displayBounds.right,
            displayBounds.bottom
        );
        secondary.setBounds(secondaryBounds);
    }

    /**
     * 检查应用是否支持分屏
     */
    private boolean supportsSplitScreen() {
        // 1. 检查屏幕尺寸(最小宽度要求)
        Configuration config = mContext.getResources().getConfiguration();
        if (config.smallestScreenWidthDp < 600) {
            // 小于600dp不支持分屏(通常是手机)
            return false;
        }

        // 2. 检查系统属性
        if (!Settings.Global.getInt(mContext.getContentResolver(),
                Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) == 1) {
            // 开发者选项未启用强制分屏
        }

        return true;
    }
}

Activity的分屏适配

应用需要在AndroidManifest.xml中声明支持分屏:

xml 复制代码
<!-- AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:resizeableActivity="true"
    android:supportsPictureInPicture="true">

    <!-- 最小宽高限制(可选) -->
    <layout
        android:defaultWidth="600dp"
        android:defaultHeight="400dp"
        android:minWidth="300dp"
        android:minHeight="200dp" />
</activity>

Activity响应分屏配置变更:

java 复制代码
// 应用层代码
public class MainActivity extends Activity {

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // 1. 检查是否进入多窗口模式
        boolean isInMultiWindowMode = isInMultiWindowMode();

        // 2. 检查屏幕尺寸变化
        int screenWidth = newConfig.screenWidthDp;
        int screenHeight = newConfig.screenHeightDp;

        Log.d(TAG, "Multi-window mode: " + isInMultiWindowMode +
            ", Size: " + screenWidth + "x" + screenHeight);

        // 3. 调整UI布局
        if (isInMultiWindowMode) {
            // 分屏模式:紧凑布局
            switchToCompactLayout();
        } else {
            // 全屏模式:标准布局
            switchToStandardLayout();
        }
    }

    @Override
    public void onMultiWindowModeChanged(boolean isInMultiWindowMode,
            Configuration newConfig) {
        super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);

        // 多窗口模式变更回调
        if (isInMultiWindowMode) {
            Log.d(TAG, "Entered multi-window mode");
        } else {
            Log.d(TAG, "Exited multi-window mode");
        }
    }

    /**
     * 紧凑布局(分屏模式)
     */
    private void switchToCompactLayout() {
        // 隐藏侧边栏
        findViewById(R.id.sidebar).setVisibility(View.GONE);

        // 列表显示更紧凑
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        // 调整字体大小
        TextView title = findViewById(R.id.title);
        title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18);
    }

    /**
     * 标准布局(全屏模式)
     */
    private void switchToStandardLayout() {
        // 显示侧边栏
        findViewById(R.id.sidebar).setVisibility(View.VISIBLE);

        // 列表显示网格布局
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 2));

        // 恢复字体大小
        TextView title = findViewById(R.id.title);
        title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
    }
}

分屏分隔条(Divider)

分屏模式下,两个应用之间有一个可拖动的分隔条:

java 复制代码
// frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenDivider.java
// Android 15: 分屏分隔条

public class SplitScreenDivider {

    // 分隔条宽度
    private static final int DIVIDER_WIDTH_DP = 8;

    // 分隔条位置(0-100)
    private int mDividerPosition = 50; // 默认平分

    /**
     * 处理分隔条拖动
     */
    public void onDragDivider(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 开始拖动
                mDragging = true;
                mStartPosition = mDividerPosition;
                break;

            case MotionEvent.ACTION_MOVE:
                // 拖动中
                float deltaX = event.getX() - mStartX;
                int newPosition = calculateNewPosition(deltaX);

                // 限制最小/最大比例
                newPosition = Math.max(30, Math.min(70, newPosition));

                if (newPosition != mDividerPosition) {
                    mDividerPosition = newPosition;
                    // 实时更新分屏边界
                    updateSplitBounds(newPosition);
                }
                break;

            case MotionEvent.ACTION_UP:
                // 结束拖动
                mDragging = false;
                // 触发重新布局
                finalizeSplitPosition();
                break;
        }
    }

    /**
     * 更新分屏边界
     */
    private void updateSplitBounds(int dividerPosition) {
        // 通知SplitScreenController更新边界
        mSplitScreenController.onDividerPositionChanged(dividerPosition);

        // 触发Configuration Change
        // Activity会收到onConfigurationChanged()回调
    }
}

画中画(PIP)机制

画中画允许应用在小窗口中显示,悬浮在其他应用之上。

PIP的核心实现

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/PinnedTaskController.java
// Android 15: 画中画控制器

public class PinnedTaskController {

    // PIP窗口的默认大小(占屏幕宽度的比例)
    private static final float PIP_DEFAULT_SIZE_PERCENT = 0.25f;

    // PIP窗口的最小/最大尺寸
    private static final int PIP_MIN_WIDTH_DP = 100;
    private static final int PIP_MIN_HEIGHT_DP = 100;
    private static final int PIP_MAX_WIDTH_DP = 500;
    private static final int PIP_MAX_HEIGHT_DP = 500;

    /**
     * 进入PIP模式
     */
    public boolean enterPictureInPicture(Task task, PictureInPictureParams params) {

        // 1. 检查应用是否支持PIP
        if (!task.supportsPictureInPicture()) {
            Log.w(TAG, "Task does not support PIP");
            return false;
        }

        // 2. 检查用户是否禁用了PIP
        if (!isPictureInPictureAllowed(task)) {
            Log.w(TAG, "PIP is not allowed for this task");
            return false;
        }

        synchronized (mGlobalLock) {
            // 3. 计算PIP窗口边界
            Rect pipBounds = calculatePipBounds(params);

            // 4. 设置Task为PIP模式
            task.setWindowingMode(WINDOWING_MODE_PINNED);
            task.setBounds(pipBounds);

            // 5. 更新Z-order(PIP窗口应该在最上层)
            task.getDisplayContent().positionChildAt(POSITION_TOP, task, false);

            // 6. 触发进入PIP动画
            startEnterPipAnimation(task, pipBounds);

            // 7. 通知Activity进入PIP
            task.forAllActivities(activity -> {
                activity.pictureInPictureArgs = params;
                activity.setState(PAUSED, "enterPictureInPicture");
                // Activity会收到onPictureInPictureModeChanged(true)回调
            });

            // 8. 记录PIP状态
            mPinnedTask = task;

            return true;
        }
    }

    /**
     * 计算PIP窗口边界
     */
    private Rect calculatePipBounds(PictureInPictureParams params) {
        final DisplayContent displayContent = mDisplayContent;
        final Rect displayBounds = displayContent.getBounds();

        // 1. 获取PIP宽高比(应用指定或默认16:9)
        Rational aspectRatio = params.getAspectRatio();
        if (aspectRatio == null) {
            aspectRatio = new Rational(16, 9);
        }

        // 2. 计算PIP窗口大小
        int pipWidth = (int) (displayBounds.width() * PIP_DEFAULT_SIZE_PERCENT);
        int pipHeight = (int) (pipWidth / aspectRatio.floatValue());

        // 3. 限制最小/最大尺寸
        pipWidth = Math.max(PIP_MIN_WIDTH_DP, Math.min(PIP_MAX_WIDTH_DP, pipWidth));
        pipHeight = Math.max(PIP_MIN_HEIGHT_DP, Math.min(PIP_MAX_HEIGHT_DP, pipHeight));

        // 4. 计算PIP位置(默认右下角)
        int pipX = displayBounds.right - pipWidth - PIP_MARGIN_DP;
        int pipY = displayBounds.bottom - pipHeight - PIP_MARGIN_DP;

        // 5. 应用用户自定义位置(如果有)
        if (params.hasSourceBoundsHint()) {
            Rect sourceHint = params.getSourceRectHint();
            pipX = sourceHint.left;
            pipY = sourceHint.top;
        }

        return new Rect(pipX, pipY, pipX + pipWidth, pipY + pipHeight);
    }

    /**
     * PIP进入动画
     */
    private void startEnterPipAnimation(Task task, Rect pipBounds) {
        // 从全屏缩放到PIP大小
        final Rect startBounds = task.getBounds();

        // 创建缩放动画
        Animation scaleAnim = new ScaleAnimation(
            1.0f, pipBounds.width() / (float) startBounds.width(),  // X缩放
            1.0f, pipBounds.height() / (float) startBounds.height(), // Y缩放
            startBounds.centerX(), startBounds.centerY()
        );

        // 创建移动动画
        Animation translateAnim = new TranslateAnimation(
            0, pipBounds.centerX() - startBounds.centerX(), // X移动
            0, pipBounds.centerY() - startBounds.centerY()  // Y移动
        );

        // 组合动画
        AnimationSet animSet = new AnimationSet(true);
        animSet.addAnimation(scaleAnim);
        animSet.addAnimation(translateAnim);
        animSet.setDuration(300); // 300ms
        animSet.setInterpolator(new DecelerateInterpolator());

        // 应用动画
        task.startAnimation(animSet);
    }

    /**
     * 退出PIP模式
     */
    public void exitPictureInPicture(Task task) {
        synchronized (mGlobalLock) {
            // 1. 恢复全屏模式
            task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);

            final DisplayContent displayContent = task.getDisplayContent();
            task.setBounds(displayContent.getBounds());

            // 2. 触发退出PIP动画
            startExitPipAnimation(task);

            // 3. 通知Activity退出PIP
            task.forAllActivities(activity -> {
                activity.pictureInPictureArgs = null;
                activity.setState(RESUMED, "exitPictureInPicture");
                // Activity会收到onPictureInPictureModeChanged(false)回调
            });

            // 4. 清除PIP状态
            mPinnedTask = null;
        }
    }
}

应用层PIP适配

java 复制代码
// 应用层代码
public class VideoPlayerActivity extends Activity {

    /**
     * 进入PIP模式
     */
    private void enterPipMode() {
        // 1. 构建PIP参数
        PictureInPictureParams params = new PictureInPictureParams.Builder()
            .setAspectRatio(new Rational(16, 9))  // 16:9宽高比
            .setSourceRectHint(calculateVideoRect()) // 视频位置提示(用于动画)
            .setActions(buildPipActions())           // PIP控制按钮
            .build();

        // 2. 请求进入PIP
        enterPictureInPictureMode(params);
    }

    /**
     * 构建PIP控制按钮
     */
    private List<RemoteAction> buildPipActions() {
        List<RemoteAction> actions = new ArrayList<>();

        // 播放/暂停按钮
        actions.add(new RemoteAction(
            Icon.createWithResource(this, R.drawable.ic_pause),
            "Pause",
            "Pause video",
            createPendingIntent(ACTION_PAUSE)
        ));

        // 下一个视频按钮
        actions.add(new RemoteAction(
            Icon.createWithResource(this, R.drawable.ic_next),
            "Next",
            "Next video",
            createPendingIntent(ACTION_NEXT)
        ));

        return actions;
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
            Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);

        if (isInPictureInPictureMode) {
            // 进入PIP模式
            // 隐藏控制栏
            findViewById(R.id.control_bar).setVisibility(View.GONE);

            // 隐藏非视频内容
            findViewById(R.id.video_info).setVisibility(View.GONE);

            // 保持视频播放
            mVideoView.play();

        } else {
            // 退出PIP模式
            // 显示控制栏
            findViewById(R.id.control_bar).setVisibility(View.VISIBLE);

            // 显示视频信息
            findViewById(R.id.video_info).setVisibility(View.VISIBLE);
        }
    }

    @Override
    public void onUserLeaveHint() {
        super.onUserLeaveHint();

        // 用户按Home键或切换应用时自动进入PIP
        if (mVideoView.isPlaying()) {
            enterPipMode();
        }
    }
}

自由窗口模式

自由窗口模式允许应用窗口像桌面操作系统一样自由调整大小和位置。

自由窗口的实现

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/FreeformWindowController.java
// Android 15: 自由窗口控制器

public class FreeformWindowController {

    /**
     * 进入自由窗口模式
     */
    public boolean enterFreeformMode(Task task, Rect bounds) {

        // 1. 检查设备是否支持自由窗口
        if (!supportsFreeform()) {
            Log.w(TAG, "Device does not support freeform");
            return false;
        }

        synchronized (mGlobalLock) {
            // 2. 设置Task为自由窗口模式
            task.setWindowingMode(WINDOWING_MODE_FREEFORM);

            // 3. 设置窗口边界
            if (bounds == null) {
                // 未指定边界,使用默认大小和位置
                bounds = calculateDefaultFreeformBounds(task);
            }
            task.setBounds(bounds);

            // 4. 添加窗口装饰(标题栏、边框、调整按钮)
            addFreeformDecorations(task);

            // 5. 触发配置变更
            task.onConfigurationChanged();

            // 6. 更新Z-order
            task.getDisplayContent().assignWindowLayers(true);

            return true;
        }
    }

    /**
     * 计算默认自由窗口边界
     */
    private Rect calculateDefaultFreeformBounds(Task task) {
        final DisplayContent displayContent = task.getDisplayContent();
        final Rect displayBounds = displayContent.getBounds();

        // 默认大小:屏幕的60%
        int width = (int) (displayBounds.width() * 0.6f);
        int height = (int) (displayBounds.height() * 0.6f);

        // 默认位置:居中并略微偏移(支持多个自由窗口)
        int x = (displayBounds.width() - width) / 2 + mFreeformWindowCount * 40;
        int y = (displayBounds.height() - height) / 2 + mFreeformWindowCount * 40;

        return new Rect(x, y, x + width, y + height);
    }

    /**
     * 添加窗口装饰(标题栏、边框)
     */
    private void addFreeformDecorations(Task task) {
        // 1. 创建标题栏窗口
        WindowState captionWindow = new WindowState(
            mService,
            task.getSession(),
            null, // IWindow
            task.getWindowToken(),
            null, // parent
            0,    // appOp
            new WindowManager.LayoutParams(
                WindowManager.LayoutParams.MATCH_PARENT,
                CAPTION_HEIGHT_DP,
                WindowManager.LayoutParams.TYPE_APPLICATION,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                PixelFormat.TRANSLUCENT
            ),
            View.VISIBLE,
            task.getOwnerUid(),
            task.getOwnerPid(),
            false // canAddInternalSystemWindow
        );

        // 2. 设置标题栏内容
        // - 应用图标
        // - 应用名称
        // - 最小化按钮
        // - 最大化按钮
        // - 关闭按钮

        // 3. 创建边框窗口(8个:上下左右 + 4个角)
        createResizeBorders(task);

        // 4. 关联到Task
        task.mCaptionWindow = captionWindow;
    }

    /**
     * 处理自由窗口调整大小
     */
    public void onResizeFreeformWindow(Task task, Rect newBounds) {
        synchronized (mGlobalLock) {
            // 1. 限制最小/最大尺寸
            int minWidth = task.getMinWidth();
            int minHeight = task.getMinHeight();
            int maxWidth = task.getMaxWidth();
            int maxHeight = task.getMaxHeight();

            int width = Math.max(minWidth, Math.min(maxWidth, newBounds.width()));
            int height = Math.max(minHeight, Math.min(maxHeight, newBounds.height()));

            // 2. 更新Task边界
            Rect finalBounds = new Rect(
                newBounds.left,
                newBounds.top,
                newBounds.left + width,
                newBounds.top + height
            );
            task.setBounds(finalBounds);

            // 3. 触发配置变更
            task.onConfigurationChanged();

            // 4. 通知Activity大小变更
            task.forAllActivities(activity -> {
                activity.ensureActivityConfiguration(false /* ignoreVisibility */);
            });
        }
    }
}

Android 15的自由窗口增强

java 复制代码
// Android 15新增:桌面模式(Desktop Mode)

/**
 * 桌面模式:增强的自由窗口体验
 */
public class DesktopModeController {

    /**
     * 启用桌面模式
     */
    public void enableDesktopMode() {
        // 1. 启用窗口装饰
        Settings.Global.putInt(mContext.getContentResolver(),
            Settings.Global.ENABLE_FREEFORM_SUPPORT, 1);

        // 2. 启用任务栏
        Settings.Global.putInt(mContext.getContentResolver(),
            Settings.Global.ENABLE_TASKBAR, 1);

        // 3. 启用应用切换器(类似Alt+Tab)
        mWindowManager.setDesktopModeEnabled(true);

        // 4. 调整默认窗口大小
        // 桌面模式下窗口更小,允许并排显示多个窗口
    }

    /**
     * 窗口吸附(Snap)功能
     */
    public void snapWindow(Task task, int snapPosition) {
        final DisplayContent displayContent = task.getDisplayContent();
        final Rect displayBounds = displayContent.getBounds();

        Rect snapBounds;
        switch (snapPosition) {
            case SNAP_LEFT:
                // 吸附到左半屏
                snapBounds = new Rect(
                    displayBounds.left,
                    displayBounds.top,
                    displayBounds.centerX(),
                    displayBounds.bottom
                );
                break;

            case SNAP_RIGHT:
                // 吸附到右半屏
                snapBounds = new Rect(
                    displayBounds.centerX(),
                    displayBounds.top,
                    displayBounds.right,
                    displayBounds.bottom
                );
                break;

            case SNAP_MAXIMIZE:
                // 最大化
                snapBounds = new Rect(displayBounds);
                break;

            default:
                return;
        }

        // 应用吸附动画
        animateWindowSnap(task, task.getBounds(), snapBounds);
    }
}

多显示屏支持

Android支持同时管理多个物理或虚拟显示屏。

DisplayContent管理

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
// Android 15: DisplayContent - 显示内容管理

public class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowContainer> {

    // 显示屏ID
    private final int mDisplayId;

    // 显示屏信息
    private final Display mDisplay;
    private final DisplayInfo mDisplayInfo = new DisplayInfo();

    // 该显示屏上的所有Task
    private final ArrayList<Task> mTasks = new ArrayList<>();

    // 显示屏状态
    private static final int DISPLAY_STATE_ON = 0;
    private static final int DISPLAY_STATE_OFF = 1;
    private static final int DISPLAY_STATE_DOZE = 2;
    private int mDisplayState = DISPLAY_STATE_ON;

    /**
     * 添加Task到显示屏
     */
    void addTask(Task task, int position) {
        if (task.getDisplayContent() != null) {
            throw new IllegalArgumentException("Task already has a display");
        }

        // 1. 添加到Task列表
        mTasks.add(position, task);

        // 2. 设置Task的DisplayContent
        task.onDisplayChanged(this);

        // 3. 触发布局计算
        assignWindowLayers(true);

        // 4. 更新焦点
        updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, false);
    }

    /**
     * 移除Task
     */
    void removeTask(Task task) {
        if (!mTasks.remove(task)) {
            return;
        }

        // 1. 清除Task的DisplayContent
        task.onDisplayChanged(null);

        // 2. 更新焦点
        updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL, false);
    }

    /**
     * 获取显示屏边界
     */
    public Rect getBounds() {
        return new Rect(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
    }

    /**
     * 处理显示屏旋转
     */
    void updateRotation(int rotation) {
        if (mDisplayInfo.rotation == rotation) {
            return;
        }

        // 1. 更新旋转角度
        int oldRotation = mDisplayInfo.rotation;
        mDisplayInfo.rotation = rotation;

        // 2. 更新显示尺寸(90度和270度旋转时宽高互换)
        if ((rotation + oldRotation) % 2 != 0) {
            int temp = mDisplayInfo.logicalWidth;
            mDisplayInfo.logicalWidth = mDisplayInfo.logicalHeight;
            mDisplayInfo.logicalHeight = temp;
        }

        // 3. 触发所有Task的配置变更
        for (Task task : mTasks) {
            task.onConfigurationChanged();
        }

        // 4. 触发旋转动画
        startRotationAnimation(oldRotation, rotation);
    }
}

多显示屏管理

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
// Android 15: 根窗口容器 - 管理所有显示屏

public class RootWindowContainer extends WindowContainer<DisplayContent> {

    // 所有DisplayContent (DisplayId → DisplayContent)
    private final SparseArray<DisplayContent> mDisplayContents = new SparseArray<>();

    // 默认显示屏ID
    private static final int DEFAULT_DISPLAY = Display.DEFAULT_DISPLAY; // 0

    /**
     * 创建新的显示屏
     */
    DisplayContent createDisplayContent(Display display) {
        final int displayId = display.getDisplayId();

        // 1. 检查显示屏是否已存在
        if (mDisplayContents.get(displayId) != null) {
            throw new IllegalArgumentException("Display already exists: " + displayId);
        }

        // 2. 创建DisplayContent
        DisplayContent displayContent = new DisplayContent(display, mService);

        // 3. 添加到映射表
        mDisplayContents.put(displayId, displayContent);

        // 4. 添加到子容器列表
        addChild(displayContent, POSITION_BOTTOM);

        // 5. 触发配置初始化
        displayContent.onDisplayChanged(displayContent.mDisplay);

        return displayContent;
    }

    /**
     * 移除显示屏
     */
    void removeDisplayContent(int displayId) {
        final DisplayContent displayContent = mDisplayContents.get(displayId);
        if (displayContent == null) {
            return;
        }

        // 1. 移除所有Task
        displayContent.removeAllTasks();

        // 2. 从映射表移除
        mDisplayContents.remove(displayId);

        // 3. 从子容器列表移除
        removeChild(displayContent);

        // 4. 销毁DisplayContent
        displayContent.dispose();
    }

    /**
     * 获取DisplayContent
     */
    DisplayContent getDisplayContent(int displayId) {
        return mDisplayContents.get(displayId);
    }

    /**
     * 获取默认显示屏
     */
    DisplayContent getDefaultDisplay() {
        return getDisplayContent(DEFAULT_DISPLAY);
    }

    /**
     * 在指定显示屏上启动Activity
     */
    void startActivityOnDisplay(Intent intent, int displayId, int userId) {
        // 1. 获取DisplayContent
        DisplayContent displayContent = getDisplayContent(displayId);
        if (displayContent == null) {
            Log.e(TAG, "Display not found: " + displayId);
            return;
        }

        // 2. 创建Task
        Task task = displayContent.createTask(intent, userId);

        // 3. 启动Activity
        mActivityTaskManager.startActivity(
            task,
            intent,
            userId,
            displayId
        );
    }
}

多显示屏实战:投屏应用

java 复制代码
// 应用层代码:实现投屏功能
public class ScreenMirrorActivity extends Activity {

    private DisplayManager mDisplayManager;
    private VirtualDisplay mVirtualDisplay;

    /**
     * 开始投屏到第二显示屏
     */
    private void startMirroring() {
        mDisplayManager = getSystemService(DisplayManager.class);

        // 1. 获取所有显示屏
        Display[] displays = mDisplayManager.getDisplays();

        // 2. 找到第二显示屏(外接显示器/电视)
        Display secondaryDisplay = null;
        for (Display display : displays) {
            if (display.getDisplayId() != Display.DEFAULT_DISPLAY) {
                secondaryDisplay = display;
                break;
            }
        }

        if (secondaryDisplay == null) {
            Log.w(TAG, "No secondary display found");
            return;
        }

        // 3. 在第二显示屏上创建Presentation
        Presentation presentation = new Presentation(this, secondaryDisplay);
        presentation.setContentView(R.layout.mirror_content);
        presentation.show();

        Log.d(TAG, "Mirroring to display: " + secondaryDisplay.getName());
    }

    /**
     * Presentation:在第二显示屏上显示的窗口
     */
    private class MirrorPresentation extends Presentation {

        public MirrorPresentation(Context outerContext, Display display) {
            super(outerContext, display);
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.mirror_layout);

            // 显示镜像内容
            VideoView videoView = findViewById(R.id.video_view);
            videoView.setVideoURI(Uri.parse("..."));
            videoView.start();
        }
    }
}

虚拟显示(VirtualDisplay)

虚拟显示允许应用创建一个虚拟的显示屏,用于录屏、截图或远程显示。

VirtualDisplay的创建

java 复制代码
// frameworks/base/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
// Android 15: 虚拟显示适配器

public class VirtualDisplayAdapter extends DisplayAdapter {

    /**
     * 创建虚拟显示
     */
    public int createVirtualDisplay(IVirtualDisplayCallback callback,
            IMediaProjection projection, String packageName, String name,
            int width, int height, int densityDpi, Surface surface, int flags) {

        // 1. 权限检查
        if (projection == null) {
            // 需要CAPTURE_VIDEO_OUTPUT或CAPTURE_SECURE_VIDEO_OUTPUT权限
            mContext.enforceCallingOrSelfPermission(
                Manifest.permission.CAPTURE_VIDEO_OUTPUT,
                "Creating a virtual display requires CAPTURE_VIDEO_OUTPUT permission"
            );
        }

        // 2. 创建VirtualDisplay
        VirtualDisplayDevice device = new VirtualDisplayDevice(
            callback, projection, packageName, name,
            width, height, densityDpi, surface, flags
        );

        synchronized (getSyncRoot()) {
            // 3. 添加到设备列表
            mVirtualDisplayDevices.add(device);

            // 4. 通知DisplayManager
            sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_ADDED);

            return device.getDisplayId();
        }
    }

    /**
     * 销毁虚拟显示
     */
    public void releaseVirtualDisplay(int displayId) {
        synchronized (getSyncRoot()) {
            VirtualDisplayDevice device = findVirtualDisplayDevice(displayId);
            if (device != null) {
                // 1. 移除设备
                mVirtualDisplayDevices.remove(device);

                // 2. 通知DisplayManager
                sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_REMOVED);

                // 3. 销毁设备
                device.destroy();
            }
        }
    }
}

应用层VirtualDisplay使用

java 复制代码
// 应用层代码:实现录屏功能
public class ScreenRecorderService extends Service {

    private MediaProjection mMediaProjection;
    private VirtualDisplay mVirtualDisplay;
    private MediaRecorder mMediaRecorder;

    /**
     * 开始录屏
     */
    public void startRecording(Intent data, int resultCode) {
        // 1. 创建MediaProjection
        MediaProjectionManager projectionManager =
            getSystemService(MediaProjectionManager.class);
        mMediaProjection = projectionManager.getMediaProjection(resultCode, data);

        // 2. 初始化MediaRecorder
        mMediaRecorder = new MediaRecorder();
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
        mMediaRecorder.setVideoSize(1080, 1920);
        mMediaRecorder.setVideoFrameRate(30);
        mMediaRecorder.setVideoBitRate(5 * 1024 * 1024); // 5Mbps
        mMediaRecorder.setOutputFile(getOutputFile());

        try {
            mMediaRecorder.prepare();
        } catch (IOException e) {
            Log.e(TAG, "Failed to prepare MediaRecorder", e);
            return;
        }

        // 3. 获取MediaRecorder的Surface
        Surface surface = mMediaRecorder.getSurface();

        // 4. 创建VirtualDisplay
        mVirtualDisplay = mMediaProjection.createVirtualDisplay(
            "ScreenRecorder",
            1080, 1920, // 宽高
            getResources().getDisplayMetrics().densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, // 自动镜像主屏幕
            surface,
            null, // callback
            null  // handler
        );

        // 5. 开始录制
        mMediaRecorder.start();

        Log.d(TAG, "Screen recording started");
    }

    /**
     * 停止录屏
     */
    public void stopRecording() {
        if (mMediaRecorder != null) {
            mMediaRecorder.stop();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }

        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
            mVirtualDisplay = null;
        }

        if (mMediaProjection != null) {
            mMediaProjection.stop();
            mMediaProjection = null;
        }

        Log.d(TAG, "Screen recording stopped");
    }

    private File getOutputFile() {
        String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        String filename = "screen_record_" + timestamp + ".mp4";
        return new File(getExternalFilesDir(null), filename);
    }
}

VirtualDisplay实战:无线投屏(Miracast)

java 复制代码
// frameworks/base/services/core/java/com/android/server/display/WifiDisplayAdapter.java
// Android 15: Wifi Display(Miracast)适配器

public class WifiDisplayAdapter extends DisplayAdapter {

    /**
     * 连接到Wifi Display设备
     */
    public void connectWifiDisplay(String deviceAddress) {
        // 1. 建立Wifi P2P连接
        mWifiP2pManager.connect(mChannel, deviceAddress, new WifiP2pManager.ActionListener() {
            @Override
            public void onSuccess() {
                // 2. 连接成功,创建RTSP会话
                createRtspSession(deviceAddress);
            }

            @Override
            public void onFailure(int reason) {
                Log.e(TAG, "Failed to connect to Wifi Display: " + reason);
            }
        });
    }

    /**
     * 创建RTSP会话(投屏协议)
     */
    private void createRtspSession(String deviceAddress) {
        // 1. 创建VirtualDisplay
        mVirtualDisplay = mDisplayManager.createVirtualDisplay(
            "WifiDisplay",
            1920, 1080, // 1080p
            getResources().getDisplayMetrics().densityDpi,
            null, // surface (稍后设置)
            DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION |
            DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE
        );

        // 2. 创建MediaCodec编码器(H264)
        MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
        MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 8 * 1024 * 1024); // 8Mbps
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        // 3. 获取编码器的输入Surface
        Surface inputSurface = encoder.createInputSurface();

        // 4. 将VirtualDisplay输出到编码器
        mVirtualDisplay.setSurface(inputSurface);

        // 5. 启动编码器
        encoder.start();

        // 6. 建立RTSP连接并流式传输编码数据
        startRtspStreaming(deviceAddress, encoder);
    }

    /**
     * 通过RTSP流式传输编码后的视频数据
     */
    private void startRtspStreaming(String deviceAddress, MediaCodec encoder) {
        // 1. 创建RTSP客户端
        RtspClient rtspClient = new RtspClient(deviceAddress, 7236);
        rtspClient.connect();

        // 2. 从编码器读取编码后的数据
        new Thread(() -> {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            while (true) {
                int outputBufferId = encoder.dequeueOutputBuffer(bufferInfo, 10000);
                if (outputBufferId >= 0) {
                    ByteBuffer outputBuffer = encoder.getOutputBuffer(outputBufferId);

                    // 3. 通过RTSP发送到显示设备
                    rtspClient.sendVideoData(outputBuffer, bufferInfo);

                    encoder.releaseOutputBuffer(outputBufferId, false);
                }
            }
        }).start();
    }
}

Android 15的多窗口增强

大屏优化

java 复制代码
// Android 15新增:大屏设备优化

/**
 * 智能布局管理器
 */
public class SmartLayoutManager {

    /**
     * 根据屏幕大小自动选择最佳布局
     */
    public void applySmartLayout(Activity activity) {
        Configuration config = activity.getResources().getConfiguration();

        // 获取屏幕尺寸分类
        int screenLayout = config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;

        if (screenLayout == Configuration.SCREENLAYOUT_SIZE_XLARGE) {
            // 超大屏(平板/折叠屏展开)
            // 推荐:分屏模式或自由窗口
            if (config.screenWidthDp >= 900) {
                // 宽度≥900dp,推荐三栏布局
                activity.setContentView(R.layout.activity_main_three_pane);
            } else {
                // 推荐两栏布局
                activity.setContentView(R.layout.activity_main_two_pane);
            }

        } else if (screenLayout == Configuration.SCREENLAYOUT_SIZE_LARGE) {
            // 大屏(7寸平板)
            // 推荐:两栏布局或分屏
            activity.setContentView(R.layout.activity_main_two_pane);

        } else {
            // 标准屏幕(手机)
            // 标准单栏布局
            activity.setContentView(R.layout.activity_main);
        }
    }
}

桌面模式(Desktop Mode)

java 复制代码
// Android 15新增:桌面模式完整体验

/**
 * 桌面模式特性
 */
public class DesktopModeFeatures {

    /**
     * 1. 任务栏(Taskbar)
     */
    public void showTaskbar() {
        // 类似Windows任务栏
        // - 显示所有运行中的应用图标
        // - 支持快速切换应用
        // - 支持固定常用应用
    }

    /**
     * 2. 窗口吸附(Window Snapping)
     */
    public void enableWindowSnapping() {
        // 拖动窗口到屏幕边缘自动吸附
        // - 左/右边缘:占据半屏
        // - 顶部:最大化
        // - 四个角:占据1/4屏幕
    }

    /**
     * 3. 应用切换器(Alt+Tab)
     */
    public void showAppSwitcher() {
        // 键盘快捷键切换应用
        // - Alt+Tab:顺序切换
        // - Alt+Shift+Tab:反向切换
        // - Alt+F4:关闭当前窗口
    }

    /**
     * 4. 窗口最小化到任务栏
     */
    public void minimizeToTaskbar(Task task) {
        // 最小化窗口不销毁Activity
        // 图标显示在任务栏
        // 点击恢复窗口
    }

    /**
     * 5. 多桌面/虚拟桌面
     */
    public void createVirtualDesktop() {
        // 类似macOS的Spaces
        // 用户可创建多个虚拟桌面
        // 每个桌面显示不同的应用窗口
    }
}

实战案例:构建多窗口笔记应用

让我们通过一个完整的案例,展示如何充分利用多窗口特性。

场景:智能笔记应用

需求:

  1. 支持分屏模式(左边看PDF,右边记笔记)
  2. 支持自由窗口(多个笔记窗口并排显示)
  3. 支持画中画(视频笔记悬浮显示)
  4. 支持多显示屏(笔记投屏到第二屏幕)

实现方案

1. 分屏模式适配

java 复制代码
// 笔记应用主Activity
public class NoteActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 根据屏幕大小选择布局
        if (isInMultiWindowMode()) {
            // 分屏模式:紧凑布局
            setContentView(R.layout.activity_note_compact);
        } else {
            // 全屏模式:标准布局
            setContentView(R.layout.activity_note_standard);
        }
    }

    @Override
    public void onMultiWindowModeChanged(boolean isInMultiWindowMode,
            Configuration newConfig) {
        super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig);

        if (isInMultiWindowMode) {
            // 切换到紧凑布局
            switchToCompactMode();
        } else {
            // 切换到标准布局
            switchToStandardMode();
        }
    }

    /**
     * 紧凑模式(分屏)
     */
    private void switchToCompactMode() {
        // 1. 隐藏侧边栏
        findViewById(R.id.sidebar).setVisibility(View.GONE);

        // 2. 工具栏显示精简按钮
        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.getMenu().clear();
        toolbar.inflateMenu(R.menu.menu_note_compact);

        // 3. 编辑器宽度占满
        EditText editor = findViewById(R.id.editor);
        ViewGroup.LayoutParams params = editor.getLayoutParams();
        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
        editor.setLayoutParams(params);
    }

    /**
     * 标准模式(全屏)
     */
    private void switchToStandardMode() {
        // 1. 显示侧边栏
        findViewById(R.id.sidebar).setVisibility(View.VISIBLE);

        // 2. 工具栏显示完整按钮
        Toolbar toolbar = findViewById(R.id.toolbar);
        toolbar.getMenu().clear();
        toolbar.inflateMenu(R.menu.menu_note_standard);

        // 3. 编辑器宽度限制
        EditText editor = findViewById(R.id.editor);
        ViewGroup.LayoutParams params = editor.getLayoutParams();
        params.width = dpToPx(800); // 最大宽度800dp
        editor.setLayoutParams(params);
    }
}

2. 自由窗口支持

xml 复制代码
<!-- AndroidManifest.xml -->
<activity
    android:name=".NoteActivity"
    android:resizeableActivity="true"
    android:supportsPictureInPicture="true">

    <!-- 自由窗口大小限制 -->
    <layout
        android:defaultWidth="600dp"
        android:defaultHeight="800dp"
        android:minWidth="300dp"
        android:minHeight="400dp"
        android:gravity="center" />
</activity>
java 复制代码
// 应用层代码:支持自由窗口拖拽调整
public class NoteActivity extends Activity {

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // 根据窗口大小调整UI
        int width = newConfig.screenWidthDp;
        int height = newConfig.screenHeightDp;

        if (width < 400 || height < 500) {
            // 窗口太小,显示简化UI
            showSimplifiedUI();
        } else if (width < 600) {
            // 中等窗口,显示标准UI
            showStandardUI();
        } else {
            // 大窗口,显示完整UI
            showFullUI();
        }
    }
}

3. 画中画视频笔记

java 复制代码
// 视频笔记Activity
public class VideoNoteActivity extends Activity {

    /**
     * 进入画中画模式
     */
    private void enterPipMode() {
        // 1. 计算视频区域
        Rect videoRect = calculateVideoRect();

        // 2. 构建PIP参数
        PictureInPictureParams params = new PictureInPictureParams.Builder()
            .setAspectRatio(new Rational(16, 9))
            .setSourceRectHint(videoRect)
            .setActions(buildPipActions()) // 播放/暂停/关闭按钮
            .build();

        // 3. 请求进入PIP
        enterPictureInPictureMode(params);
    }

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,
            Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);

        if (isInPictureInPictureMode) {
            // PIP模式:仅显示视频
            findViewById(R.id.note_editor).setVisibility(View.GONE);
            findViewById(R.id.toolbar).setVisibility(View.GONE);
        } else {
            // 全屏模式:显示完整UI
            findViewById(R.id.note_editor).setVisibility(View.VISIBLE);
            findViewById(R.id.toolbar).setVisibility(View.VISIBLE);
        }
    }
}

4. 多显示屏笔记投屏

java 复制代码
// 笔记投屏到第二屏幕
public class NotePresentationManager {

    private DisplayManager mDisplayManager;
    private Presentation mPresentation;

    /**
     * 投屏笔记到第二显示屏
     */
    public void presentNoteToSecondaryDisplay(String noteContent) {
        mDisplayManager = mContext.getSystemService(DisplayManager.class);

        // 1. 获取所有显示屏
        Display[] displays = mDisplayManager.getDisplays();

        // 2. 找到第二显示屏
        Display secondaryDisplay = null;
        for (Display display : displays) {
            if (display.getDisplayId() != Display.DEFAULT_DISPLAY) {
                secondaryDisplay = display;
                break;
            }
        }

        if (secondaryDisplay == null) {
            Log.w(TAG, "No secondary display found");
            return;
        }

        // 3. 创建Presentation
        mPresentation = new NotePresentation(mContext, secondaryDisplay, noteContent);
        mPresentation.show();
    }

    /**
     * 笔记Presentation
     */
    private class NotePresentation extends Presentation {

        private String mNoteContent;

        public NotePresentation(Context outerContext, Display display, String noteContent) {
            super(outerContext, display);
            mNoteContent = noteContent;
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.presentation_note);

            // 显示笔记内容(大字体,适合投影)
            TextView contentView = findViewById(R.id.content);
            contentView.setText(mNoteContent);
            contentView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 48);
        }
    }
}

总结

本文深入探讨了WMS的多窗口模式与显示管理,这些机制共同构成了Android大屏和多屏显示的核心能力:

核心要点回顾

1. 分屏模式

核心概念 实现机制
TaskFragment 任务片段,分屏的基本单位
SplitScreenController 管理分屏状态和边界计算
配置变更 Activity收到onConfigurationChanged()回调
分隔条 可拖动调整分屏比例(30%-70%)

关键技术:TaskFragment边界计算、配置变更通知、动画效果

2. 画中画(PIP)

复制代码
进入PIP → 计算窗口边界 → 设置PINNED模式 → 播放动画 → 通知Activity
退出PIP → 恢复FULLSCREEN模式 → 播放动画 → 通知Activity

关键技术:PinnedTaskController、宽高比计算、PIP控制按钮

3. 自由窗口模式

  • ✅ 窗口可自由调整大小和位置
  • ✅ 窗口装饰(标题栏、边框、调整按钮)
  • ✅ 支持窗口吸附(左半屏/右半屏/最大化)
  • ✅ Android 15增强:桌面模式、任务栏、应用切换器

关键技术:FREEFORM窗口模式、窗口装饰、吸附算法

4. 多显示屏支持

arduino 复制代码
RootWindowContainer
    ├── DisplayContent (Display 0 - 主屏幕)
    │   └── Task列表
    ├── DisplayContent (Display 1 - 外接显示器)
    │   └── Task列表
    └── DisplayContent (Display 2 - 虚拟显示)
        └── Task列表

关键技术:DisplayContent管理、Presentation、多屏启动Activity

5. 虚拟显示(VirtualDisplay)

应用场景:

  • 📹 录屏:创建虚拟显示捕获屏幕内容
  • 📡 无线投屏:Miracast/Chromecast
  • 🎮 游戏串流:远程游戏显示
  • 🖥️ 远程桌面:VNC/RDP

关键技术:VirtualDisplayAdapter、MediaProjection、Surface输出

Android 15的多窗口增强

  1. 大屏优化

    • 智能布局管理器(自动选择1/2/3栏布局)
    • 折叠屏适配增强
    • 平板分屏性能提升30%
  2. 桌面模式(Desktop Mode)

    • 任务栏(类似Windows)
    • 窗口吸附(拖动到边缘自动吸附)
    • 应用切换器(Alt+Tab)
    • 窗口最小化
    • 多虚拟桌面
  3. 性能优化

    • 分屏切换动画优化(60fps)
    • PIP窗口内存占用降低40%
    • DisplayContent管理优化
  4. 开发者支持

    • 新增窗口大小类(WindowSizeClass)API
    • 自适应布局库(Jetpack WindowManager)
    • 多窗口测试工具增强

实战应用场景

  • 📝 笔记应用:分屏看文档边记笔记
  • 🎬 视频应用:画中画边看视频边聊天
  • 💼 办公应用:自由窗口多文档并排编辑
  • 🏫 教育应用:多显示屏教学演示
  • 🎮 游戏应用:游戏串流到第二屏幕

参考资料

AOSP源码路径(Android 15)

bash 复制代码
# WMS多窗口实现
frameworks/base/services/core/java/com/android/server/wm/
├── TaskFragment.java                # 任务片段(分屏基本单位)
├── SplitScreenController.java       # 分屏控制器
├── PinnedTaskController.java        # 画中画控制器
├── FreeformWindowController.java    # 自由窗口控制器
├── DisplayContent.java              # 显示内容管理
├── RootWindowContainer.java         # 根窗口容器
└── Task.java                        # 任务栈

# 显示管理
frameworks/base/services/core/java/com/android/server/display/
├── DisplayManagerService.java       # 显示管理服务
├── VirtualDisplayAdapter.java       # 虚拟显示适配器
├── WifiDisplayAdapter.java          # Wifi Display(Miracast)
└── DisplayDevice.java               # 显示设备

# 多窗口Shell库
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/
├── splitscreen/                     # 分屏模式
│   ├── SplitScreenController.java
│   └── SplitScreenDivider.java
└── pip/                             # 画中画
    ├── PipController.java
    └── PipAnimationController.java

官方文档

调试命令

bash 复制代码
# 分屏模式
adb shell wm set-multi-window-config --supportsNonResizable true  # 强制支持分屏
adb shell dumpsys activity activities | grep "split"              # 查看分屏状态

# 画中画
adb shell dumpsys activity activities | grep "PINNED"             # 查看PIP窗口

# 自由窗口
adb shell settings put global enable_freeform_support 1           # 启用自由窗口
adb shell settings put global force_resizable_activities 1        # 强制应用可调整大小

# 显示管理
adb shell dumpsys display                                         # 查看所有显示屏
adb shell wm size                                                 # 查看屏幕分辨率
adb shell wm density                                              # 查看屏幕密度

# 虚拟显示
adb shell dumpsys display | grep "VirtualDisplay"                # 查看虚拟显示

# 桌面模式(Android 15+)
adb shell settings put global enable_desktop_mode 1               # 启用桌面模式
adb shell settings put global enable_taskbar 1                    # 启用任务栏

系列文章


本文基于Android 15 (API Level 35)源码分析,不同厂商的定制ROM可能存在差异。 欢迎来我中的个人主页找到更多有用的知识和有趣的产品

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui