Flutter景观设计工具


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

项目概述

运行效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目背景与目标

景观设计是一门融合艺术与科学的综合性学科,涉及园林规划、植物配置、空间布局等多个领域。传统的景观设计依赖于专业软件和手绘草图,学习门槛较高。本项目基于Flutter框架开发一款景观设计工具,旨在降低景观设计的入门门槛,让普通用户也能轻松创建专业的景观设计方案。

项目的核心目标涵盖多个维度:构建完整的景观元素库,实现直观的拖拽编辑,设计专业的绘制引擎,打造流畅的用户体验,以及确保应用的稳定性和性能表现。通过本项目的开发,不仅能够深入理解Flutter在图形编辑类应用中的应用,更能掌握自定义绘制、手势交互、状态管理等核心技术要点。

二、技术选型与架构设计

技术栈分析

本项目选用Flutter作为开发框架,主要基于以下考量:Flutter的Skia渲染引擎提供了强大的图形处理能力,非常适合图形编辑类应用;声明式UI编程范式能够高效构建复杂的编辑界面;热重载功能大幅提升了开发调试效率;丰富的Widget组件库为应用UI开发提供了坚实基础。

Dart语言作为Flutter的开发语言,具备强类型、异步编程支持、优秀的性能表现等特性。项目采用单文件架构,将所有应用逻辑集中在main.dart文件中,这种设计既便于代码管理,又利于理解应用整体架构。

架构层次划分

应用架构采用分层设计思想,主要分为以下几个层次:

数据模型层:定义应用中的核心数据结构,包括LandscapeElement(景观元素)、LandscapeProject(景观项目)、ElementType(元素类型)等类和枚举。这些模型类封装了景观设计的状态和行为,构成了应用逻辑的基础。

业务逻辑层:实现应用的核心功能逻辑,包括元素添加、选择、移动、删除、图层管理等。这一层是应用的心脏,决定了应用的功能性和可用性。

渲染表现层:负责应用界面的绘制和UI展示,使用Flutter的Material Design组件库实现现代化的界面设计,通过CustomPaint组件实现景观元素的实时渲染。

状态管理层:管理应用的各种状态,包括当前项目、选中元素、工具索引、缩放级别等,确保应用状态的一致性和可预测性。

核心功能模块详解

一、景观元素系统

元素类型定义

应用提供十二种景观元素类型,涵盖景观设计的主要元素:

元素类型 英文标识 图标 默认颜色 应用场景
树木 tree park 绿色 乔木种植
灌木 shrub grass 浅绿色 绿篱灌木
花卉 flower local_florist 粉色 花坛花境
草坪 grass landscape 浅绿色 草地铺装
水池 water water 蓝色 水景设计
石头 stone texture 灰色 置石造景
小路 path route 棕色 园路设计
建筑 building home 蓝灰色 建筑布局
围栏 fence fence 棕色 边界围合
路灯 light light_mode 琥珀色 照明设计
长椅 bench chair 橙色 休憩设施
凉亭 gazebo temple_buddhist 青色 景观构筑
元素数据模型

景观元素封装了位置、尺寸、变换等属性:

class LandscapeElement {
  String id;
  ElementType type;
  double x;
  double y;
  double width;
  double height;
  double rotation;
  double scale;
  String? label;
  Color? customColor;
}

元素ID使用时间戳生成,确保唯一性;位置使用画布坐标,支持精确到像素的定位;宽高定义元素的边界框,用于碰撞检测;旋转角度以度为单位,支持0-360度旋转;缩放比例支持0.5-3.0倍缩放;标签属性允许用户为元素添加文字标注;自定义颜色允许用户修改元素的默认颜色。

元素绘制实现

每种元素类型都有对应的绘制方法,使用Canvas API实现矢量图形绘制:

树木绘制:树木由树干和树冠两部分组成,树干使用棕色矩形,树冠使用绿色三角形叠加:

void _drawTree(Canvas canvas, Paint paint, Paint strokePaint, LandscapeElement element) {
  paint.color = Colors.brown;
  canvas.drawRect(const Rect.fromLTWH(-5, 0, 10, 25), paint);

  paint.color = element.customColor ?? Colors.green.shade700;
  final path = Path();
  path.moveTo(0, -30);
  path.lineTo(-20, 5);
  path.lineTo(20, 5);
  path.close();
  canvas.drawPath(path, paint);

  path.reset();
  path.moveTo(0, -20);
  path.lineTo(-15, 10);
  path.lineTo(15, 10);
  path.close();
  canvas.drawPath(path, paint);
}

花卉绘制:花卉由茎干和花瓣组成,花瓣使用圆形围绕中心排列:

