Flutter-自定义画板

简介: Flutter-自定义画板

效果

image.png

功能

  • 支持绘制线、圆、矩形,支持拓展
  • 支持撤回上一步
  • 支持清空画板
  • 支持自定义画笔颜色,宽度

实现

定义绘制类型

/// 类型
enum ShapeType {
  //线
  line,
  //圆
  circle,
  //矩形
  rectangle,
  //拓展
}

定义绘制抽象类

import 'dart:ui';

/// 绘制抽象类
abstract class Shape {
  void draw(
    Canvas canvas,
    List<Offset> points,
    Paint paint,
  );
}

实现线、圆、矩形绘制

/// 绘制圆
class CircleShape implements Shape {
  @override
  void draw(Canvas canvas, List<Offset> points, Paint paint) {
    if (points.length >= 2) {
      double radius = (points[points.length - 1] - points[1]).distance / 2;
      paint.style = PaintingStyle.stroke;
      canvas.drawCircle(points[0], radius, paint);
    }
  }
}

/// 绘制线
class LineShape implements Shape {
  @override
  void draw(Canvas canvas, List<Offset> points, Paint paint) {
    for (int i = 0; i < points.length - 1; i++) {
      canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
}

/// 绘制方
class RectangleShape implements Shape {
  @override
  void draw(Canvas canvas, List<Offset> points, Paint paint) {
    if (points.length >= 2) {
      final rect = Rect.fromPoints(points[0], points[points.length - 1]);
      paint.style = PaintingStyle.stroke;
      canvas.drawRect(rect, paint);
    }
  }
}

定义工厂类 factory

/// 根据绘制类型返回具体绘制对象
Shape getShape(ShapeType type) {
  switch (type) {
    case ShapeType.line:
      return LineShape();
    case ShapeType.circle:
      return CircleShape();
    case ShapeType.rectangle:
      return RectangleShape();
  }
}

定义笔画参数对象

/// 笔画参数对象
class DrawingStroke {
  Color color;
  double width;
  List<Offset> points;
  ShapeType type;

  DrawingStroke({
    this.color = Colors.black,
    this.width = 2.0,
    this.points = const [],
    this.type = ShapeType.line,
  });
}

定义绘制控制器

/// 绘制控制器
class DrawingController {
  final _strokes = <DrawingStroke>[];
  final _listeners = <VoidCallback>[];

  // 所有绘制笔画数据
  List<DrawingStroke> get strokes => List.unmodifiable(_strokes);
  // 画笔颜色
  Color selectedColor = Colors.black;
  // 画笔宽度
  double strokeWidth = 2.0;
  // 绘制类型
  ShapeType selectedType = ShapeType.line;

  // 开始绘制
  void startDrawing(Offset point) {
    _strokes.add(DrawingStroke(
      color: selectedColor,
      width: strokeWidth,
      points: [point],
      type: selectedType,
    ));
    _notifyListeners();
  }

  // 正在绘制
  void updateDrawing(Offset point) {
    if (_strokes.isNotEmpty) {
      _strokes.last.points.add(point);
      _notifyListeners();
    }
  }

  // 结束当前笔画绘制
  void endDrawing() {
    _notifyListeners();
  }

  // 撤回一笔
  void undo() {
    if (_strokes.isNotEmpty) {
      _strokes.removeLast();
      _notifyListeners();
    }
  }

  // 清空数据
  void clear() {
    _strokes.clear();
    _notifyListeners();
  }

  // 设置画笔颜色
  void setColor(Color color) {
    selectedColor = color;
    _notifyListeners();
  }

  // 设置画笔宽度
  void setStrokeWidth(double width) {
    strokeWidth = width;
    _notifyListeners();
  }

  // 设置绘制类型
  void setDrawingType(ShapeType type) {
    selectedType = type;
    _notifyListeners();
  }

  void _notifyListeners() {
    for (var listener in _listeners) {
      listener();
    }
  }

  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }
}

定义画板类DrawingBoard

class DrawingBoard extends StatefulWidget {
  final DrawingController controller;

  const DrawingBoard({Key? key, required this.controller}) : super(key: key);

  @override
  State<StatefulWidget> createState() => DrawingBoardState();
}

class DrawingBoardState extends State<DrawingBoard> {
  @override
  void initState() {
    super.initState();
    widget.controller.addListener(_updateState);
  }

  void _updateState() {
    setState(() {});
  }

