flutter 根据设计图生成界面(自定义涂鸦画板)

随着 flutter 的兴起,越来越多的公司开始使用 flutter ,最近一老同事问我关于如何使用 flutter 实现一个你画我猜的小游戏,现把这个分享给大家~

已实现的功能

  • 画板自由涂鸦
  • 选择画笔颜色
  • 选择画笔大小
  • 撤销到上一步
  • 反撤销
  • 清空画布
  • 橡皮擦
  • 基于 WebSocket 实时发送到服务器
  • WebSocket 服务端转发给其它连接
  • 接受 WebSocket 的消息内容绘制

使用到的技术

  • 基础组件的使用(Scaffold、AppBar、IconButton、Container、Column、Stack、Padding、Icon 等)
  • 自定义 CustomPainter ,在 Canvas 上使用 Paint 绘制
  • 手势识别 GestureDetector 事件的使用
  • Flutter 基于 Provider 插件的状态管理实现
  • 简单的实现 WebSocket 通讯(真实项目考虑的问题要更多,比如心跳,重连,网络波动处理等)

最终效果

三屏实时同步

flutter 根据设计图生成界面(自定义涂鸦画板)(1)

实战开始

  • 打开 pubspec.yaml 引用状态管理和 WebSocket 库

dev_dependencies: flutter_test: sdk: flutter provider: ^4.0.1 web_socket_channel: ^1.1.0

  • 创建 draw_entity.dart 实体类

import 'package:flutter/widgets.dart'; //基础实体( pengzhenkun - 2020.04.30 ) class DrawEntity { Offset offset; String color; double strokeWidth; DrawEntity(this.offset, {this.color = "default", this.strokeWidth = 5.0}); }

  • 创建 signature_painter.dart 自定义画板