void _drawFlower(Canvas canvas, Paint paint, Paint strokePaint, LandscapeElement element) {
  paint.color = Colors.green;
  canvas.drawRect(const Rect.fromLTWH(-1, 0, 2, 15), paint);

  paint.color = element.customColor ?? Colors.pink;
  for (int i = 0; i < 5; i++) {
    final angle = i * 72 * math.pi / 180;
    canvas.drawCircle(Offset(8 * math.cos(angle), 8 * math.sin(angle) - 5), 5, paint);
  }
  paint.color = Colors.yellow;
  canvas.drawCircle(const Offset(0, -5), 4, paint);
}

水池绘制:水池使用椭圆形表示,添加波纹线条增强视觉效果:

void _drawWater(Canvas canvas, Paint paint, Paint strokePaint, LandscapeElement element) {
  paint.color = element.customColor ?? Colors.blue.shade300;
  canvas.drawOval(const Rect.fromLTWH(-25, -15, 50, 30), paint);

  strokePaint.color = Colors.blue.shade200;
  strokePaint.strokeWidth = 1;
  canvas.drawLine(const Offset(-15, 0), const Offset(-5, 0), strokePaint);
  canvas.drawLine(const Offset(5, -5), const Offset(15, -5), strokePaint);
  canvas.drawLine(const Offset(-10, 5), const Offset(0, 5), strokePaint);
}

二、画布编辑系统

画布坐标系统

画布采用笛卡尔坐标系,原点位于左上角,X轴向右为正,Y轴向下为正。元素位置使用画布坐标,支持缩放和平移变换:

void _onCanvasTap(TapDownDetails details, Size canvasSize) {
  final localPosition = (details.localPosition - _offset) / _zoom;

  for (var element in widget.project.elements.reversed) {
    if (_isPointInElement(localPosition, element)) {
      setState(() => _selectedElement = element);
      return;
    }
  }
}

点击位置需要经过逆变换,从屏幕坐标转换为画布坐标,才能正确判断点击了哪个元素。

网格吸附功能

网格吸附帮助用户精确对齐元素:

final snappedPosition = _snapToGrid
    ? Offset(
        (localPosition.dx / _gridSize).round() * _gridSize,
        (localPosition.dy / _gridSize).round() * _gridSize,
      )
    : localPosition;

网格大小默认为20像素,吸附时将坐标四舍五入到最近的网格交点。网格吸附可以开关,满足不同精度的设计需求。

元素拖拽移动

元素支持拖拽移动,实时更新位置:

onPanUpdate: (details) {
  if (_selectedElement != null && _toolIndex == 0) {
    setState(() {
      _selectedElement!.x += details.delta.dx / _zoom;
      _selectedElement!.y += details.delta.dy / _zoom;
      if (_snapToGrid) {
        _selectedElement!.x = (_selectedElement!.x / _gridSize).round() * _gridSize;
        _selectedElement!.y = (_selectedElement!.y / _gridSize).round() * _gridSize;
      }
    });
  }
},

拖拽时需要考虑缩放比例,移动距离需要除以缩放比例才能得到正确的画布位移。如果开启了网格吸附,移动后自动吸附到最近的网格交点。

元素碰撞检测

点击检测使用矩形碰撞算法:

bool _isPointInElement(Offset point, LandscapeElement element) {
  final halfWidth = element.width * element.scale / 2;
  final halfHeight = element.height * element.scale / 2;
  return point.dx >= element.x - halfWidth &&
      point.dx <= element.x + halfWidth &&
      point.dy >= element.y - halfHeight &&
      point.dy <= element.y + halfHeight;
}

元素边界框以中心点为基准,宽高需要乘以缩放比例。点击坐标在边界框内即判定为命中。

三、图层管理系统

图层顺序控制

元素按照添加顺序渲染,后添加的元素显示在上层:

for (var element in elements) {
  _drawElement(canvas, element, element == selectedElement);
}

图层管理提供置于顶层和置于底层功能:

void _bringToFront(LandscapeElement element) {
  setState(() {
    widget.project.elements.remove(element);
    widget.project.elements.add(element);
  });
}

void _sendToBack(LandscapeElement element) {
  setState(() {
    widget.project.elements.remove(element);
    widget.project.elements.insert(0, element);
  });
}

置于顶层将元素移动到列表末尾,置于底层将元素移动到列表开头。列表顺序即为渲染顺序。

元素复制功能

元素复制创建完全相同的副本,位置偏移避免重叠:

