深入解析Launcher3 中的 CellLayout

3 阅读18分钟

AOSP Launcher3 CellLayout 深度解析

一、总体定位

Launcher3 视图层级:

DragLayer
 └── Workspace (PagedView)
      ├── CellLayout (Page 0 — 主屏)
      │    └── ShortcutAndWidgetContainer
      │         ├── BubbleTextView (App Icon)
      │         ├── BubbleTextView (Shortcut)
      │         ├── AppWidgetHostView (Widget)
      │         ├── FolderIcon
      │         └── ...
      ├── CellLayout (Page 1)
      │    └── ShortcutAndWidgetContainer
      ├── CellLayout (Page 2)
      │    └── ShortcutAndWidgetContainer
      └── ...

Hotseat
 └── CellLayout (底部快捷栏)
      └── ShortcutAndWidgetContainer

CellLayout 是 Launcher 桌面每一页的核心容器,负责将屏幕划分为 N×M 的网格,管理图标/Widget 在网格中的放置、拖拽、重排、动画。


二、核心类图

CellLayout (ViewGroup)
 │
 ├── 内部容器
 │    └── ShortcutAndWidgetContainer (实际持有子 View)
 │
 ├── 核心数据结构
 │    ├── boolean[][] mOccupied        ── 占用矩阵
 │    ├── boolean[][] mTmpOccupied     ── 临时占用矩阵
 │    ├── int[] mTempLocation          ── 临时坐标
 │    ├── GridOccupancy mOccupied      ── 封装后的占用矩阵
 │    └── ItemConfiguration            ── 重排配置快照
 │
 ├── 布局参数
 │    ├── mCountX, mCountY             ── 网格列/行数
 │    ├── mCellWidth, mCellHeight      ── 单元格像素尺寸
 │    ├── mWidthGap, mHeightGap        ── 单元格间距
 │    ├── mBorderSpaceX, mBorderSpaceY ── 边框间距
 │    └── mFixedCellWidth/Height       ── 固定单元格尺寸
 │
 ├── 拖拽支持
 │    ├── DragEnforcer                 ── 拖拽规则
 │    ├── mDragCenter                  ── 拖拽中心坐标
 │    ├── mDragCell                    ── 拖拽源单元格
 │    ├── mReorderAnimators            ── 重排动画 Map
 │    └── CellLayoutLayoutParams       ── 子 View 布局参数
 │
 ├── 视觉反馈
 │    ├── CellLayoutContainer          ── 背景容器
 │    ├── mDragOutlinePaint            ── 拖拽轮廓画笔
 │    ├── mFolderLeaveBehindCell       ── 文件夹拖出残影
 │    └── mItemPlacementDirty          ── 标记需要刷新
 │
 └── 辅助内部类
      ├── CellInfo                     ── 单元格信息
      ├── LayoutParams (CellLayoutLayoutParams)
      ├── ItemConfiguration            ── 重排方案
      └── GridOccupancy                ── 占用矩阵封装

三、源码结构分析

3.1 关键成员变量

/**
 * CellLayout.java (简化版 — 基于 AOSP Android 14)
 */
public class CellLayout extends ViewGroup {

    // ============================================================
    // 网格配置
    // ============================================================

    /** 网格列数和行数 (典型值: 4×5 或 5×5) */
    private int mCountX;  // 列数
    private int mCountY;  // 行数

    /** 单元格像素尺寸 */
    private int mCellWidth;
    private int mCellHeight;

    /**
     * 单元格之间的间距
     * 在新版 Launcher3 中被 mBorderSpace 取代
     */
    private int mBorderSpaceX;
    private int mBorderSpaceY;

    /** 内边距 (网格区域到 CellLayout 边缘的距离) */
    private int mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom;

    // ============================================================
    // 核心数据结构 — 占用矩阵
    // ============================================================

    /**
     * ★ 占用矩阵: mCountX × mCountY 的 boolean 二维数组
     *
     * mOccupied[x][y] = true  表示该单元格已被占用
     * mOccupied[x][y] = false 表示空闲
     *
     * 这是 CellLayout 最核心的数据结构, 所有放置/移动/查找
     * 算法都基于此矩阵
     */
    final GridOccupancy mOccupied;

    /** 临时占用矩阵 (拖拽计算时使用, 避免修改原矩阵) */
    final GridOccupancy mTmpOccupied;

    // ============================================================
    // 子 View 容器
    // ============================================================

    /**
     * ★ 真正持有子 View 的容器
     *
     * CellLayout 自身不直接 addView 子元素,
     * 而是将 Icon/Widget 添加到此内部容器中
     */
    private ShortcutAndWidgetContainer mShortcutsAndWidgets;

    // ============================================================
    // 拖拽相关
    // ============================================================

    /** 拖拽时的目标单元格 */
    private final int[] mDragCell = new int[2];

    /** 拖拽时的目标单元格中心像素坐标 */
    private final int[] mTmpPoint = new int[2];
    private final int[] mTempLocation = new int[2];

    /** 拖拽预览: 轮廓动画 */
    private final CellLayoutContainer mContainer;
    private Drawable mDragOutline = null;

    /** 是否正在拖拽中 */
    private boolean mDragging;

    /**
     * ★ 重排动画映射: 每个被挤走的 View 对应一个动画
     * Key   = 被重排的 View
     * Value = 该 View 的位移动画
     */
    private final HashMap<CellLayoutLayoutParams, Animator> mReorderAnimators = new HashMap<>();

    /** 已占用的 ItemInfo 列表 (用于重排算法) */
    private final ArrayList<ItemInfo> mIntersectingViews = new ArrayList<>();

    // ============================================================
    // 文件夹
    // ============================================================

    /** 文件夹拖出时的"残影"位置 */
    private int[] mFolderLeaveBehindCell = {-1, -1};

    // ============================================================
    // 常量
    // ============================================================

    /** 拖拽重排动画时长 */
    private static final int REORDER_ANIMATION_DURATION = 150;

