Flutter自定义Widget实践之简易五子棋

前言

最近在学习Flutter的自定义Widget相关内容,于是就自己写了一个flutter的简易五子棋的页面,以加强学习相关的内容。

实现原理及规则说明

主要原理就是通过flutter提供的CustomPaint 组件来实现自定义图形绘制。主要是自定义一个棋盘背景类,以及棋子类。

自定义棋盘背景类代码如下

class MyChessBg extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    print('paint bg');
    var rect = Offset.zero & size;
    print('paint bg ${rect.left} ${rect.right}');
    //画棋盘
    drawChessboard(canvas, rect);
    //画棋子
    // drawPieces(canvas, rect);
  }

  // 返回false, 后面介绍
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  void drawChessboard(Canvas canvas, Rect rect) {
    //棋盘背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0xFFDCC48C);
    canvas.drawRect(rect, paint);

    //画棋盘网格
    paint
      ..style = PaintingStyle.stroke //线
      ..color = Colors.black38
      ..strokeWidth = 1.0;

    //画横线
    for (int i = 0; i <= 15; ++i) {
      double dy = rect.top + rect.height / 15 * i;
      canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
    }

    for (int i = 0; i <= 15; ++i) {
      double dx = rect.left + rect.width / 15 * i;
      canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
    }
  }

自定义棋子类代码如下

class MyChessCh extends CustomPainter {
  MyChessCh({Key? key, required this.offset}) : super();

  late final List offset;

  @override
  void paint(Canvas canvas, Size size) {
    print('paint ch');
    var rect = Offset.zero & size;
    //画棋子
    // drawPieces(canvas, rect);
    drawPieces1(canvas, offset);
  }

  void drawPieces1(Canvas canvas, List offsets) {
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    for (var i = 0; i < offsets.length; i++) {
      //画一个黑子
      paint.color = Colors.black;
      if (i % 2 == 0) {
        //画一个黑子
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      } else {
        //画一个白子
        paint.color = Colors.white;
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      }
    }
  }

  //画棋子
  void drawPieces(Canvas canvas, Rect rect) {
    double eWidth = rect.width / 15;
    double eHeight = rect.height / 15;
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    //画一个黑子
    canvas.drawCircle(
      Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
    //画一个白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

五子棋实现规则说明:
1.判赢规则说明:
主要是通过当前落子的8个方向去进行判断,是否有一个方向满足了同一颜色棋子以及满足5颗。如下图所示。


d9fb02d9e36578f3c0c34037221e741.png

以下是部分代码实现:

///判断获胜的方法。
  ///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
  ///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
  ///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
  bool win(Offset offset, bool black, List offs) {

    //向左遍历 ,步长为20
    List l = [];
    int l_conut = 1;
    for (var x = offset.dx - 20; x > 0; x = x - 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        l_conut++;
        l.add(item);
        if (l_conut >= 5) {
          print("左赢的列表:${l}");
          return true;
        }
      } else {
        break;
      }
    }

    //向右遍历
    int r_conut = 1;
    List r = [];
    for (var x = offset.dx + 20; x <= 300; x = x + 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        r_conut++;
        r.add(item);
        if (r_conut >= 5) {
          print("右赢的列表:${r}");
          return true;
        }
      } else {
        break;
      }
    }

    //向上遍历
    int t_conut = 1;
    List t = [];
    for (var y = offset.dy - 20; y > 0; y = y - 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        t_conut++;
        t.add(item);
        if (t_conut >= 5) {
          print("上赢的列表:${t}");
          return true;
        }
      } else {
        break;
      }
    }

    //向下遍历
    int b_conut = 1;
    List b = [];
    for (var y = offset.dy + 20; y <= 300; y = y + 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        b_conut++;
        b.add(item);
        if (b_conut >= 5) {
          print("下赢的列表:${b}");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List lt = [];
    for (var x = offset.dx - 20, y = offset.dy - 20;
        x > 0 && y > 0;
        x = x - 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lt_conut++;
        lt.add(item);
        if (lt_conut >= 5) {
          print("左上赢的列表:${lt}");
          return true;
        }
      } else {
        break;
      }
    }

    //右上
    int rt_conut = 1;
    List rt = [];
    for (var x = offset.dx + 20, y = offset.dy - 20;
        x <= 300 && y > 0;
        x = x + 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rt_conut++;
        rt.add(item);
        if (rt_conut >= 5) {
          print("右上赢的列表:${rt}");
          return true;
        }
      } else {
        break;
      }
    }

    //左下
    int lb_conut = 1;
    List lb = [];
    for (var x = offset.dx - 20, y = offset.dy + 20;
        x > 0 && y <= 300;
        x = x - 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lb_conut++;
        lb.add(item);
        if (lb_conut >= 5) {
          print("左下赢的列表:${lb}");
          return true;
        }
      } else {
        break;
      }
    }

    //右下
    int rb_conut = 1;
    List rb = [];
    for (var x = offset.dx + 20, y = offset.dy + 20;
        x <= 300 && y <= 300;
        x = x + 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rb_conut++;
        rb.add(item);
        if (rb_conut >= 5) {
          print("右下赢的列表:${rb}");
          return true;
        }
      } else {
        break;
      }
    }

    return false;
  }