  @override
  void dispose() {
    super.dispose();
    widget.controller.removeListener(_updateState);
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, size) {
      return SizedBox(
        width: size.maxWidth,
        height: size.maxHeight,
        child: GestureDetector(
          onPanStart: (details) {
            widget.controller.startDrawing(details.localPosition);
          },
          onPanUpdate: (details) {
            widget.controller.updateDrawing(details.localPosition);
          },
          onPanEnd: (_) {
            widget.controller.endDrawing();
          },
          child: CustomPaint(
            painter: DrawingPainter(strokes: widget.controller.strokes),
            size: Size.infinite,
          ),
        ),
      );
    });
  }
}

class DrawingPainter extends CustomPainter {
  final Paint drawPaint = Paint();

  DrawingPainter({required this.strokes});

  List<DrawingStroke> strokes;

  @override
  void paint(Canvas canvas, Size size) {
    for (var stroke in strokes) {
      drawPaint
        ..color = stroke.color
        ..strokeCap = StrokeCap.round
        ..strokeWidth = stroke.width;

      Shape shape = getShape(stroke.type);
      shape.draw(canvas, stroke.points, drawPaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}
使用画板
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_xy/xydemo/drawingboard/drawing/type.dart';
import 'package:flutter_xy/xydemo/drawingboard/drawing/view.dart';

import 'drawing/controller.dart';

class DrawingPage extends StatefulWidget {
  const DrawingPage({Key? key}) : super(key: key);

  @override
  DrawingPageState createState() => DrawingPageState();
}

class DrawingPageState extends State<DrawingPage> {
  final _controller = DrawingController();

  @override
  void initState() {
    super.initState();
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.landscapeLeft,
      DeviceOrientation.landscapeRight,
    ]);
  }

  @override
  void dispose() {
    super.dispose();
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown,
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Row(
          children: [
            SizedBox(
              width: 120,
              child: ListView(
                scrollDirection: Axis.vertical,
                padding: const EdgeInsets.symmetric(horizontal: 20),
                children: [
                  const SizedBox(width: 10),
                  _buildText("操作"),
                  const SizedBox(width: 10),
                  _buildButton('Undo', () => _controller.undo()),
                  const SizedBox(width: 10),
                  _buildButton('Clear', () => _controller.clear()),
                  const SizedBox(width: 10),
                  _buildText("画笔颜色"),
                  const SizedBox(width: 10),
                  _buildColorButton(Colors.red),
                  const SizedBox(width: 10),
                  _buildColorButton(Colors.blue),
                  const SizedBox(width: 10),
                  _buildText("画笔宽度"),
                  const SizedBox(width: 10),
                  _buildStrokeWidthButton(2.0),
                  const SizedBox(width: 10),
                  _buildStrokeWidthButton(5.0),
                  const SizedBox(width: 10),
                  _buildText("画笔类型"),
                  const SizedBox(width: 10),
                  _buildTypeButton(ShapeType.line, '线'),
                  const SizedBox(width: 10),
                  _buildTypeButton(ShapeType.circle, '圆'),
                  const SizedBox(width: 10),
                  _buildTypeButton(ShapeType.rectangle, '方'),
                ],
              ),
            ),
            Expanded(
              child: Column(
                children: [
                  Expanded(
                    child: DrawingBoard(
                      controller: _controller,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildText(String text) {
    return Text(
      text,
      style: const TextStyle(
        fontSize: 12,
        fontWeight: FontWeight.w600,
      ),
    );
  }

  Widget _buildButton(String text, VoidCallback onPressed) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(text),
    );
  }

  Widget _buildColorButton(Color color) {
    return ElevatedButton(
      onPressed: () => _controller.setColor(color),
      style: ElevatedButton.styleFrom(primary: color),
      child: const SizedBox(width: 30, height: 30),
    );
  }

  Widget _buildStrokeWidthButton(double width) {
    return ElevatedButton(
      onPressed: () => _controller.setStrokeWidth(width),
      child: Text(width.toString()),
    );
  }

  Widget _buildTypeButton(ShapeType type, String label) {
    return ElevatedButton(
      onPressed: () => _controller.setDrawingType(type),
      child: Text(label),
    );
  }
}

运行效果如下图:

image.png

详情见 github.com/yixiaolunhui/flutter_xy

相关文章
|
1月前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
110 1
|
1月前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
131 1
|
1月前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
80 1
|
1月前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
86 1
|
1月前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
84 1
|
3月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
114 3
|
4月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
109 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
4月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
64 0
|
4月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
21 0
|
4月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
111 0