You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter组件的拖拽、缩放与自定义网格重排实现求助

实现Flutter安卓桌面风格网格瓦片组件方案

核心思路

放弃单纯依赖Draggable的原生偏移逻辑,改用数据驱动的网格状态管理:用模型记录每个瓦片的位置、尺寸,通过拖拽时的坐标计算映射到网格行列,配合碰撞检测实现吸附、缩放和重排。


1. 定义瓦片数据模型

先统一管理瓦片的核心属性,所有操作基于模型更新:

class GridTile {
  final String id;
  int row;      // 起始行
  int col;      // 起始列
  int width;    // 占用列数(1/2/3...)
  int height;   // 占用行数(1/2/5...)

  GridTile({
    required this.id,
    required this.row,
    required this.col,
    required this.width,
    required this.height,
  });
}

2. 网格吸附实现

拖拽时将全局坐标转换为网格行列,而非直接使用像素偏移:

// 定义网格基础参数
const double cellSize = 80; // 单个网格单元格尺寸
const int gridMaxCols = 10;
const int gridMaxRows = 10;

// 计算拖拽位置对应的网格行列
Offset getGridCoords(Offset globalOffset, BuildContext context) {
  final renderBox = context.findRenderObject() as RenderBox;
  final localOffset = renderBox.globalToLocal(globalOffset);
  
  // 转换为网格行列,同时做边界限制
  final targetCol = (localOffset.dx / cellSize).floor().clamp(0, gridMaxCols - 1);
  final targetRow = (localOffset.dy / cellSize).floor().clamp(0, gridMaxRows - 1);
  
  return Offset(targetCol.toDouble(), targetRow.toDouble());
}

在Draggable的onDragEnd回调中,用上述方法计算目标位置,更新对应瓦片的row和col。

3. 多尺寸缩放实现

给瓦片添加缩放手柄,通过手势更新尺寸,同时校验边界和碰撞:

// 瓦片内部的缩放手柄
GestureDetector(
  onPanUpdate: (details) {
    setState(() {
      // 按单元格粒度调整尺寸
      final deltaCol = (details.delta.dx / cellSize).round();
      final deltaRow = (details.delta.dy / cellSize).round();
      
      // 限制最小尺寸1x1,且不超出网格边界
      tile.width = (tile.width + deltaCol).clamp(1, gridMaxCols - tile.col);
      tile.height = (tile.height + deltaRow).clamp(1, gridMaxRows - tile.row);
    });
  },
  child: Positioned(
    bottom: 4,
    right: 4,
    child: Container(
      width: 24,
      height: 24,
      decoration: BoxDecoration(
        color: Colors.grey.shade700,
        borderRadius: BorderRadius.circular(4),
      ),
      child: const Icon(Icons.drag_handle, size: 16, color: Colors.white),
    ),
  ),
)

4. 碰撞检测与重排/交换

碰撞检测工具函数

判断目标位置是否被其他瓦片占用:

bool isTileOverlap(GridTile targetTile, List<GridTile> allTiles, String excludeId) {
  for (final tile in allTiles) {
    if (tile.id == excludeId) continue;
    
    // 矩形碰撞判断逻辑
    final targetRight = targetTile.col + targetTile.width;
    final targetBottom = targetTile.row + targetTile.height;
    final tileRight = tile.col + tile.width;
    final tileBottom = tile.row + tile.height;
    
    if (targetTile.col < tileRight && targetRight > tile.col 
        && targetTile.row < tileBottom && targetBottom > tile.row) {
      return true;
    }
  }
  return false;
}

交换逻辑

当拖拽目标位置被占用时,直接交换两个瓦片的位置:

void swapTiles(GridTile draggedTile, GridTile targetTile) {
  setState(() {
    // 临时存储目标瓦片位置
    final tempRow = targetTile.row;
    final tempCol = targetTile.col;
    
    // 交换位置
    targetTile.row = draggedTile.row;
    targetTile.col = draggedTile.col;
    draggedTile.row = tempRow;
    draggedTile.col = tempCol;
  });
}

自动重排逻辑

若目标位置无法直接交换,尝试将重叠瓦片向右侧/下侧移动,直到腾出空间:

void rearrangeTiles(GridTile draggedTile, List<GridTile> allTiles) {
  final overlappingTiles = allTiles.where((tile) {
    return tile.id != draggedTile.id && isTileOverlap(draggedTile, [tile], draggedTile.id);
  }).toList();

  for (final tile in overlappingTiles) {
    // 尝试向右移动
    int newCol = tile.col + 1;
    while (newCol + tile.width <= gridMaxCols 
           && isTileOverlap(tile.copyWith(col: newCol), allTiles, tile.id)) {
      newCol++;
    }
    // 右侧无空间则向下移动
    if (newCol + tile.width > gridMaxCols) {
      int newRow = tile.row + 1;
      while (newRow + tile.height <= gridMaxRows 
             && isTileOverlap(tile.copyWith(row: newRow), allTiles, tile.id)) {
        newRow++;
      }
      if (newRow + tile.height <= gridMaxRows) {
        setState(() => tile.row = newRow);
      }
    } else {
      setState(() => tile.col = newCol);
    }
  }
}

5. 布局渲染

用Stack配合Positioned渲染所有瓦片,根据模型的行列和尺寸计算位置:

Stack(
  children: allTiles.map((tile) {
    return Positioned(
      left: tile.col * cellSize,
      top: tile.row * cellSize,
      width: tile.width * cellSize,
      height: tile.height * cellSize,
      child: Draggable<GridTile>(
        data: tile,
        feedback: _buildTilePreview(tile), // 拖拽时的预览
        childWhenDragging: Container(color: Colors.transparent),
        onDragEnd: (details) {
          final targetCoords = getGridCoords(details.offset, context);
          setState(() {
            tile.col = targetCoords.dx.toInt();
            tile.row = targetCoords.dy.toInt();
          });
          // 检查碰撞并处理重排/交换
          handleCollision(tile, allTiles);
        },
        child: _buildTileContent(tile), // 瓦片实际内容
      ),
    );
  }).toList(),
)

内容的提问来源于stack exchange,提问作者PWebber

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 16:10:55