    /** 图标放置模式 */
    public static final int MODE_SHOW_REORDER_HINT = 0;
    public static final int MODE_DRAG_OVER = 1;
    public static final int MODE_ON_DROP = 2;
    public static final int MODE_ON_DROP_EXTERNAL = 3;
    public static final int MODE_ACCEPT_DROP = 4;
}

3.2 GridOccupancy — 占用矩阵

/**
 * GridOccupancy — CellLayout 的核心数据结构
 *
 * 封装 boolean[][] 矩阵, 提供查找空位、标记占用等操作
 *
 * 示例 (4列 × 5行 桌面):
 *
 *   列→  0     1     2     3
 * 行↓ ┌─────┬─────┬─────┬─────┐
 *  0  │  📱  │  📷  │     │     │
 *     ├─────┼─────┼─────┼─────┤
 *  1  │  🎵  │     │ ┌─────────┐│
 *     ├─────┼─────┤ │ Widget  ││
 *  2  │     │     │ │ 2×2     ││
 *     ├─────┼─────┤ └─────────┘│
 *  3  │  📁  │     │     │     │
 *     ├─────┼─────┼─────┼─────┤
 *  4  │     │     │     │     │
 *     └─────┴─────┴─────┴─────┘
 *
 * 对应矩阵:
 *   cells[0][0]=T  cells[1][0]=T  cells[2][0]=F  cells[3][0]=F
 *   cells[0][1]=T  cells[1][1]=F  cells[2][1]=T  cells[3][1]=T
 *   cells[0][2]=F  cells[1][2]=F  cells[2][2]=T  cells[3][2]=T
 *   cells[0][3]=T  cells[1][3]=F  cells[2][3]=F  cells[3][3]=F
 *   cells[0][4]=F  cells[1][4]=F  cells[2][4]=F  cells[3][4]=F
 */
public class GridOccupancy {

    /** 底层矩阵: cells[x][y] */
    public boolean[][] cells;
    private final int mCountX;
    private final int mCountY;

    public GridOccupancy(int countX, int countY) {
        mCountX = countX;
        mCountY = countY;
        cells = new boolean[countX][countY];
    }

    /**
     * ★ 标记一个区域为已占用
     *
     * @param cellX 起始列
     * @param cellY 起始行
     * @param spanX 横跨列数 (1=单格, 2=两列宽)
     * @param spanY 纵跨行数
     * @param value true=占用, false=释放
     */
    public void markCells(int cellX, int cellY, int spanX, int spanY, boolean value) {
        if (cellX < 0 || cellY < 0) return;
        for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
            for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
                cells[x][y] = value;
            }
        }
    }

    /** 标记 CellInfo 对应的区域 */
    public void markCells(CellInfo info, boolean value) {
        markCells(info.cellX, info.cellY, info.spanX, info.spanY, value);
    }

    /**
     * ★ 检查一个区域是否完全空闲
     *
     * @return true = 所有单元格都空闲, 可以放置
     */
    public boolean isRegionVacant(int cellX, int cellY, int spanX, int spanY) {
        for (int x = cellX; x < cellX + spanX && x < mCountX; x++) {
            for (int y = cellY; y < cellY + spanY && y < mCountY; y++) {
                if (x < 0 || y < 0 || cells[x][y]) {
                    return false;
                }
            }
        }
        return true;
    }

    /** 清空所有占用标记 */
    public void clear() {
        for (int x = 0; x < mCountX; x++) {
            for (int y = 0; y < mCountY; y++) {
                cells[x][y] = false;
            }
        }
    }

    /** 从另一个占用矩阵复制 */
    public void copyTo(GridOccupancy dest) {
        for (int x = 0; x < mCountX; x++) {
            System.arraycopy(cells[x], 0, dest.cells[x], 0, mCountY);
        }
    }

    /**
     * ★ 查找能容纳 spanX×spanY 的最近空位
     *
     * 搜索策略: 从给定位置开始, 按距离由近到远搜索
     *
     * @param cellXY  起始位置 (输入) / 找到的位置 (输出)
     * @param spanX   需要的列数
     * @param spanY   需要的行数
     * @return true = 找到空位
     */
    public boolean findNearestVacantArea(int[] cellXY, int spanX, int spanY) {
        // ... 螺旋搜索或距离排序搜索
        return false; // simplified
    }
}

四、布局系统详解

4.1 坐标系统

CellLayout 坐标系统:

 (0,0) 像素坐标起点
  ┌─────────────────────────────────────────────┐
  │  paddingLeft                   paddingRight  │
  │  ┌───────────────────────────────────────┐  │
  │  │ Cell(0,0)   Cell(1,0)   Cell(2,0)     │  │ paddingTop
  │  │  ┌───┐gap┌───┐gap┌───┐               │  │
  │  │  │   │   │   │   │   │               │  │
  │  │  └───┘   └───┘   └───┘               │  │
  │  │  gap(Y)                               │  │
  │  │  ┌───┐   ┌───┐   ┌───┐               │  │
  │  │  │   │   │   │   │   │               │  │
  │  │  └───┘   └───┘   └───┘               │  │
  │  │  Cell(0,1)                            │  │
  │  └───────────────────────────────────────┘  │
  │                                paddingBottom │
  └─────────────────────────────────────────────┘

两种坐标:
  1. 网格坐标 (cellX, cellY)  — 整数, 表示第几列第几行
  2. 像素坐标 (pixelX, pixelY) — 相对于 CellLayout 左上角的像素位置

核心转换公式:
  pixelX = paddingLeft + cellX * (cellWidth + borderSpaceX)
  pixelY = paddingTop  + cellY * (cellHeight + borderSpaceY)

反向转换:
  cellX = (pixelX - paddingLeft) / (cellWidth + borderSpaceX)
  cellY = (pixelY - paddingTop)  / (cellHeight + borderSpaceY)

4.2 单元格尺寸计算

/**
 * ★ 核心方法: 根据 DeviceProfile 计算网格参数
 *
 * 调用链: DeviceProfile → InvariantDeviceProfile → CellLayout.setGridSize()
 */
