Flutter自定義Widget實踐之簡易五子棋

前言

最近在學(xué)習(xí)Flutter的自定義Widget相關(guān)內(nèi)容,于是就自己寫了一個flutter的簡易五子棋的頁面,以加強(qiáng)學(xué)習(xí)相關(guān)的內(nèi)容。

實現(xiàn)原理及規(guī)則說明

主要原理就是通過flutter提供的CustomPaint 組件來實現(xiàn)自定義圖形繪制。主要是自定義一個棋盤背景類,以及棋子類。

自定義棋盤背景類代碼如下

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);

    //畫棋盤網(wǎng)格
    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> 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<Offset> 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;
  }
}

五子棋實現(xiàn)規(guī)則說明:
1.判贏規(guī)則說明:
主要是通過當(dāng)前落子的8個方向去進(jìn)行判斷,是否有一個方向滿足了同一顏色棋子以及滿足5顆。如下圖所示。


d9fb02d9e36578f3c0c34037221e741.png

以下是部分代碼實現(xiàn):

///判斷獲勝的方法。
  ///主要判斷依據(jù):判斷當(dāng)前點的8個方向是否能連成5個相同的顏色的子,8個方向依次遍歷,符合條件就返回。
  ///offset : 當(dāng)前點 , black:黑子還是白子 , offs:對應(yīng)顏色的子的集合。
  ///注意:計數(shù)的值必須每個方向一個,如果用同一個技術(shù)標(biāo)志,會導(dǎo)致技術(shù)值不正確。每個方向只要符合條件,都會令count加一,最后會變成一個方向沒到5就出現(xiàn)獲勝的情況。
  bool win(Offset offset, bool black, List<Offset> offs) {

    //向左遍歷 ,步長為20
    List<Offset> l = <Offset>[];
    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<Offset> r = <Offset>[];
    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<Offset> t = <Offset>[];
    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<Offset> b = <Offset>[];
    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("下贏的列表:$");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List<Offset> lt = <Offset>[];
    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<Offset> rt = <Offset>[];
    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<Offset> lb = <Offset>[];
    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<Offset> rb = <Offset>[];
    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.棋子位置判定:
當(dāng)點擊位置沒有出現(xiàn)在棋盤的棋線的交叉點時,需要將棋子移動到最近的正確位置。當(dāng)點擊的點在框內(nèi)時,計算當(dāng)前點擊的位置距離四周框邊的距離,來判斷應(yīng)該落子的位置。


image.png

以下是部分代碼實現(xiàn):

 ///將點擊位置轉(zhuǎn)換成最近的有效的棋盤點位置。
  ///計算邏輯:x軸坐標(biāo) = 點擊點位置 - 前一個豎線的x軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的x坐標(biāo),反之取上一根豎線的x坐標(biāo)
  ///        y軸坐標(biāo) = 點擊點位置 - 前一個豎線的y軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的y坐標(biāo),反之取上一根豎線的y坐標(biāo)
  Offset transOffset(Offset offset) {
    double ddx = 0;//最終位子的x坐標(biāo)
    double ddy = 0;//最終位子的y坐標(biāo)
    double level = 20;//一格的寬度
    int modx = offset.dx ~/ level;//在x軸上,點擊的位置左側(cè)的格數(shù)
    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);
  }

整體實現(xiàn)

整體實現(xiàn)就是,在一個stack布局上,底部繪制棋盤背景,頂部繪制棋子,然后監(jiān)控點擊事件,在up事件中處理當(dāng)前點擊位置,來刷新棋盤上的棋子位置,但不刷新棋盤的位置,主要用到了RepaintBoundary組件來實現(xiàn)棋盤的重構(gòu)刷新。
通過兩個數(shù)組來存儲黑白子。再通過另一個數(shù)組來存儲整體棋子的位置順序。然后進(jìn)行雙方下棋,直至出現(xiàn)勝利者。
整體代碼如下:

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<Offset> offs = <Offset>[];//所有棋子的集合
    List<Offset> boffs = <Offset>[];//黑棋集合
    List<Offset> woffs = <Offset>[];//白棋集合
    // 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: "該位置已經(jīng)下過子了,不能重復(fù)下");
                    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(() {});
                },
              );
            }),
          )
        ],
      ),
    );
  }

  ///判斷獲勝的方法。
  ///主要判斷依據(jù):判斷當(dāng)前點的8個方向是否能連成5個相同的顏色的子,8個方向依次遍歷,符合條件就返回。
  ///offset : 當(dāng)前點 , black:黑子還是白子 , offs:對應(yīng)顏色的子的集合。
  ///注意:計數(shù)的值必須每個方向一個,如果用同一個技術(shù)標(biāo)志,會導(dǎo)致技術(shù)值不正確。每個方向只要符合條件,都會令count加一,最后會變成一個方向沒到5就出現(xiàn)獲勝的情況。
  bool win(Offset offset, bool black, List<Offset> offs) {

    //向左遍歷 ,步長為20
    List<Offset> l = <Offset>[];
    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<Offset> r = <Offset>[];
    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<Offset> t = <Offset>[];
    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<Offset> b = <Offset>[];
    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("下贏的列表:$");
          return true;
        }
      } else {
        break;
      }
    }

    //左上
    int lt_conut = 1;
    List<Offset> lt = <Offset>[];
    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<Offset> rt = <Offset>[];
    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<Offset> lb = <Offset>[];
    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<Offset> rb = <Offset>[];
    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;
  }

  ///將點擊位置轉(zhuǎn)換成最近的有效的棋盤點位置。
  ///計算邏輯:x軸坐標(biāo) = 點擊點位置 - 前一個豎線的x軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的x坐標(biāo),反之取上一根豎線的x坐標(biāo)
  ///        y軸坐標(biāo) = 點擊點位置 - 前一個豎線的y軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的y坐標(biāo),反之取上一根豎線的y坐標(biāo)
  Offset transOffset(Offset offset) {
    double ddx = 0;//最終位子的x坐標(biāo)
    double ddy = 0;//最終位子的y坐標(biāo)
    double level = 20;//一格的寬度
    int modx = offset.dx ~/ level;//在x軸上,點擊的位置左側(cè)的格數(shù)
    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> 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<Offset> 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);

    //畫棋盤網(wǎng)格
    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);
    }
  }
}

總結(jié):

該內(nèi)容,目前只是實現(xiàn)簡易的五子棋的基本功能,其中還有很多細(xì)節(jié)尚未完善,僅供參考。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容