2.棋子位置判定:
当点击位置没有出现在棋盘的棋线的交叉点时,需要将棋子移动到最近的正确位置。当点击的点在框内时,计算当前点击的位置距离四周框边的距离,来判断应该落子的位置。


image.png

以下是部分代码实现:

 ///将点击位置转换成最近的有效的棋盘点位置。
  ///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
  ///        y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
  Offset transOffset(Offset offset) {
    double ddx = 0;//最终位子的x坐标
    double ddy = 0;//最终位子的y坐标
    double level = 20;//一格的宽度
    int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
    if (offset.dx - level * modx <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddx = level * modx;
    } else {
      ddx = level * (modx + 1);
    }

    int mody = offset.dy ~/ level;
    if (offset.dy - level * mody <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddy = level * mody;
    } else {
      ddy = level * (mody + 1);
    }
    print("ddx= ${ddx} + ddy = ${ddy}");
    return Offset(ddx, ddy);
  }

整体实现

整体实现就是,在一个stack布局上,底部绘制棋盘背景,顶部绘制棋子,然后监控点击事件,在up事件中处理当前点击位置,来刷新棋盘上的棋子位置,但不刷新棋盘的位置,主要用到了RepaintBoundary组件来实现棋盘的重构刷新。
通过两个数组来存储黑白子。再通过另一个数组来存储整体棋子的位置顺序。然后进行双方下棋,直至出现胜利者。
整体代码如下:

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';

class CustomChessBg extends StatelessWidget {
  const CustomChessBg({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    Offset off = Offset.zero;
    List offs = [];//所有棋子的集合
    List boffs = [];//黑棋集合
    List woffs = [];//白棋集合
    // offs.add(off);
    return DecoratedBox(
      decoration: BoxDecoration(color: Colors.white),
      child: Stack(
        children: [
          Center(
              child: RepaintBoundary(
                  child: CustomPaint(
            size: const Size(300, 300), //指定画布大小
            painter: MyChessBg(),
          ))),
          Center(
            child: StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
              return Listener(
                child: CustomPaint(
                  size: const Size(300, 300), //指定画布大小
                  painter: MyChessCh(offset: offs),
                ),
                onPointerUp: (event) {
                  print(event.localPosition);
                  var ll = transOffset(event.localPosition);
                  if (offs.contains(ll)) {
                    Fluttertoast.showToast(msg: "该位置已经下过子了,不能重复下");
                    return;
                  }
                  offs.add(ll);
                  if (offs.length % 2 == 1) {
                    //第一颗是黑棋
                    boffs.add(ll);
                    if (win(ll, true, boffs)) {
                      // Fluttertoast.showToast(msg: "黑棋获胜");
                      showDialog(
                          context: context,
                          barrierDismissible: false,
                          builder: (context) {
                            return AlertDialog(
                              title: Text("获胜提醒!"),
                              content: Text("黑棋获胜"),
                              actions: [
                                TextButton(
                                    onPressed: () {
                                      offs.clear();
                                      boffs.clear();
                                      woffs.clear();
                                      Navigator.of(context).pop();
                                      setState(() {});
                                    },
                                    child: Text("确定")),
                              ],
                            );
                          });
                    }
                  } else {
                    woffs.add(ll);
                    if (win(ll, false, woffs)) {
                      showDialog(
                          context: context,
                          barrierDismissible: false,
                          builder: (context) {
                            return AlertDialog(
                              title: Text("获胜提醒!"),
                              content: Text("白棋获胜"),
                              actions: [
                                TextButton(
                                    onPressed: () {
                                      offs.clear();
                                      boffs.clear();
                                      woffs.clear();
                                      Navigator.of(context).pop();
                                      setState(() {});
                                    },
                                    child: Text("确定")),
                              ],
                            );
                          });
                    }
                  }
                  setState(() {});
                },
              );
            }),
          )
        ],
      ),
    );
  }

  ///判断获胜的方法。
  ///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
  ///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
  ///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
  bool win(Offset offset, bool black, List offs) {

    //向左遍历 ,步长为20
    List l = [];
    int l_conut = 1;
    for (var x = offset.dx - 20; x > 0; x = x - 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        l_conut++;
        l.add(item);
        if (l_conut >= 5) {
          print("左赢的列表:${l}");
          return true;
        }
      } else {
        break;
      }
    }

    //向右遍历
    int r_conut = 1;
    List r = [];
    for (var x = offset.dx + 20; x <= 300; x = x + 20) {
      var item = Offset(x, offset.dy);
      if (offs.contains(item)) {
        r_conut++;
        r.add(item);
        if (r_conut >= 5) {
          print("右赢的列表:${r}");
          return true;
        }
      } else {
        break;
      }
    }

    //向上遍历
    int t_conut = 1;
    List t = [];
    for (var y = offset.dy - 20; y > 0; y = y - 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        t_conut++;
        t.add(item);
        if (t_conut >= 5) {
          print("上赢的列表:${t}");
          return true;
        }
      } else {
        break;
      }
    }

    //向下遍历
    int b_conut = 1;
    List b = [];
    for (var y = offset.dy + 20; y <= 300; y = y + 20) {
      var item = Offset(offset.dx, y);
      if (offs.contains(item)) {
        b_conut++;
        b.add(item);
        if (b_conut >= 5) {
          print("下赢的列表:${b}");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List lt = [];
    for (var x = offset.dx - 20, y = offset.dy - 20;
        x > 0 && y > 0;
        x = x - 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lt_conut++;
        lt.add(item);
        if (lt_conut >= 5) {
          print("左上赢的列表:${lt}");
          return true;
        }
      } else {
        break;
      }
    }

    //右上
    int rt_conut = 1;
    List rt = [];
    for (var x = offset.dx + 20, y = offset.dy - 20;
        x <= 300 && y > 0;
        x = x + 20, y = y - 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rt_conut++;
        rt.add(item);
        if (rt_conut >= 5) {
          print("右上赢的列表:${rt}");
          return true;
        }
      } else {
        break;
      }
    }

    //左下
    int lb_conut = 1;
    List lb = [];
    for (var x = offset.dx - 20, y = offset.dy + 20;
        x > 0 && y <= 300;
        x = x - 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        lb_conut++;
        lb.add(item);
        if (lb_conut >= 5) {
          print("左下赢的列表:${lb}");
          return true;
        }
      } else {
        break;
      }
    }

    //右下
    int rb_conut = 1;
    List rb = [];
    for (var x = offset.dx + 20, y = offset.dy + 20;
        x <= 300 && y <= 300;
        x = x + 20, y = y + 20) {
      var item = Offset(x, y);
      if (offs.contains(item)) {
        rb_conut++;
        rb.add(item);
        if (rb_conut >= 5) {
          print("右下赢的列表:${rb}");
          return true;
        }
      } else {
        break;
      }
    }

    return false;
  }

  ///将点击位置转换成最近的有效的棋盘点位置。
  ///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
  ///        y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
  Offset transOffset(Offset offset) {
    double ddx = 0;//最终位子的x坐标
    double ddy = 0;//最终位子的y坐标
    double level = 20;//一格的宽度
    int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
    if (offset.dx - level * modx <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddx = level * modx;
    } else {
      ddx = level * (modx + 1);
    }

    int mody = offset.dy ~/ level;
    if (offset.dy - level * mody <= 10) {
      //没过半格,取上一个点,否者取下一格
      ddy = level * mody;
    } else {
      ddy = level * (mody + 1);
    }
    print("ddx= ${ddx} + ddy = ${ddy}");
    return Offset(ddx, ddy);
  }
}

