Flutter中如何将CustomMultiChildLayout与Draggable结合使用?
解决CustomMultiChildLayout拖拽卡片后重新布局的问题
你当前的问题核心是没有把卡片的位置状态交给State管理,Delegate无法感知位置变化。原来的Stack(Positioned)是通过setState更新top/left触发重绘,而CustomMultiChildLayout需要依赖传入的状态数据变化来触发Delegate重新布局。
核心修改思路
- 在State中维护每个卡片的位置映射,代替Delegate内部的临时位置
- 拖拽结束时更新对应卡片的位置并调用setState,让Delegate感知变化
- Delegate通过对比新旧位置数据,决定是否重新布局
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 假设的依赖组件实现 class DetailCardWidget extends StatelessWidget { final Function(Offset) callback; const DetailCardWidget({super.key, required this.callback}); @override Widget build(BuildContext context) { return Container( width: 150, height: 100, color: Colors.grey[200], child: const Center(child: Text('卡片')), ); } } class Background extends StatelessWidget { const Background({super.key}); @override Widget build(BuildContext context) { return Container(color: Colors.white); } } class Menu extends StatelessWidget { final VoidCallback callback; const Menu({super.key, required this.callback}); @override Widget build(BuildContext context) { return ElevatedButton(onPressed: callback, child: const Text('添加卡片')); } } Offset start = Offset.infinite; Offset end = Offset.infinite; var positions = <(Offset, Offset)>[]; class InfiniteCanvas extends ConsumerStatefulWidget { const InfiniteCanvas({super.key}); @override ConsumerState<ConsumerStatefulWidget> createState() => _InfiniteCanvasState(); } class _InfiniteCanvasState extends ConsumerState<InfiniteCanvas> { double cx = 1000.0; double cy = 1000.0; bool notSetStartPosition = true; var detailCardWidgets = <LayoutId>[]; // 核心:维护每个卡片的位置映射 final Map<GlobalKey, Offset> _cardPositions = {}; checkCallbackStatus(pos) { if (notSetStartPosition) { setEdgeStartPositionCallback(pos); } else { setEdgeEndPositionCallback(pos); } } setEdgeStartPositionCallback(pos) { setState(() { start = pos; notSetStartPosition = false; }); } setEdgeEndPositionCallback(pos) { setState(() { end = pos; notSetStartPosition = true; if (start != Offset.infinite && end != Offset.infinite) { positions.add((start, end)); start = Offset.infinite; end = Offset.infinite; } }); } createDetailCardWidget() { final GlobalKey key = GlobalKey(); // 初始化卡片位置:从上到下排列,间距5 final initialOffset = Offset(0, detailCardWidgets.length * (100 + 5)); setState(() { _cardPositions[key] = initialOffset; detailCardWidgets.add(LayoutId( id: key, child: Draggable( feedback: DetailCardWidget(callback: checkCallbackStatus), onDragEnd: (details) { // 将全局坐标转换为画布本地坐标(适配InteractiveViewer) final renderBox = context.findRenderObject() as RenderBox?; final localOffset = renderBox?.globalToLocal(details.offset) ?? details.offset; setState(() { _cardPositions[key] = localOffset; }); }, child: DetailCardWidget(callback: checkCallbackStatus), ), )); }); } @override Widget build(BuildContext context) { return Listener( child: LayoutBuilder( builder: (context, constraints) { return Stack( children: [ InteractiveViewer( boundaryMargin: const EdgeInsets.all(0), clipBehavior: Clip.none, constrained: false, child: SizedBox( width: 2 * cx, height: 2 * cy, child: Stack( children: [ const Background(), CustomMultiChildLayout( delegate: DetailCardWidgetsDelegate(_cardPositions, detailCardWidgets), children: [...detailCardWidgets], ), CustomPaint( painter: EdgePainter(positions: positions), ), ], ), ), ), Positioned( left: 10, bottom: 10, child: Menu(callback: createDetailCardWidget), ), ], ); }, ), ); } } class EdgePainter extends CustomPainter { final List<(Offset, Offset)> positions; EdgePainter({required this.positions}); @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.blue; for (var i in positions) { if (i.$1 != Offset.infinite && i.$2 != Offset.infinite) { canvas.drawLine(i.$1, i.$2, paint); } } } @override bool shouldRepaint(covariant EdgePainter oldDelegate) { // 当连线列表变化时重绘 return oldDelegate.positions.length != positions.length; } } class DetailCardWidgetsDelegate extends MultiChildLayoutDelegate { final Map<GlobalKey, Offset> cardPositions; final List<GlobalKey> layoutIds; DetailCardWidgetsDelegate(this.cardPositions, List<LayoutId> list) : layoutIds = list.map((e) => e.id as GlobalKey).toList(); @override void performLayout(Size size) { for (final key in layoutIds) { if (hasChild(key)) { // 让子控件自行决定大小,仅限制在画布范围内 final childSize = layoutChild(key, BoxConstraints.loose(size)); final position = cardPositions[key] ?? Offset.zero; positionChild(key, position); } } } @override bool shouldRelayout(covariant DetailCardWidgetsDelegate oldDelegate) { // 卡片数量变化或位置变化时触发重布局 if (oldDelegate.layoutIds.length != layoutIds.length) return true; for (final key in layoutIds) { if (oldDelegate.cardPositions[key] != cardPositions[key]) { return true; } } return false; } }
关键修改点说明
- State维护位置映射:用
_cardPositions存储每个卡片GlobalKey对应的位置,拖拽结束时更新这个map并调用setState - Delegate依赖状态数据:Delegate接收
cardPositions,在performLayout中根据这个map定位每个卡片 - 正确的重布局判断:
shouldRelayout对比新旧Delegate的位置数据,有变化则返回true触发重新布局 - 修复CustomPainter问题:原来的EdgePainter在paint方法中修改全局数据,现在改为由State管理positions列表,仅在paint中绘制,
shouldRepaint正确判断是否重绘
内容的提问来源于stack exchange,提问作者user23295538
相关产品推荐
相关产品推荐