void _duplicateElement(LandscapeElement element) {
  final newElement = LandscapeElement(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    type: element.type,
    x: element.x + 20,
    y: element.y + 20,
    width: element.width,
    height: element.height,
    rotation: element.rotation,
    scale: element.scale,
    label: element.label,
    customColor: element.customColor,
  );
  setState(() {
    widget.project.elements.add(newElement);
    _selectedElement = newElement;
  });
}

复制后自动选中新元素,方便用户继续编辑。

四、属性编辑面板

位置编辑

位置编辑使用文本输入框,支持精确数值输入:

Row(
  children: [
    Expanded(
      child: TextField(
        decoration: const InputDecoration(labelText: 'X', border: OutlineInputBorder()),
        controller: TextEditingController(text: element.x.round().toString()),
        keyboardType: TextInputType.number,
        onChanged: (value) {
          final v = double.tryParse(value);
          if (v != null) setState(() => element.x = v);
        },
      ),
    ),
    const SizedBox(width: 8),
    Expanded(
      child: TextField(
        decoration: const InputDecoration(labelText: 'Y', border: OutlineInputBorder()),
        controller: TextEditingController(text: element.y.round().toString()),
        keyboardType: TextInputType.number,
        onChanged: (value) {
          final v = double.tryParse(value);
          if (v != null) setState(() => element.y = v);
        },
      ),
    ),
  ],
)

输入框显示当前值的整数形式,支持实时更新。无效输入会被忽略,保持原有值不变。

缩放与旋转

缩放和旋转使用滑块控件,提供直观的调节体验:

const Text('缩放', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
  value: element.scale,
  min: 0.5,
  max: 3.0,
  divisions: 25,
  label: element.scale.toStringAsFixed(1),
  onChanged: (value) => setState(() => element.scale = value),
),
const SizedBox(height: 8),
const Text('旋转', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
  value: element.rotation,
  min: 0,
  max: 360,
  divisions: 36,
  label: '${element.rotation.round()}°',
  onChanged: (value) => setState(() => element.rotation = value),
),

缩放范围0.5-3.0,分为25档;旋转范围0-360度,分为36档,每档10度。滑块显示当前值的标签,方便用户确认。

标签编辑

标签编辑使用文本输入框,支持为元素添加文字标注:

TextField(
  decoration: const InputDecoration(border: OutlineInputBorder()),
  controller: TextEditingController(text: element.label ?? ''),
  onChanged: (value) => setState(() => element.label = value.isEmpty ? null : value),
)

标签为空时设为null,不显示标签。标签显示在元素下方,背景为白色,确保可读性。

五、项目管理模块

项目数据模型

项目封装了画布尺寸、元素列表等属性:

class LandscapeProject {
  String id;
  String name;
  double width;
  double height;
  List<LandscapeElement> elements;
  DateTime createdAt;
  DateTime modifiedAt;
  String? backgroundPath;
}

画布尺寸默认为800×600像素,支持自定义。创建时间和修改时间用于项目排序和展示。背景路径预留了背景图片功能。

项目列表展示

项目列表使用卡片布局,每个卡片显示项目预览和基本信息:

Widget _buildProjectCard(LandscapeProject project) {
  return Card(
    margin: const EdgeInsets.only(bottom: 12),
    child: InkWell(
      onTap: () => _openProject(project),
      onLongPress: () => _showProjectOptions(project),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: Colors.green.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              child: CustomPaint(
                painter: ProjectPreviewPainter(project.elements),
              ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(project.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                  const SizedBox(height: 4),
                  Text('${project.width.toInt()} × ${project.height.toInt()} | ${project.elements.length} 个元素'),
                  const SizedBox(height: 4),
                  Text('修改于 ${_formatDate(project.modifiedAt)}'),
                ],
              ),
            ),
            Icon(Icons.chevron_right, color: Colors.grey.shade400),
          ],
        ),
      ),
    ),
  );
}

项目预览使用CustomPaint绘制元素缩略图,直观展示项目内容。

项目预览绘制

项目预览将元素缩小绘制到80×80的区域内:

class ProjectPreviewPainter extends CustomPainter {
  final List<LandscapeElement> elements;

  ProjectPreviewPainter(this.elements);

  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..style = PaintingStyle.fill;

    for (var element in elements) {
      paint.color = element.defaultColor.withOpacity(0.7);
      canvas.drawCircle(
        Offset(element.x / 10, element.y / 10),
        8,
        paint,
      );
    }
  }
}

预览将元素位置缩小10倍,用圆形表示元素位置和类型颜色,提供快速预览效果。

UI界面开发

一、主界面布局

主界面采用底部导航栏设计,包含三个主要页面:

