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

如何在Flutter中实现带滑块的可调节2D模型?

Flutter分段式可交互2D蘑菇模型实现方案

一、优化CustomPaint的绘制质感

直接用直线、矩形堆出来的模型必然粗糙,换用更贴合自然形态的绘制方式:

  • 贝塞尔曲线替代基础图形:菌柄的弯曲用二次/三次贝塞尔曲线实现,菌盖用Path.arcTo或椭圆路径绘制,避免生硬的几何形状。
  • 渐变与阴影增强层次:给菌盖加径向渐变(模拟顶部亮、边缘暗的光影),菌柄用线性渐变,同时给每个分段的Paint设置阴影,让模型更有立体感。
  • 强制抗锯齿:在Paint里开启isAntiAlias: true,消除边缘锯齿感。

二、分段式模块化绘制

把蘑菇拆成独立分段后,单独控制每个部分的参数会更灵活:

  • 封装分段绘制函数:比如drawBase()、drawLowerStem()、drawCap(),每个函数接收宽度、高度、曲率等参数,单独处理绘制逻辑。
  • 动态计算分段尺寸:根据总长度滑块的值,按预设比例分配各分段的高度(比如基座占总长度10%,下菌柄占30%),宽度则直接用对应滑块的数值。

三、交互状态与曲率控制

用StatefulWidget管理所有滑块参数,实现实时刷新:

  • 参数绑定滑块:总长度、各分段宽度分别对应独立滑块,曲率则用一个范围为-1~1的滑块控制弯曲方向和程度,再用下拉框选择曲率起始的分段。
  • 曲率应用逻辑:针对选定的起始分段及之后的部分,在绘制路径时调整贝塞尔曲线的控制点位置,比如曲率为正向右弯,为负向左弯。

四、核心代码示例

class MushroomPainter extends CustomPainter {
  final double totalLength;
  final double baseWidth;
  final double lowerStemWidth;
  final double upperStemWidth;
  final double capBaseWidth;
  final double capWidth;
  final int curvatureStartIndex;
  final double curvature;

  MushroomPainter({
    required this.totalLength,
    required this.baseWidth,
    required this.lowerStemWidth,
    required this.upperStemWidth,
    required this.capBaseWidth,
    required this.capWidth,
    required this.curvatureStartIndex,
    required this.curvature,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..isAntiAlias = true;
    // 按比例分配各分段高度
    final segmentHeights = [
      totalLength * 0.1,
      totalLength * 0.3,
      totalLength * 0.3,
      totalLength * 0.05,
      totalLength * 0.25,
    ];
    double currentY = size.height - totalLength; // 从画布底部开始绘制

    // 绘制基座
    paint.color = Colors.brown[700]!;
    if (curvatureStartIndex <= 0) {
      final basePath = Path()
        ..moveTo(size.width/2 - baseWidth/2, currentY)
        ..quadraticBezierTo(
          size.width/2 + baseWidth/2 * curvature,
          currentY + segmentHeights[0]/2,
          size.width/2 + baseWidth/2,
          currentY,
        )
        ..lineTo(size.width/2 + baseWidth/2, currentY + segmentHeights[0])
        ..lineTo(size.width/2 - baseWidth/2, currentY + segmentHeights[0])
        ..close();
      canvas.drawPath(basePath, paint);
    } else {
      final baseRect = RRect.fromRectAndRadius(
        Rect.fromLTWH(
          size.width / 2 - baseWidth/2,
          currentY,
          baseWidth,
          segmentHeights[0],
        ),
        const Radius.circular(8),
      );
      canvas.drawRRect(baseRect, paint);
    }
    currentY += segmentHeights[0];

    // 绘制下菌柄
    paint.color = Colors.beige[200]!;
    paint.shadowColor = Colors.grey[400]!;
    paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
    final curvatureOffset = curvatureStartIndex <=1 ? curvature * 20 : 0;
    final lowerStemPath = Path()
      ..moveTo(size.width/2 - lowerStemWidth/2, currentY)
      ..quadraticBezierTo(
        size.width/2 - lowerStemWidth/2 + curvatureOffset,
        currentY + segmentHeights[1]/2,
        size.width/2 - upperStemWidth/2,
        currentY + segmentHeights[1],
      )
      ..quadraticBezierTo(
        size.width/2 + upperStemWidth/2 + curvatureOffset,
        currentY + segmentHeights[1]/2,
        size.width/2 + lowerStemWidth/2,
        currentY,
      )
      ..close();
    canvas.drawPath(lowerStemPath, paint);
    currentY += segmentHeights[1];

    // 绘制上菌柄
    final upperCurvatureOffset = curvatureStartIndex <=2 ? curvature * 15 : 0;
    final upperStemPath = Path()
      ..moveTo(size.width/2 - upperStemWidth/2, currentY)
      ..quadraticBezierTo(
        size.width/2 - upperStemWidth/2 + upperCurvatureOffset,
        currentY + segmentHeights[2]/2,
        size.width/2 - capBaseWidth/2,
        currentY + segmentHeights[2],
      )
      ..quadraticBezierTo(
        size.width/2 + capBaseWidth/2 + upperCurvatureOffset,
        currentY + segmentHeights[2]/2,
        size.width/2 + upperStemWidth/2,
        currentY,
      )
      ..close();
    canvas.drawPath(upperStemPath, paint);
    currentY += segmentHeights[2];

    // 绘制菌盖基座
    paint.color = Colors.beige[300]!;
    final capBaseRect = Rect.fromLTWH(
      size.width/2 - capBaseWidth/2,
      currentY,
      capBaseWidth,
      segmentHeights[3],
    );
    canvas.drawRect(capBaseRect, paint);
    currentY += segmentHeights[3];

    // 绘制菌盖
    paint.shadowColor = Colors.grey[500]!;
    paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3);
    paint.shader = RadialGradient(
      colors: [Colors.red[200]!, Colors.red[400]!],
      center: Alignment.topCenter,
      radius: 0.8,
    ).createShader(Rect.fromCircle(
      center: Offset(size.width/2, currentY),
      radius: capWidth/2,
    ));
    final capPath = Path()
      ..addOval(Rect.fromLTWH(
        size.width/2 - capWidth/2,
        currentY - segmentHeights[4],
        capWidth,
        segmentHeights[4] * 1.5,
      ))
      ..addRect(Rect.fromLTWH(
        size.width/2 - capBaseWidth/2,
        currentY,
        capBaseWidth,
        segmentHeights[3],
      ))
      ..fillType = PathFillType.evenOdd;
    canvas.drawPath(capPath, paint);
  }