import 'package:flutter/material.dart'; import 'package:fluttercontrol/page/drawguess/draw_entity.dart'; import 'package:fluttercontrol/page/drawguess/draw_provider.dart'; //自定义 Canvas 画板( pengzhenkun - 2020.04.30 ) class SignaturePainter extends CustomPainter { List<DrawEntity> pointsList; Paint pt; SignaturePainter(this.pointsList) { pt = Paint() //设置笔的属性 ..color = pintColor["default"] ..strokeCap = StrokeCap.round ..isAntiAlias = true ..strokeWidth = 3.0 ..style = PaintingStyle.stroke ..strokeJoin = StrokeJoin.bevel; } void paint(Canvas canvas, Size size) { for (int i = 0; i < pointsList.length - 1; i ) { //画线 if (pointsList[i] != null && pointsList[i 1] != null) { pt ..color = pintColor[pointsList[i].color] ..strokeWidth = pointsList[i].strokeWidth; canvas.drawLine(pointsList[i].offset, pointsList[i 1].offset, pt); } } } //是否重绘 bool shouldRepaint(SignaturePainter other) => other.pointsList != pointsList; }

  • 创建 draw_provider.dart 状态管理
  • 记录 撤销的数据、存储要画的数据、预处理的数据、默认颜色、默认字体大小、Socket连接(为了好理解,Socket连接也写在了此类)

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttercontrol/page/drawguess/draw_entity.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; //可选的画板颜色 Map<String, Color> pintColor = { 'default': Color(0xFFB275F5), 'black': Colors.black, 'brown': Colors.brown, 'gray': Colors.grey, 'blueGrey': Colors.blueGrey, 'blue': Colors.blue, 'cyan': Colors.cyan, 'deepPurple': Colors.deepPurple, 'orange': Colors.orange, 'green': Colors.green, 'indigo': Colors.indigo, 'pink': Colors.pink, 'teal': Colors.teal, 'red': Colors.red, 'purple': Colors.purple, 'blueAccent': Colors.blueAccent, 'white': Colors.white, }; //数据管理 WebSocket,基础数据,通讯,连接维护等( pengzhenkun - 2020.04.30 ) class DrawProvider with ChangeNotifier { final String _URL = 'ws://10.10.3.55:8080/mini'; List<List<DrawEntity>> undoPoints = List<List<DrawEntity>>(); // 撤销的数据 List<List<DrawEntity>> points = List<List<DrawEntity>>(); // 存储要画的数据 List<DrawEntity> pointsList = List<DrawEntity>(); //预处理的数据,避免绘制时处理卡顿 String pentColor = "default";//默认颜色 double pentSize = 5;//默认字体大小 //Socket 连接 WebSocketChannel _channel; //开始连接 connect() { _socketConnect(); } _socketConnect() { _channel = IOWebSocketChannel.connect(_URL); _channel.stream.listen( (message) { //监听到的消息 print("收到消息:$message"); message = jsonDecode(message); if (message["type"] == "sendDraw") { //正在连续绘制 if (points.length == 0) { points.add(List<DrawEntity>()); points.add(List<DrawEntity>()); } pentColor = message["pentColor"]; pentSize = message["pentSize"]; //添加绘制 //添加绘制 points[points.length - 2].add(DrawEntity( Offset(message["dx"], message["dy"]), color: pentColor, strokeWidth: pentSize)); //通知更新 setState(); } else if (message["type"] == "sendDrawNull") { //手抬起,添加占位 //添加绘制标识 points.add(List<DrawEntity>()); //通知更新 setState(); } else if (message["type"] == "clear") { //清空画板 points.clear(); //通知更新 setState(); } else if (message["type"] == "sendDrawUndo") { //撤销,缓存到撤销容器 undoPoints.add(points[points.length - 3]); //添加到撤销的数据里 points.removeAt(points.length - 3); //移除数据 //通知更新 setState(); } else if (message["type"] == "reverseUndoDate") { //反撤销数据 List<DrawEntity> ss = undoPoints.removeLast(); points.insert(points.length - 2, ss); //通知更新 setState(); } }, onDone: () { print("连接断开 onDone"); //尝试重新连接 _socketConnect(); }, onError: (err) { print("连接异常 onError"); }, cancelOnError: true, ); } //清除数据 clear() { //清除数据 points.clear(); //通知更新 setState(); _channel.sink .add(jsonEncode({'uuid': 'xxxx', 'type': 'clear', 'msg': 'clear'})); } //绘制数据 sendDraw(Offset localPosition) { if (points.length == 0) { points.add(List<DrawEntity>()); points.add(List<DrawEntity>()); } //添加绘制 points[points.length - 2].add( DrawEntity(localPosition, color: pentColor, strokeWidth: pentSize)); // points.add(localPosition); //通知更新 setState(); //发送绘制消息给服务端 _channel.sink.add(jsonEncode({ 'uuid': 'xxxx', 'type': 'sendDraw', 'pentColor': pentColor, 'pentSize': pentSize, "dx": localPosition.dx, "dy": localPosition.dy })); } //绘制Null数据隔断标识 sendDrawNull() { //添加绘制标识 points.add(List<DrawEntity>()); //通知更新 setState(); //发送绘制消息给服务端 _channel.sink.add(jsonEncode({'uuid': 'xxxx', 'type': 'sendDrawNull'})); } //撤销一条数据 undoDate() { //撤销,缓存到撤销容器 undoPoints.add(points[points.length - 3]); //添加到撤销的数据里 points.removeAt(points.length - 3); //移除数据 setState(); //发送绘制消息给服务端 _channel.sink.add(jsonEncode({'uuid': 'xxxx', 'type': 'sendDrawUndo'})); } //反撤销一条数据 reverseUndoDate() { List<DrawEntity> ss = undoPoints.removeLast(); points.insert(points.length - 2, ss); setState(); //发送绘制消息给服务端 _channel.sink.add(jsonEncode({'uuid': 'xxxx', 'type': 'reverseUndoDate'})); } @override void dispose() { _channel.sink?.close(); super.dispose(); } _update() { pointsList = List<DrawEntity>(); for (int i = 0; i < points.length - 1; i ) { pointsList.addAll(points[i]); pointsList.add(null); } } setState() { _update(); notifyListeners(); } }

  • 有了以上的实现后,创建 draw_page.dart 搭建我们的主页面