///自定义棋子类
class MyChessCh extends CustomPainter {
  MyChessCh({Key? key, required this.offset}) : super();

  late final List offset;

  @override
  void paint(Canvas canvas, Size size) {
    print('paint ch');
    var rect = Offset.zero & size;
    //画棋子
    // drawPieces(canvas, rect);
    drawPieces1(canvas, offset);
  }

  void drawPieces1(Canvas canvas, List offsets) {
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    for (var i = 0; i < offsets.length; i++) {
      //画一个黑子
      paint.color = Colors.black;
      if (i % 2 == 0) {
        //画一个黑子
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      } else {
        //画一个白子
        paint.color = Colors.white;
        canvas.drawCircle(
          offsets[i],
          8,
          paint,
        );
      }
    }
  }

  //画棋子
  void drawPieces(Canvas canvas, Rect rect) {
    double eWidth = rect.width / 15;
    double eHeight = rect.height / 15;
    //画一个黑子
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    //画一个黑子
    canvas.drawCircle(
      Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
    //画一个白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

///自定义棋盘背景类
class MyChessBg extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    print('paint bg');
    var rect = Offset.zero & size;
    print('paint bg ${rect.left} ${rect.right}');
    //画棋盘
    drawChessboard(canvas, rect);
    //画棋子
    // drawPieces(canvas, rect);
  }

  // 返回false, 后面介绍
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

  void drawChessboard(Canvas canvas, Rect rect) {
    //棋盘背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0xFFDCC48C);
    canvas.drawRect(rect, paint);

    //画棋盘网格
    paint
      ..style = PaintingStyle.stroke //线
      ..color = Colors.black38
      ..strokeWidth = 1.0;

    //画横线
    for (int i = 0; i <= 15; ++i) {
      double dy = rect.top + rect.height / 15 * i;
      canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
    }

    for (int i = 0; i <= 15; ++i) {
      double dx = rect.left + rect.width / 15 * i;
      canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
    }
  }
}

总结:

该内容,目前只是实现简易的五子棋的基本功能,其中还有很多细节尚未完善,仅供参考。

你可能感兴趣的:(Flutter自定义Widget实践之简易五子棋)