  @override
  bool shouldRepaint(covariant MushroomPainter oldDelegate) {
    return oldDelegate.totalLength != totalLength ||
        oldDelegate.baseWidth != baseWidth ||
        oldDelegate.lowerStemWidth != lowerStemWidth ||
        oldDelegate.upperStemWidth != upperStemWidth ||
        oldDelegate.capBaseWidth != capBaseWidth ||
        oldDelegate.capWidth != capWidth ||
        oldDelegate.curvatureStartIndex != curvatureStartIndex ||
        oldDelegate.curvature != curvature;
  }
}

// 交互控制界面
class MushroomEditor extends StatefulWidget {
  const MushroomEditor({super.key});

  @override
  State<MushroomEditor> createState() => _MushroomEditorState();
}

class _MushroomEditorState extends State<MushroomEditor> {
  double _totalLength = 300;
  double _baseWidth = 60;
  double _lowerStemWidth = 40;
  double _upperStemWidth = 30;
  double _capBaseWidth = 50;
  double _capWidth = 120;
  int _curvatureStartIndex = 1;
  double _curvature = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: CustomPaint(
            painter: MushroomPainter(
              totalLength: _totalLength,
              baseWidth: _baseWidth,
              lowerStemWidth: _lowerStemWidth,
              upperStemWidth: _upperStemWidth,
              capBaseWidth: _capBaseWidth,
              capWidth: _capWidth,
              curvatureStartIndex: _curvatureStartIndex,
              curvature: _curvature,
            ),
            child: const SizedBox.expand(),
          ),
        ),
        // 总长度滑块
        Slider(
          value: _totalLength,
          min: 150,
          max: 400,
          onChanged: (val) => setState(() => _totalLength = val),
          label: '总长度: ${_totalLength.toInt()}',
        ),
        // 基座宽度滑块
        Slider(
          value: _baseWidth,
          min: 30,
          max: 80,
          onChanged: (val) => setState(() => _baseWidth = val),
          label: '基座宽度: ${_baseWidth.toInt()}',
        ),
        // 曲率起始分段选择
        DropdownButton<int>(
          value: _curvatureStartIndex,
          items: const [
            DropdownMenuItem(value: 0, child: Text('基座')),
            DropdownMenuItem(value: 1, child: Text('下菌柄')),
            DropdownMenuItem(value: 2, child: Text('上菌柄')),
          ],
          onChanged: (val) => setState(() => _curvatureStartIndex = val!),
        ),
        // 曲率调节滑块
        Slider(
          value: _curvature,
          min: -1,
          max: 1,
          onChanged: (val) => setState(() => _curvature = val),
          label: '曲率: ${_curvature.toStringAsFixed(2)}',
        ),
        // 其他分段的滑块可自行添加
      ],
    );
  }
}

五、额外优化建议

  • 给菌盖添加纹理:用Canvas.drawPoints或小椭圆绘制菌盖表面的斑点,增强真实感。
  • 限制参数范围:避免滑块调整时出现不合理的尺寸(比如宽度为0),给滑块设置合理的min/max值。
  • 缓存绘制结果:如果模型复杂,可使用PictureRecorder缓存静态部分,提升性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 20:20:36