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
相关产品推荐
相关产品推荐