public void setGridSize(int countX, int countY) {
    mCountX = countX;
    mCountY = countY;
    mOccupied = new GridOccupancy(countX, countY);
    mTmpOccupied = new GridOccupancy(countX, countY);
    requestLayout();
}

/**
 * ★ onMeasure — 计算单元格尺寸
 *
 * 典型计算逻辑 (简化版):
 *
 * 可用宽度 = MeasureSpec 宽度 - paddingLeft - paddingRight
 * 可用高度 = MeasureSpec 高度 - paddingTop - paddingBottom
 *
 * cellWidth  = (可用宽度 - (mCountX - 1) * borderSpaceX) / mCountX
 * cellHeight = (可用高度 - (mCountY - 1) * borderSpaceY) / mCountY
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    // 从 DeviceProfile 获取配置 (而非自己计算)
    DeviceProfile dp = mActivity.getDeviceProfile();

    mCellWidth = dp.cellWidthPx;
    mCellHeight = dp.cellHeightPx;
    mBorderSpaceX = dp.cellLayoutBorderSpacePx.x;
    mBorderSpaceY = dp.cellLayoutBorderSpacePx.y;

    mShortcutsAndWidgets.setCellDimensions(
        mCellWidth, mCellHeight,
        mBorderSpaceX, mBorderSpaceY,
        mCountX, mCountY);

    int newWidth = widthSize;
    int newHeight = heightSize;

    setMeasuredDimension(newWidth, newHeight);

    // 测量子 View
    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
        newWidth - getPaddingLeft() - getPaddingRight(),
        MeasureSpec.EXACTLY);
    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
        newHeight - getPaddingTop() - getPaddingBottom(),
        MeasureSpec.EXACTLY);

    mShortcutsAndWidgets.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

4.3 子 View 布局 (onLayout)

/**
 * ★ CellLayout.onLayout — 将子 View 放到正确位置
 *
 * CellLayout 只有一个直接子 View: mShortcutsAndWidgets
 * 实际的 Icon/Widget 布局由 ShortcutAndWidgetContainer.onLayout 完成
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int left = getPaddingLeft();
    int top = getPaddingTop();
    int right = r - l - getPaddingRight();
    int bottom = b - t - getPaddingBottom();

    mShortcutsAndWidgets.layout(left, top, right, bottom);
}

/**
 * ShortcutAndWidgetContainer.onLayout — 真正的子 View 定位
 */
// 在 ShortcutAndWidgetContainer 中:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() == GONE) continue;

        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();

        // ★ 根据 cellX, cellY 计算像素坐标
        int childLeft = lp.x;  // 已在 setupLp 中计算好
        int childTop = lp.y;

        child.layout(childLeft, childTop,
                     childLeft + lp.width,
                     childTop + lp.height);
    }
}

4.4 CellLayoutLayoutParams — 布局参数

/**
 * ★ CellLayout.LayoutParams (CellLayoutLayoutParams)
 *
 * 每个放在 CellLayout 中的子 View 都有此参数
 * 它记录了 View 在网格中的位置和跨度
 */
public static class CellLayoutLayoutParams extends MarginLayoutParams {

    /**
     * 网格坐标
     * cellX = 起始列 (0-based)
     * cellY = 起始行 (0-based)
     */
    public int cellX;
    public int cellY;

    /**
     * 临时网格坐标 (拖拽过程中使用)
     * 拖拽结束后会赋值给 cellX/cellY
     */
    public int tmpCellX;
    public int tmpCellY;

    /**
     * 跨度
     * spanX = 横跨列数 (图标=1, 2×2 Widget=2)
     * spanY = 纵跨行数
     */
    public int cellHSpan;
    public int cellVSpan;

    /** 是否可以被重排挤走 */
    public boolean canReorder = true;

    /**
     * 像素坐标 (由 setup() 计算)
     * 在 ShortcutAndWidgetContainer.onLayout 中使用
     */
    public int x;
    public int y;

    /** 是否被拖拽中 */
    public boolean dropped;

    /**
     * ★ setup — 从网格坐标计算像素坐标
     *
     * 核心公式:
     *   x = paddingLeft + cellX * (cellWidth + borderSpaceX)
     *   y = paddingTop + cellY * (cellHeight + borderSpaceY)
     *   width = cellHSpan * cellWidth + (cellHSpan - 1) * borderSpaceX
     *   height = cellVSpan * cellHeight + (cellVSpan - 1) * borderSpaceY
     */
    public void setup(int cellWidth, int cellHeight,
                      boolean invertX, int colCount,
                      int borderSpaceX, int borderSpaceY,
                      Point borderSpace) {

        // RTL 支持
        int myCellX = invertX ? (colCount - cellX - cellHSpan) : cellX;

        // 计算像素位置
        x = myCellX * (cellWidth + borderSpaceX);
        y = cellY * (cellHeight + borderSpaceY);

        // 计算占用的总像素尺寸
        width = cellHSpan * cellWidth + (cellHSpan - 1) * borderSpaceX;
        height = cellVSpan * cellHeight + (cellVSpan - 1) * borderSpaceY;
    }
}

五、放置算法 ⭐⭐⭐

5.1 查找空位

/**
 * ★★★ findNearestArea — CellLayout 最核心的算法
 *
 * 在网格中查找最接近 (pixelX, pixelY) 的空位,
 * 要求能容纳 spanX × spanY 的区域
 *
 * 用途:
 *   1. 拖拽时预览放置位置
 *   2. 安装新应用时自动找位置
 *   3. 旋转屏幕后重新布局
 *
 * @param pixelX      目标像素 X
 * @param pixelY      目标像素 Y
 * @param spanX       需要的列跨度
 * @param spanY       需要的行跨度
 * @param result      输出: 找到的 (cellX, cellY)
 * @param resultSpan  输出: 实际使用的跨度 (Widget 可能缩放)
 * @return 找到的 (cellX, cellY), 未找到返回 null
 */