import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttercontrol/page/drawguess/draw_provider.dart'; import 'package:fluttercontrol/page/drawguess/widget/signature_painter.dart'; import 'package:provider/provider.dart'; //绘制布局页面 ( pengzhenkun - 2020.04.30 ) class DrawPage extends StatefulWidget { @override _DrawPageState createState() => _DrawPageState(); } class _DrawPageState extends State<DrawPage> { DrawProvider _provider = DrawProvider(); @override void initState() { super.initState(); _provider.connect(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("WebSocket Draw"), actions: <Widget>[ IconButton( icon: Icon(Icons.call_missed_outgoing), onPressed: () { //撤销一步 _provider.undoDate(); }, ), IconButton( icon: Icon(Icons.call_missed), onPressed: () { //反撤销 _provider.reverseUndoDate(); }, ), ], ), body: ChangeNotifierProvider.value( value: _provider, child: Consumer<DrawProvider>( builder: (context, drawProvider, _) { return Container( color: Color(0x18262B33), child: Column( children: <Widget>[ Expanded( child: Stack( children: [ Container( color: Colors.white, ), Text(drawProvider.points.length.toString()), GestureDetector( //手势探测器,一个特殊的widget,想要给一个widge添加手势,直接用这货包裹起来 onPanUpdate: (DragUpdateDetails details) { //按下 RenderBox referenceBox = context.findRenderObject(); Offset localPosition = referenceBox .globalToLocal(details.globalPosition); drawProvider.sendDraw(localPosition); }, onPanEnd: (DragEndDetails details) { drawProvider.sendDrawNull(); }, //抬起来 ), CustomPaint( painter: SignaturePainter(drawProvider.pointsList)), ], ), ), Padding( padding: EdgeInsets.only(left: 10, right: 80, bottom: 20), child: Wrap( spacing: 5, runSpacing: 5, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[ buildInkWell(drawProvider, 5), buildInkWell(drawProvider, 8), buildInkWell(drawProvider, 10), buildInkWell(drawProvider, 15), buildInkWell(drawProvider, 17), buildInkWell(drawProvider, 20), ], ), ), Padding( padding: EdgeInsets.only(left: 10, right: 80, bottom: 20), child: Wrap( spacing: 5, runSpacing: 5, children: pintColor.keys.map((key) { Color value = pintColor[key]; return InkWell( onTap: () { // setColor(context, key); drawProvider.pentColor = key; drawProvider.notifyListeners(); }, child: Container( width: 32, height: 32, color: value, child: drawProvider.pentColor == key ? Icon( Icons.done, color: Colors.white, ) : null, ), ); }).toList(), ), ) ], ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: _provider.clear, tooltip: '', child: Icon(Icons.clear), )); } InkWell buildInkWell(DrawProvider drawProvider, double size) { return InkWell( onTap: () { drawProvider.pentSize = size; drawProvider.notifyListeners(); }, child: Container( width: 40, height: 40, child: Center( child: Container( decoration: new BoxDecoration( color: pintColor[drawProvider.pentColor], //设置四周圆角 角度 borderRadius: BorderRadius.all(Radius.circular(size / 2)), //设置四周边框 border: drawProvider.pentSize == size ? Border.all(width: 1, color: Colors.black) : null, ), width: size, height: size, ), ), ), ); } @override void dispose() { _provider.dispose(); super.dispose(); } }

  • WebSocket 服务端使用 Dart 编写,没有太多逻辑,只做了数据的转发.
  • 核心代码如下

//处理消息 void handMsg(dynamic msg, sct) { print('收到客户端消息:${msg}' webSockets.length.toString()); msg = jsonDecode(msg); if (msg["type"] == "sendDraw" ||//正在连续绘制 msg["type"] == "clear" ||//清空画板 msg["type"] == "sendDrawNull" ||//手抬起,添加占位 msg["type"] == "sendDrawUndo" ||//撤销,缓存到撤销容器 msg["type"] == "reverseUndoDate")//反撤销数据 //给其它所有客户端回复当前客户端发了什么 for (WebSocket webSocket in webSockets) { //判断是否有关闭代码,如果没有证明客户端当前未关闭,给它回复 if (webSocket.closeCode == null && webSocket != sct) { //回复客户端一条消息 webSocket.add(jsonEncode(msg)); } } }

大功告成

flutter 根据设计图生成界面(自定义涂鸦画板)(2)

附源码:

  • Flutter:https://gitee.com/pengzhenkun/flutter_draw
  • DartServer:https://gitee.com/pengzhenkun/dart_server
,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页