BottomNavigationBar(
  currentIndex: _currentIndex > 2 ? 0 : _currentIndex,
  onTap: (index) => setState(() => _currentIndex = index),
  selectedItemColor: Colors.green,
  unselectedItemColor: Colors.grey,
  items: const [
    BottomNavigationBarItem(icon: Icon(Icons.folder), label: '项目'),
    BottomNavigationBarItem(icon: Icon(Icons.view_quilt), label: '模板'),
    BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
  ],
)

底部导航栏使用绿色作为选中颜色,与景观设计的主题相符。三个页面分别是项目列表、设计模板和设置页面,覆盖了应用的主要功能入口。

二、编辑器界面

编辑器界面分为左中右三栏布局:

Scaffold(
  appBar: AppBar(
    leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: widget.onBack),
    title: Text(widget.project.name),
    actions: [
      IconButton(icon: Icon(_showGrid ? Icons.grid_on : Icons.grid_off), onPressed: () => setState(() => _showGrid = !_showGrid)),
      IconButton(icon: Icon(_snapToGrid ? Icons.snap_to_pixel : Icons.snap_outlined), onPressed: () => setState(() => _snapToGrid = !_snapToGrid)),
      IconButton(icon: const Icon(Icons.undo), onPressed: () {}),
      IconButton(icon: const Icon(Icons.redo), onPressed: () {}),
      PopupMenuButton<String>(...),
    ],
  ),
  body: Row(
    children: [
      _buildToolPanel(),
      Expanded(child: _buildCanvas()),
      if (_selectedElement != null) _buildPropertyPanel(),
    ],
  ),
)

应用栏提供返回、网格开关、吸附开关、撤销、重做、导出等操作按钮。左侧工具面板宽度80像素,提供元素选择工具;中间画布区域自适应宽度,支持缩放和平移;右侧属性面板宽度240像素,选中元素时显示。

三、工具面板设计

工具面板采用垂直列表布局,顶部为选择工具,下方为元素工具:

Widget _buildToolPanel() {
  return Container(
    width: 80,
    color: Colors.grey.shade200,
    child: Column(
      children: [
        _buildToolButton(0, Icons.pan_tool, '选择'),
        const Divider(height: 1),
        Expanded(
          child: ListView.builder(
            itemCount: _elementTools.length,
            itemBuilder: (context, index) {
              final tool = _elementTools[index];
              return _buildToolButton(index + 1, tool['icon'], tool['name']);
            },
          ),
        ),
      ],
    ),
  );
}

工具按钮显示图标和名称,选中状态以绿色背景标识。选择工具用于选择和移动元素,元素工具用于添加对应类型的元素。

四、画布渲染实现

画布使用InteractiveViewer组件实现缩放和平移:

InteractiveViewer(
  minScale: 0.25,
  maxScale: 4.0,
  onInteractionUpdate: (details) {
    setState(() {
      _zoom = details.scale;
      _offset = details.focalPoint;
    });
  },
  child: Container(
    width: widget.project.width,
    height: widget.project.height,
    color: Colors.white,
    child: CustomPaint(
      painter: LandscapeCanvasPainter(
        elements: widget.project.elements,
        selectedElement: _selectedElement,
        showGrid: _showGrid,
        gridSize: _gridSize,
      ),
    ),
  ),
)

InteractiveViewer支持双指缩放和拖拽平移,缩放范围0.25-4.0倍。画布使用CustomPaint绘制网格和元素,通过LandscapeCanvasPainter实现自定义绘制。

性能优化方案

一、绘制优化

画布绘制采用增量更新策略,只在元素变化时重绘:


bool shouldRepaint(covariant LandscapeCanvasPainter oldDelegate) {
  return elements != oldDelegate.elements ||
      selectedElement != oldDelegate.selectedElement ||
      showGrid != oldDelegate.showGrid;
}

shouldRepaint方法比较新旧状态,只有状态变化时才触发重绘,避免不必要的渲染开销。

二、状态管理优化

应用状态采用setState()方法管理,确保状态的一致性和可预测性:

setState(() {
  _selectedElement!.x += details.delta.dx / _zoom;
  _selectedElement!.y += details.delta.dy / _zoom;
});

状态更新批量执行,减少了不必要的重绘次数。对于复杂的状态管理,可以考虑使用Provider、Riverpod等状态管理方案。

三、列表渲染优化

项目列表和工具列表采用ListView.builder组件,实现了按需渲染:

ListView.builder(
  itemCount: _elementTools.length,
  itemBuilder: (context, index) {
    final tool = _elementTools[index];
    return _buildToolButton(index + 1, tool['icon'], tool['name']);
  },
)