int[] findNearestArea(int pixelX, int pixelY,
                      int spanX, int spanY,
                      int[] result, int[] resultSpan) {

    // 将像素坐标转换为浮点网格坐标
    double bestDistance = Double.MAX_VALUE;
    int bestX = -1, bestY = -1;

    // 遍历所有可能的放置位置
    for (int x = 0; x <= mCountX - spanX; x++) {
        for (int y = 0; y <= mCountY - spanY; y++) {

            // 检查该区域是否完全空闲
            if (!mOccupied.isRegionVacant(x, y, spanX, spanY)) {
                continue;
            }

            // 计算该位置中心到目标的距离
            // 使用像素距离, 考虑 span 的中心偏移
            double cellCenterX = x * (mCellWidth + mBorderSpaceX)
                                 + spanX * mCellWidth / 2.0;
            double cellCenterY = y * (mCellHeight + mBorderSpaceY)
                                 + spanY * mCellHeight / 2.0;

            double distance = Math.hypot(pixelX - cellCenterX,
                                         pixelY - cellCenterY);

            if (distance < bestDistance) {
                bestDistance = distance;
                bestX = x;
                bestY = y;
            }
        }
    }

    if (bestX >= 0) {
        result[0] = bestX;
        result[1] = bestY;
        return result;
    }

    return null;  // 没有空位
}

5.2 精确像素→网格转换

/**
 * 将像素坐标转换为网格坐标 (最近的单元格)
 *
 * 不考虑占用状态, 纯粹的坐标转换
 */
void pointToCellExact(int x, int y, int[] result) {
    int hStartPadding = getPaddingLeft();
    int vStartPadding = getPaddingTop();

    result[0] = (x - hStartPadding) / (mCellWidth + mBorderSpaceX);
    result[1] = (y - vStartPadding) / (mCellHeight + mBorderSpaceY);

    // 边界钳制
    result[0] = Math.max(0, Math.min(result[0], mCountX - 1));
    result[1] = Math.max(0, Math.min(result[1], mCountY - 1));
}

/**
 * 网格坐标转换为像素坐标 (单元格左上角)
 */
void cellToPoint(int cellX, int cellY, int[] result) {
    int hStartPadding = getPaddingLeft();
    int vStartPadding = getPaddingTop();

    result[0] = hStartPadding + cellX * (mCellWidth + mBorderSpaceX);
    result[1] = vStartPadding + cellY * (mCellHeight + mBorderSpaceY);
}

/**
 * 网格坐标转换为像素坐标 (单元格中心)
 */
void cellToCenterPoint(int cellX, int cellY, int[] result) {
    cellToPoint(cellX, cellY, result);
    result[0] += mCellWidth / 2;
    result[1] += mCellHeight / 2;
}

/**
 * ★ 根据像素坐标计算离哪个单元格中心最近
 *
 * 用于拖拽时确定放置位置
 */
void pointToCellRounded(int x, int y, int[] result) {
    pointToCellExact(x, y, result);
    // 四舍五入到最近的格子
}

六、拖拽系统 ⭐⭐⭐

6.1 拖拽流程总览

拖拽放置流程:

 用户长按图标
      │
      ▼
 ┌────────────────┐
 │ DragController  │  捕获 touch 事件
 │ .startDrag()    │
 └────────┬───────┘
          │
          ▼
 ┌────────────────┐
 │ Workspace       │  确定拖到了哪个 CellLayout
 │ .onDragOver()   │
 └────────┬───────┘
          │
          ▼
 ┌────────────────────────────────────────────────┐
 │ CellLayout.onDragOver()                        │
 │                                                │
 │  1. pointToCellRounded() → 目标网格坐标        │
 │  2. findNearestArea() → 最近空位               │
 │  3. 如果有空位:                                │
 │     └─ 显示放置预览 (高亮目标格子)             │
 │  4. 如果无空位:                                │
 │     └─ attemptReorder() → 尝试挤走其他图标     │
 │        ├─ simulateReorder() → 模拟重排         │
 │        └─ animateReorder() → 播放重排动画      │
 └────────────────────┬───────────────────────────┘
                      │
                      ▼ 用户松手
 ┌────────────────────────────────────────────────┐
 │ CellLayout.onDrop()                            │
 │                                                │
 │  1. 最终确定放置位置                           │
 │  2. addViewToCellLayout() → 添加 View          │
 │  3. markCellsForView() → 更新占用矩阵         │
 │  4. 通知 Launcher 持久化到数据库               │
 └────────────────────────────────────────────────┘

6.2 拖拽核心方法

/**
 * ★ onDragEnter — 拖拽进入此 CellLayout
 */
void onDragEnter() {
    mDragging = true;
    // 清除之前的拖拽状态
    mDragCell[0] = mDragCell[1] = -1;
}

/**
 * ★ onDragExit — 拖拽离开此 CellLayout
 */
void onDragExit() {
    mDragging = false;
    // 清除拖拽预览
    clearDragOutlines();
    // 恢复所有被重排的 View 到原位
    revertTempState();
}

/**
 * ★★★ createAreaForResize — 为 Widget 调整大小创建空间
 *
 * 当用户拖拽 Widget 边缘调整大小时,
 * 需要把占用区域内的图标挤走
 */
public void createAreaForResize(int cellX, int cellY,
                                 int spanX, int spanY,
                                 View dragView, int[] direction,
                                 boolean commit) {
    // 1. 标记需要的区域
    // 2. 查找该区域内的已有 View
    // 3. 尝试将它们推到周围空位
    // 4. 如果 commit=true, 永久保存新位置
}

/**
 * ★ addViewToCellLayout — 向网格中添加一个 View
 *
 * @param child   要添加的 View (BubbleTextView / AppWidgetHostView 等)
 * @param index   插入顺序 (-1 = 末尾)
 * @param childId View 的 ID (对应数据库中的 ItemInfo._id)
 * @param lp      布局参数 (包含 cellX, cellY, spanX, spanY)
 * @param markCells 是否标记占用矩阵
 */
