flutter offset_Flutter 自定义涂鸦画板,基于 WebSocket 实现你画我猜

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

已实现的功能

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

使用到的技术

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

最终效果

三屏实时同步

f7010938ea15558228fa9e6fc12675d2.png

实战开始

  • 打开 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 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 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> undoPoints = List>(); // 撤销的数据  List> points = List>(); // 存储要画的数据  List pointsList = List(); //预处理的数据,避免绘制时处理卡顿  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());            points.add(List());          }          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());          //通知更新          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 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());      points.add(List());    }    //添加绘制    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());    //通知更新    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 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();    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 {  DrawProvider _provider = DrawProvider();  @override  void initState() {    super.initState();    _provider.connect();  }  @override  Widget build(BuildContext context) {    return Scaffold(        appBar: AppBar(          title: Text("WebSocket Draw"),          actions: [            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(            builder: (context, drawProvider, _) {              return Container(                color: Color(0x18262B33),                child: Column(                  children: [                    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: [                          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));        }      }  }

大功告成

bea581fb22f859c1ec289d554c30114c.gif
附源码:
  • Flutter:https://gitee.com/pengzhenkun/flutter_draw
  • DartServer:https://gitee.com/pengzhenkun/dart_server
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值