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

Flutter中如何将CustomMultiChildLayout与Draggable结合使用?

解决CustomMultiChildLayout拖拽卡片后重新布局的问题

你当前的问题核心是没有把卡片的位置状态交给State管理,Delegate无法感知位置变化。原来的Stack(Positioned)是通过setState更新top/left触发重绘,而CustomMultiChildLayout需要依赖传入的状态数据变化来触发Delegate重新布局。

核心修改思路

  1. 在State中维护每个卡片的位置映射,代替Delegate内部的临时位置
  2. 拖拽结束时更新对应卡片的位置并调用setState,让Delegate感知变化
  3. 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;
  }
}

关键修改点说明

  1. State维护位置映射:用_cardPositions存储每个卡片GlobalKey对应的位置,拖拽结束时更新这个map并调用setState
  2. Delegate依赖状态数据:Delegate接收cardPositions,在performLayout中根据这个map定位每个卡片
  3. 正确的重布局判断:shouldRelayout对比新旧Delegate的位置数据,有变化则返回true触发重新布局
  4. 修复CustomPainter问题:原来的EdgePainter在paint方法中修改全局数据,现在改为由State管理positions列表,仅在paint中绘制,shouldRepaint正确判断是否重绘

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 06:12:05