public boolean addViewToCellLayout(View child, int index, int childId,
                                    CellLayoutLayoutParams lp,
                                    boolean markCells) {
    // 1. 检查目标区域是否空闲 (如果不允许重叠)
    if (markCells) {
        if (!mOccupied.isRegionVacant(lp.cellX, lp.cellY,
                                       lp.cellHSpan, lp.cellVSpan)) {
            return false;
        }
    }

    // 2. 将 View 添加到 ShortcutsAndWidgets 容器
    // (不是直接 addView 到 CellLayout!)
    mShortcutsAndWidgets.addView(child, index, lp);

    // 3. 标记占用矩阵
    if (markCells) {
        markCellsAsOccupiedForView(child);
    }

    return true;
}

/**
 * 从网格中移除 View
 */
public void removeView(View view) {
    // 1. 释放占用矩阵
    markCellsAsUnoccupiedForView(view);

    // 2. 从容器移除
    mShortcutsAndWidgets.removeView(view);
}

/**
 * 标记 View 占用的网格区域
 */
public void markCellsAsOccupiedForView(View view) {
    CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();
    mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, true);
}

public void markCellsAsUnoccupiedForView(View view) {
    CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();
    mOccupied.markCells(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, false);
}

七、重排算法 ⭐⭐⭐⭐⭐

重排算法是 CellLayout 中最复杂的部分。当拖拽一个图标到已有图标的位置时,需要将已有图标"推开"。

7.1 重排策略

重排方向策略:

  当拖拽 📱 到 📷 的位置:

  方案1: 向右推
  ┌───┬───┬───┬───┐        ┌───┬───┬───┬───┐
  │ 📱│ 📷│   │   │  ──→   │   │ 📱│ 📷│   │
  └───┴───┴───┴───┘        └───┴───┴───┴───┘

  方案2: 向下推
  ┌───┬───┬───┬───┐        ┌───┬───┬───┬───┐
  │ 📱│ 📷│   │   │  ──→   │ 📱│   │   │   │
  │   │   │   │   │        │   │ 📷│   │   │
  └───┴───┴───┴───┘        └───┴───┴───┴───┘

  方案3: 交换位置
  ┌───┬───┬───┬───┐        ┌───┬───┬───┬───┐
  │ 📱│ 📷│   │   │  ──→   │ 📷│ 📱│   │   │
  └───┴───┴───┴───┘        └───┴───┴───┴───┘

算法选择最小移动代价的方案。

7.2 核心重排实现

/**
 * ★★★ performReorder — 执行重排算法
 *
 * 尝试在目标位置放置一个 spanX×spanY 的项目,
 * 并将冲突的项目推到合适的位置
 *
 * @param pixelX, pixelY  目标像素坐标
 * @param spanX, spanY    需要的跨度
 * @param dragView        被拖拽的 View
 * @param mode            模式 (预览/确认)
 * @return 最终放置的单元格坐标
 */
int[] performReorder(int pixelX, int pixelY,
                     int minSpanX, int minSpanY,
                     int spanX, int spanY,
                     View dragView,
                     int[] result, int[] resultSpan,
                     int mode) {

    // 1. 转换像素坐标到最近的网格坐标
    int[] targetCell = findNearestArea(pixelX, pixelY, spanX, spanY,
                                       mTmpOccupied, result, resultSpan);

    // 2. 如果找到空位, 直接使用
    if (targetCell != null && mTmpOccupied.isRegionVacant(
            targetCell[0], targetCell[1], spanX, spanY)) {
        if (mode == MODE_ON_DROP) {
            return targetCell; // 直接放置
        }
        // MODE_DRAG_OVER: 显示预览
        showDragOutline(targetCell[0], targetCell[1], spanX, spanY);
        return targetCell;
    }

    // 3. ★ 没有空位 → 尝试重排
    //    使用 ItemConfiguration 模拟不同重排方案
    ItemConfiguration bestSolution = findBestSolution(
        pixelX, pixelY, spanX, spanY, dragView);

    if (bestSolution == null) {
        return null; // 完全放不下
    }

    // 4. 应用最佳方案
    if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) {
        commitReorder(bestSolution);
    } else {
        // 预览模式: 动画展示
        animateReorder(bestSolution);
    }

    result[0] = bestSolution.cellX;
    result[1] = bestSolution.cellY;
    return result;
}

/**
 * ★★★ 内部类: ItemConfiguration
 *
 * 表示一种重排方案的完整快照
 * 包含每个被移动的 View 的新位置
 */
private class ItemConfiguration {
    /** 被拖拽项的目标位置 */
    int cellX, cellY;
    int spanX, spanY;

    /**
     * ★ 被重排的 View → 新位置 的映射
     * Key   = 需要移动的 View
     * Value = 该 View 的新 CellAndSpan
     */
    final HashMap<View, CellAndSpan> map = new HashMap<>();

    /** 该方案的占用矩阵 (模拟结果) */
    final GridOccupancy mOccupied;

    /** 是否有效 (所有项目都找到了位置) */
    boolean isSolution = false;

    /** 方案的总移动代价 (越小越好) */
    float cost = 0f;
}

/**
 * ★ CellAndSpan — 单元格坐标 + 跨度
 */
private static class CellAndSpan {
    int cellX, cellY;
    int spanX, spanY;

    CellAndSpan(int x, int y, int sx, int sy) {
        this.cellX = x;  this.cellY = y;
        this.spanX = sx;  this.spanY = sy;
    }
}

/**
 * ★★★ findBestSolution — 搜索最佳重排方案
 *
 * 尝试 4 个方向的推移, 选择代价最小的方案
 */
private ItemConfiguration findBestSolution(
        int pixelX, int pixelY, int spanX, int spanY, View dragView) {

    // 4 个推移方向: 左, 右, 上, 下
    int[][] directions = {
        {-1, 0},  // 左
        { 1, 0},  // 右
        { 0,-1},  // 上
        { 0, 1},  // 下
    };

    ItemConfiguration bestConfig = null;
    float bestCost = Float.MAX_VALUE;

    for (int[] direction : directions) {
        // 创建临时配置
        ItemConfiguration config = new ItemConfiguration();
        config.cellX = /* 目标 cellX */;
        config.cellY = /* 目标 cellY */;
        config.spanX = spanX;
        config.spanY = spanY;

        // ★ 核心: 模拟推移
        if (pushViewsToTempLocation(
                config, direction, dragView, spanX, spanY)) {

            // 计算代价 = 所有被移动 View 的距离之和
            float cost = computeCost(config);

            if (cost < bestCost) {
                bestCost = cost;
                bestConfig = config;
            }
        }
    }

    return bestConfig;
}

