美文网首页Flutter UIFlutter utils
Flutter 自定义涂鸦画板,基于 WebSocket 实现你

Flutter 自定义涂鸦画板,基于 WebSocket 实现你

作者: 小K_Js | 来源:发表于2020-05-01 16:24 被阅读0次

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

    已实现的功能

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

    使用到的技术

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

    最终效果

    三屏实时同步


    image

    实战开始

    • 打开 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));
            }
          }
      }
    

    大功告成

    image

    附源码:

    flutter涂鸦、flutter画板、flutter WebSocket

    相关文章

      网友评论

        本文标题:Flutter 自定义涂鸦画板,基于 WebSocket 实现你

        本文链接:https://www.haomeiwen.com/subject/ureqghtx.html