只有可见区域的项目才会被创建和渲染,大幅降低了内存占用和渲染开销。

测试方案与步骤

一、功能测试

功能测试旨在验证应用各项功能是否按预期工作。测试用例应覆盖所有核心功能模块,确保应用逻辑的正确性。

元素操作测试:验证元素添加、选择、移动、删除功能是否正常;测试元素复制、图层调整功能是否正确;检查元素属性编辑是否生效。

画布操作测试:验证画布缩放、平移功能是否正常;测试网格显示、网格吸附功能是否正确;检查元素碰撞检测是否准确。

项目管理测试:验证项目创建、重命名、复制、删除功能是否正常;测试项目列表展示是否正确;检查项目预览是否准确。

二、性能测试

性能测试关注应用的运行效率,确保在各种情况下都能流畅运行。

大量元素测试:测试画布包含大量元素时的渲染性能,确保拖拽流畅。

缩放性能测试:测试不同缩放级别下的渲染性能,确保无卡顿。

内存占用测试:监测应用运行过程中的内存使用情况,确保没有内存泄漏。

三、兼容性测试

兼容性测试确保应用在不同环境下都能正常运行。

多平台测试:在Android、iOS等平台分别测试应用功能,验证跨平台一致性。

屏幕适配测试:测试应用在不同屏幕尺寸下的表现,确保布局正确。

横竖屏测试:测试应用在横竖屏切换时的表现,确保界面正常。

四、用户体验测试

用户体验测试关注应用的易用性和美观度。

操作便捷性测试:邀请用户试用应用,收集对操作流程的反馈,评估交互设计的合理性。

视觉体验测试:评估应用的视觉效果,包括色彩搭配、图标设计、布局美观度等。

响应速度测试:测试应用的响应速度,确保操作反馈及时,提升用户体验。

项目总结与展望

一、项目成果总结

本项目成功实现了一款功能完整、界面专业的景观设计工具,涵盖了图形编辑类应用开发的核心要素。通过Flutter框架的应用,实现了跨平台的应用体验,证明了Flutter在图形编辑类应用开发领域的可行性。

项目采用模块化设计思想,将应用功能划分为元素系统、画布编辑、图层管理、属性编辑、项目管理等独立模块,各模块职责明确,耦合度低,便于维护和扩展。

代码实现注重性能优化和用户体验,通过自定义绘制、增量更新、状态管理等手段,确保了应用在各种情况下的流畅运行。

二、技术亮点总结

矢量图形绘制:使用Canvas API实现了十二种景观元素的矢量绘制,支持缩放和旋转而不失真。

画布交互系统:实现了完整的画布交互,包括缩放、平移、网格吸附、元素拖拽等功能。

图层管理系统:实现了图层顺序控制,支持置于顶层、置于底层等操作。

属性编辑面板:实现了完整的属性编辑,包括位置、尺寸、缩放、旋转、标签等属性。

项目预览功能:实现了项目缩略图预览,直观展示项目内容。

三、未来优化方向

更多元素类型:增加更多景观元素类型,如喷泉、雕塑、花架、景墙等,丰富元素库。

元素组合功能:支持元素组合,将多个元素组合为一个整体,便于移动和复制。

撤销重做功能:实现完整的撤销重做功能,记录用户的每一步操作。

背景图片功能:支持导入背景图片,在真实场景上进行景观设计。

导出高清图片:实现高清图片导出,支持指定分辨率和格式。

云同步功能:实现项目云同步,支持多设备协作。

测量标注功能:实现距离测量和尺寸标注,提供专业的设计辅助。

植物数据库:集成植物数据库,提供植物习性、养护信息等专业知识。

四、开发经验总结

通过本项目的开发,积累了宝贵的Flutter应用开发经验:

自定义绘制的重要性:图形编辑类应用的核心是自定义绘制,理解Canvas API、Path、Paint等概念,是开发此类应用的基础。

交互设计的核心地位:编辑类应用最终服务于用户,交互设计是评判应用质量的核心标准。从元素的选中反馈到属性的实时更新,每个细节都需要精心打磨。

性能优化的持续性:性能优化不是一次性工作,需要在开发过程中持续关注,通过性能分析工具定位瓶颈,针对性优化。

跨平台兼容性的挑战:跨平台开发需要考虑不同平台的差异,包括屏幕尺寸、像素密度、触摸精度等,确保应用在各平台上的表现一致。

本项目为Flutter图形编辑应用开发提供了一个完整的实践案例,展示了如何实现矢量绘制、画布交互、图层管理等核心功能,希望能够为相关开发者提供参考和启发,推动Flutter在专业工具类应用开发领域的应用和发展。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