/**
 * ★★★ pushViewsToTempLocation — 递归推移算法
 *
 * 将占据目标区域的 View 向指定方向推移,
 * 如果推移后又与其他 View 冲突, 则递归推移
 *
 * @param config    当前方案配置
 * @param direction 推移方向 {dx, dy}
 * @param dragView  正在拖拽的 View (不参与被推)
 * @return true = 成功找到方案, false = 推不动
 */
private boolean pushViewsToTempLocation(
        ItemConfiguration config,
        int[] direction,
        View dragView,
        int spanX, int spanY) {

    // 1. 找出目标区域内所有被冲突的 View
    ArrayList<View> conflicting = findConflictingViews(
        config.cellX, config.cellY, spanX, spanY, dragView);

    if (conflicting.isEmpty()) {
        return true; // 没有冲突, 直接放置
    }

    // 2. 逐个尝试推移冲突的 View
    for (View view : conflicting) {
        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();

        // 尝试在推移方向找空位
        int newX = lp.cellX + direction[0];
        int newY = lp.cellY + direction[1];

        // 边界检查
        while (newX >= 0 && newX + lp.cellHSpan <= mCountX &&
               newY >= 0 && newY + lp.cellVSpan <= mCountY) {

            // 检查新位置是否空闲 (在模拟矩阵中)
            if (config.mOccupied.isRegionVacant(
                    newX, newY, lp.cellHSpan, lp.cellVSpan)) {
                // 找到空位! 记录到方案中
                config.map.put(view, new CellAndSpan(
                    newX, newY, lp.cellHSpan, lp.cellVSpan));
                config.mOccupied.markCells(
                    newX, newY, lp.cellHSpan, lp.cellVSpan, true);
                break;
            }

            // 继续沿方向移动
            newX += direction[0];
            newY += direction[1];
        }

        // 如果某个 View 推不动, 该方案失败
        if (!config.map.containsKey(view)) {
            return false;
        }
    }

    return true;
}

/**
 * 计算重排方案的代价
 *
 * 代价 = 所有被移动 View 的曼哈顿距离之和
 * 距离越小, 方案越优
 */
private float computeCost(ItemConfiguration config) {
    float totalCost = 0f;

    for (Map.Entry<View, CellAndSpan> entry : config.map.entrySet()) {
        View view = entry.getKey();
        CellAndSpan newPos = entry.getValue();
        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();

        // 曼哈顿距离
        float dist = Math.abs(newPos.cellX - lp.cellX)
                   + Math.abs(newPos.cellY - lp.cellY);

        totalCost += dist;
    }

    return totalCost;
}

/**
 * ★ commitReorder — 确认重排方案, 将 View 移动到新位置
 */
private void commitReorder(ItemConfiguration config) {
    for (Map.Entry<View, CellAndSpan> entry : config.map.entrySet()) {
        View view = entry.getKey();
        CellAndSpan newPos = entry.getValue();

        // 更新布局参数
        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();
        lp.cellX = newPos.cellX;
        lp.cellY = newPos.cellY;
        lp.cellHSpan = newPos.spanX;
        lp.cellVSpan = newPos.spanY;

        // 重新布局
        view.requestLayout();

        // 更新数据库 (通过 ItemInfo)
        Object tag = view.getTag();
        if (tag instanceof ItemInfo) {
            ItemInfo info = (ItemInfo) tag;
            info.cellX = newPos.cellX;
            info.cellY = newPos.cellY;
            // LauncherModel.updateItemInDatabase(...)
        }
    }

    // 重建占用矩阵
    markCellsForAllChildren();
}

/**
 * ★ animateReorder — 播放重排动画
 *
 * 被推移的 View 从原位置平滑动画到新位置
 */
private void animateReorder(ItemConfiguration config) {

    for (Map.Entry<View, CellAndSpan> entry : config.map.entrySet()) {
        View view = entry.getKey();
        CellAndSpan newPos = entry.getValue();
        CellLayoutLayoutParams lp = (CellLayoutLayoutParams) view.getLayoutParams();

        // 计算动画起止像素坐标
        int[] oldPixel = new int[2];
        int[] newPixel = new int[2];
        cellToPoint(lp.cellX, lp.cellY, oldPixel);
        cellToPoint(newPos.cellX, newPos.cellY, newPixel);

        // 取消之前的动画
        Animator oldAnim = mReorderAnimators.get(lp);
        if (oldAnim != null) oldAnim.cancel();

        // 创建平移动画
        ObjectAnimator animX = ObjectAnimator.ofFloat(
            view, "translationX",
            oldPixel[0] - newPixel[0], 0f);
        ObjectAnimator animY = ObjectAnimator.ofFloat(
            view, "translationY",
            oldPixel[1] - newPixel[1], 0f);

        AnimatorSet set = new AnimatorSet();
        set.playTogether(animX, animY);
        set.setDuration(REORDER_ANIMATION_DURATION);
        set.setInterpolator(new DecelerateInterpolator(1.5f));

        mReorderAnimators.put(lp, set);

        // 先更新布局参数到新位置
        lp.tmpCellX = newPos.cellX;
        lp.tmpCellY = newPos.cellY;

        set.start();
    }
}

八、绘制系统

8.1 onDraw

/**
 * ★ CellLayout.onDraw — 绘制网格视觉元素
 *
 * CellLayout 自身绘制:
 *   1. 拖拽目标区域高亮
 *   2. 拖拽轮廓预览
 *   3. 文件夹拖出残影
 *   4. 调试模式下的网格线
 *
 * 子 View (图标/Widget) 的绘制由 ShortcutsAndWidgets 处理
 */
