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组件实现景观元素的实时渲染。

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

核心功能模块详解

一、景观元素系统

元素类型定义

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

元素类型英文标识图标默认颜色应用场景
树木treepark绿色乔木种植
灌木shrubgrass浅绿色绿篱灌木
花卉flowerlocal_florist粉色花坛花境
草坪grasslandscape浅绿色草地铺装
水池waterwater蓝色水景设计
石头stonetexture灰色置石造景
小路pathroute棕色园路设计
建筑buildinghome蓝灰色建筑布局
围栏fencefence棕色边界围合
路灯lightlight_mode琥珀色照明设计
长椅benchchair橙色休憩设施
凉亭gazebotemple_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

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