@Override
protected void onDraw(Canvas canvas) {
    // 1. 绘制拖拽轮廓 (Drop target outline)
    if (mDragging && mDragOutline != null) {
        int[] loc = new int[2];
        cellToPoint(mDragCell[0], mDragCell[1], loc);

        // 拖拽轮廓带半透明效果
        mDragOutline.setAlpha((int)(mDragOutlineAlpha * 255));
        mDragOutline.setBounds(
            loc[0], loc[1],
            loc[0] + mDragOutline.getIntrinsicWidth(),
            loc[1] + mDragOutline.getIntrinsicHeight());
        mDragOutline.draw(canvas);
    }

    // 2. 绘制文件夹拖出后的"残影"效果
    if (mFolderLeaveBehindCell[0] >= 0) {
        drawFolderLeaveBehind(canvas);
    }

    // 3. (调试模式) 绘制网格线
    if (DEBUG_VISUALIZE_OCCUPIED) {
        drawOccupiedGrid(canvas);
    }
}

/**
 * 调试: 可视化占用矩阵
 */
private void drawOccupiedGrid(Canvas canvas) {
    Paint paint = new Paint();

    for (int x = 0; x < mCountX; x++) {
        for (int y = 0; y < mCountY; y++) {
            int[] pixel = new int[2];
            cellToPoint(x, y, pixel);

            if (mOccupied.cells[x][y]) {
                paint.setColor(0x44FF0000); // 红色 = 已占用
            } else {
                paint.setColor(0x4400FF00); // 绿色 = 空闲
            }

            canvas.drawRect(
                pixel[0], pixel[1],
                pixel[0] + mCellWidth,
                pixel[1] + mCellHeight,
                paint);

            // 绘制格子边框
            paint.setColor(0x22FFFFFF);
            paint.setStyle(Paint.Style.STROKE);
            canvas.drawRect(
                pixel[0], pixel[1],
                pixel[0] + mCellWidth,
                pixel[1] + mCellHeight,
                paint);
            paint.setStyle(Paint.Style.FILL);
        }
    }
}

九、与其他组件的交互

9.1 组件交互图

┌──────────────────────────────────────────────────────────┐
│                      DragLayer                           │
│  (Touch 事件分发根节点, 拖拽 DragView 的绘制容器)       │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │                   Workspace                       │   │
│  │  (PagedView 子类, 管理多个 CellLayout 页面)      │   │
│  │                                                   │   │
│  │  ★ 关键交互:                                     │   │
│  │  • 确定 touch 在哪个 CellLayout 上                │   │
│  │  • 跨页面拖拽时切换 CellLayout                    │   │
│  │  • 调用 CellLayout 的 onDragOver/onDrop           │   │
│  │                                                   │   │
│  │  ┌─────────────┐  ┌─────────────┐               │   │
│  │  │  CellLayout  │  │  CellLayout  │  ...         │   │
│  │  │  (Page 0)    │  │  (Page 1)    │              │   │
│  │  └──────┬──────┘  └─────────────┘               │   │
│  │         │                                         │   │
│  └─────────┼─────────────────────────────────────────┘   │
│            │                                              │
│            ▼                                              │
│  ┌────────────────────────────────────┐                  │
│  │ ShortcutAndWidgetContainer         │                  │
│  │ (CellLayout 的唯一直接子 View)    │                  │
│  │                                    │                  │
│  │ ├── BubbleTextView (App Icon)     │                  │
│  │ ├── BubbleTextView (Shortcut)     │                  │
│  │ ├── AppWidgetHostView (Widget)    │                  │
│  │ └── FolderIcon (文件夹)           │                  │
│  └────────────────────────────────────┘                  │
└──────────────────────────────────────────────────────────┘

┌─────────────────┐    ┌─────────────────┐
│ DragController   │    │ LauncherModel    │
│                  │    │                  │
│ 管理拖拽生命周期  │    │ 数据持久化       │
│ • startDrag()    │◄──►│ • updateItem()   │
│ • dispatchMove() │    │ • addItem()      │
│ • onDrop()       │    │ • deleteItem()   │
└────────┬────────┘    └─────────────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ ItemInfo         │    │ DeviceProfile    │
│                  │    │                  │
│ 每个桌面项目的    │    │ 设备参数配置     │
│ 数据模型:        │    │ • countX/Y       │
│ • cellX, cellY   │    │ • cellWidthPx    │
│ • spanX, spanY   │    │ • cellHeightPx   │
│ • container      │    │ • borderSpace    │
│ • screenId       │    │ • iconSizePx     │
└─────────────────┘    └─────────────────┘

9.2 Workspace ↔ CellLayout 协作

/**
 * Workspace 中调用 CellLayout 的典型流程
 */

// === 拖拽过程 ===

// Workspace.onDragOver() 中:
void onDragOver(DragObject d) {
    // 1. 根据拖拽坐标确定当前 CellLayout
    CellLayout layout = getCurrentDropLayout();

    // 2. 如果切换到了新 CellLayout
    if (layout != mDragTargetLayout) {
        if (mDragTargetLayout != null) {
            mDragTargetLayout.onDragExit();     // 离开旧页
        }
        mDragTargetLayout = layout;
        mDragTargetLayout.onDragEnter();        // 进入新页
    }

    // 3. 让 CellLayout 计算放置位置
    int[] targetCell = new int[2];
    layout.performReorder(
        d.x, d.y,
        item.minSpanX, item.minSpanY,
        item.spanX, item.spanY,
        d.dragView.getView(),
        targetCell, null,
        CellLayout.MODE_DRAG_OVER);
}

// Workspace.onDrop() 中:
void onDrop(DragObject d) {
    CellLayout layout = mDragTargetLayout;

    // 1. 最终确定放置位置
    int[] targetCell = new int[2];
    layout.performReorder(
        d.x, d.y,
        item.minSpanX, item.minSpanY,
        item.spanX, item.spanY,
        d.dragView.getView(),
        targetCell, null,
        CellLayout.MODE_ON_DROP);

    // 2. 添加到 CellLayout
    if (targetCell != null) {
        layout.addViewToCellLayout(view, -1, info.id,
            new CellLayoutLayoutParams(
                targetCell[0], targetCell[1],
                item.spanX, item.spanY),
            true);

        // 3. 持久化到数据库
        LauncherModel.modifyItemInDatabase(
            getContext(), info,
            LauncherSettings.Favorites.CONTAINER_DESKTOP,
            layout.getPageIndex(),
            targetCell[0], targetCell[1],
            item.spanX, item.spanY);
    }
}

// === 新应用安装时自动放置 ===

// 调用链: PackageInstallReceiver → LauncherModel → Workspace

boolean findEmptyCellAndOccupy(ItemInfo info) {
    // 遍历所有 CellLayout 找空位
    for (CellLayout layout : getWorkspaceScreens()) {
        if (layout.findCellForSpan(info.spanX, info.spanY, result)) {
            // 找到空位
            info.cellX = result[0];
            info.cellY = result[1];
            info.screenId = layout.getScreenId();
            return true;
        }
    }
    return false; // 所有页面都满了
}

9.3 Hotseat 中的 CellLayout

/**
 * Hotseat 底部快捷栏也使用 CellLayout
 *
 * 区别:
 *   - 通常是 1 行 N 列 (如 5×1)
 *   - 不支持 Widget (只支持图标/文件夹)
 *   - 不支持多页
 *   - 有特殊的 All Apps 按钮位置
 */
public class Hotseat extends CellLayout {
    // 或在新版中:
    public class Hotseat extends FrameLayout {
        private CellLayout mContent;  // 内部使用 CellLayout

        void setup() {
            mContent.setGridSize(
                dp.numHotseatIcons, 1);  // N 列 × 1 行
        }
    }
}

十、性能优化

/**
 * CellLayout 中的性能优化策略
 */

// ============================================================
// 1. 避免频繁重建占用矩阵
// ============================================================

/**
 * 使用 mTmpOccupied 进行拖拽时的模拟计算,
 * 不修改真正的 mOccupied
 * 只在 drop 确认时才更新 mOccupied
 */
void onDragOver(...) {
    mOccupied.copyTo(mTmpOccupied);  // 复制到临时矩阵
    // 所有模拟操作在 mTmpOccupied 上进行
}

// ============================================================
// 2. 动画复用
// ============================================================

/**
 * mReorderAnimators 缓存: 避免重复创建动画对象
 * 拖拽过程中如果同一个 View 需要新动画, 取消旧的再创建
 */

// ============================================================
// 3. 减少 requestLayout 调用
// ============================================================

/**
 * 批量更新时使用 mItemPlacementDirty 标记
 * 而不是每次修改都 requestLayout
 */
void markDirty() {
    mItemPlacementDirty = true;
    // 在下一帧统一处理
}

// ============================================================
// 4. 硬件加速层
// ============================================================

/**
 * 拖拽时对 CellLayout 设置硬件加速层
 * 拖拽结束后恢复
 */
void onDragEnter() {
    setLayerType(LAYER_TYPE_HARDWARE, null);
}

void onDragExit() {
    setLayerType(LAYER_TYPE_NONE, null);
}

// ============================================================
// 5. 预计算像素坐标
// ============================================================

/**
 * CellLayoutLayoutParams.setup() 在 measure 阶段预计算
 * 而不是在 layout/draw 时计算
 * 避免每帧重复计算
 */

十一、关键难点总结

┌─────────────────────────────────────────────────────────────┐
│                CellLayout 核心难点                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│ 1. 重排算法 (Reorder)                                      │
│    • 多方向推移 + 递归冲突解决                              │
│    • 最优方案搜索 (最小移动代价)                            │
│    • 动画与逻辑分离 (模拟 vs 提交)                         │
│                                                             │
│ 2. Widget 处理                                              │
│    • 多格跨度 (spanX×spanY)                                │
│    • resize 时的空间创建                                    │
│    • 最小/最大跨度约束 (minSpan/maxSpan)                   │
│                                                             │
│ 3. 坐标系转换                                               │
│    • 像素 ↔ 网格 双向转换                                  │
│    • RTL (Right-to-Left) 镜像                              │
│    • 多分辨率/DPI 适配                                     │
│    • 旋转屏幕后重新布局                                    │
│                                                             │
│ 4. 拖拽视觉反馈                                            │
│    • 拖拽轮廓预览 (drag outline)                           │
│    • 重排动画 (平移 + 缩放)                                │
│    • 文件夹打开/关闭动画                                   │
│    • 跨页面拖拽时的页面切换                                │
│                                                             │
│ 5. 状态一致性                                               │
│    • 占用矩阵 ↔ 实际子 View 同步                          │
│    • 拖拽中的临时状态 vs 持久化状态                        │
│    • 多线程: UI 线程 vs Model 线程                         │
│    • 配置变更 (旋转) 后状态恢复                            │
│                                                             │
│ 6. 自适应网格                                               │
│    • 不同设备/屏幕尺寸的网格行列数                         │
│    • DeviceProfile 驱动的布局参数                           │
│    • 可变网格 (Android 12+ 支持 Widget 建议尺寸)           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
CellLayout 核心方法调用频率:

高频 (每次拖拽移动):
  ├── pointToCellRounded()     — 每次 touch move
  ├── findNearestArea()        — 每次 touch move
  ├── performReorder()         — 目标变化时
  │    ├── findConflictingViews()
  │    ├── pushViewsToTempLocation()
  │    └── animateReorder()
  └── onDraw() (drag outline)

中频 (拖拽开始/结束):
  ├── onDragEnter() / onDragExit()
  ├── addViewToCellLayout()
  ├── removeView()
  ├── markCellsAsOccupied/Unoccupied()
  └── commitReorder()

低频 (初始化/配置变更):
  ├── setGridSize()
  ├── onMeasure() / onLayout()
  └── DeviceProfile 参数更新

CellLayout 是 Launcher3 中代码量最大、逻辑最复杂的单个类,核心在于占用矩阵管理重排算法。理解了这两个核心,就掌握了 CellLayout 90% 的设计意图。