Flutter象棋:2.绘制棋盘

Flutter绘制棋盘

这一节作为将这一系列课程真正的开始!当然我们肯定不是要创建什么 HelloWord 的啦。

象棋游戏在 AppStore 中的游戏主要分类是什么 — Board 类!Board 是棋类游戏的主要关注点。这一节开始,我们将以上一节创建的项目 chessroad 为基础,使用 Canvas 绘制的我们的棋盘 Board。

本节目标

为了适应各种各样的屏幕,我们绘制棋盘没有使用图片资源,全部使用 Canvas 绘图方式完成。我们将要绘制的棋盘是这样的:

棋盘效果

棋盘绘制亦然包含许多的细节,为了让一节的篇幅不至于太长,我们这一节的绘制工作只包含棋盘,不包含棋子!

中国象棋棋盘

在开始棋盘绘制之前,我们要了解一下中国象棋的棋盘。棋盘有一基本特性,这些特性在我们绘图时将体现到:

  • 棋盘由 10 条横线、9 条纵线组成,棋子放在纵横线交叉的点位上;
  • 棋盘中间有一条「河界」,部分纵线是没有贯通「河界」的(有的象棋软件中线没有贯通);
  • 棋盘上下两侧的九宫(「将」可以移动的范围)从左上角到右下角、以及从右上角到左下角有斜线,作为「仕」移动的引导线。
  • 两边的「炮」和「兵/卒」初始位置处有位置指示标志;
  • 棋盘的上下两侧有指示纵线(象棋术语中叫「路数」)的数字。
  • 代表红方的「路数」用中文数字「一」到「九」表示;
  • 代表黑方的路数用阿拉伯数字「1」到「9」表示;
  • 对于两方棋手来说,象棋的「路数」是从右边数超的, 相对于棋手所坐的方向,最右侧一路是第一路、最左侧是第九路。这符合中国人传统的文字从右向左书写的习惯

了解这些信息后,我们开始准备绘制棋盘!

绘制并显示棋盘

我们使用 vscode 打开之前创建的“chinese_chess”项目,在项目根目下,我们建立一个 lib/board 文件夹,在其中新建 board-widget.dart 文件。在这个文件的中,我们添加一个新的 Widget 的实现类 BoardWidget:

class BoardWidget extends StatelessWidget {
		    ...
		}
		
复制

对于棋盘的实现,我们的想法是这样的:

  • 用 Container 指定背景色的方式来实现代表棋盘底的「木板」
  • 用 CustomPaint 来绘制棋盘上的线、位置坐标
  • 棋盘上下边沿的「路数」指示文字,以及中间的「河界」文字,由布局方式添加上去

代表棋盘的 BoardWiget 将来会被用在不同的场景,例如「对战」或「阅读棋盘」。考虑到目前的手机有多种不同纵横比的屏幕,我们有必要将 BoardWidget 实现成一个「适用性」强的组件 — 至少对屏幕的尺寸变化有很强的适应能力:

这里我们需要考虑到 BoardWidget 的宽度 (width) 应该是由它的创建者(父组件)指定的。由于棋盘中的格式都是小正方形开,棋盘的宽度一确认,其高度也就确定了。

有了这些思考和了解,我们在 BoardWidget 中开始创建代表棋盘「木板」的 Container:

import 'package:flutter/material.dart';
		import '../common/color-consts.dart';
		
		class BoardWidget extends StatelessWidget {
		
		  // 棋盘内边界 + 棋盘上的路数指定文字高度
		  static const Padding = 5.0, DigitsHeight = 36.0;
		
		  // 棋盘的宽高
		  final double width, height;
		
		  BoardWidget({@required this.width}) :
		    height = (width - Padding * 2) / 9 * 10 + (Padding + DigitsHeight) * 2;
		
		  Widget build(BuildContext context) {
		    //
		    return Container(
		      width: width,
		      height: height,
		      decoration: BoxDecoration(
		        borderRadius: BorderRadius.circular(5),
		        color: ColorConsts.BoardBackground,
		      ),
		      child: null,
		    );
		  }
		}
		
复制

在上边的代码中,我们添加了两个常量:

  • Padding 表示了棋盘边界到棋子之间的距离
  • DigitsHeight 表示了为棋盘上下边沿的「路数」数字显示预留的高度

特别说明:
有时候我们贴出的代码为了简洁,没有添加需要的部分 import 语句,你需要我自行添加。
在 vscode 中,如果一个类型或方法没有被 import 引入,会在其下显示红色波浪线。
当你定位到这一行代码,并按 Cmd+.时,vscode 会自动地给出你导入文件建议,选中并回车即可完成必要包的导入。

我们只有「木板」的棋盘就算是准备好了。为了能从手机上看到这个简陋的棋盘,我们需要搭建一个简单的对战页面:

新建 lib/routes 文件夹,在其下创建 battle-page.dart 文件:

import 'package:flutter/material.dart';
		import '../board/board-widget.dart';
		
		class BattlePage extends StatelessWidget {
		  // 棋盘的纵横方向的边距
		  static const BoardMarginV = 10.0, BoardMarginH = 10.0;
		
		  @override
		  Widget build(BuildContext context) {
		    //
		    final windowSize = MediaQuery.of(context).size;
		    final boardHeight = windowSize.width - BoardMarginH * 2;
		
		    return Scaffold(
		      appBar: AppBar(title: Text('Battle')),
		      body: Container(
		        margin: const EdgeInsets.symmetric(
		          horizontal: BoardMarginH,
		          vertical: BoardMarginV,
		        ),
		        child: BoardWidget(width: boardHeight),
		      ),
		    );
		  }
		}
		
复制

上边的 BattlePage 代码为我们的 BoardWidget 提供了必要的舞台:

  • 我们使用 MediaQuery 查询了窗口的尺寸, 得到了 BoardWidget 的适当宽度
  • BoardWidget 被放在外层的另一个 Container 中,这个 Container 定义了左右上下的边边距。不要小看这两个边距值,将来在做屏幕适合的时候,动动这两个值就行了,BoardWidget 都不用一丝丝修改。

打开 Flutter自动为我们创建的 main.dart 文件,删除原有的代码,添加以下的代码:

import 'package:flutter/material.dart';
		import 'routes/battle-page.dart';
		
		void main() {
		  // runApp(MyApp());
		  runApp(ChessApp());
		}
		
		class ChessApp extends StatelessWidget {
		  @override
		  Widget build(BuildContext context) {
		    return MaterialApp(
		      title: '中国象棋',
		      theme: ThemeData(
		        primarySwatch: Colors.lime,
		        visualDensity: VisualDensity.adaptivePlatformDensity,
		      ),
		      debugShowCheckedModeBanner: false, //禁止显示 App 右上角的一个「Debug」字样的条幅
		      home: BattlePage(),
		    );
		  }
		}
		
复制

上边的代码中,我们指定了 BattlePage 页面为我们启动后的默认页面,在这之前还为 App 指定了主体颜色。

由于我们修改了默认 App 的类名称,所以我们需要在 test/widget_test.dart 中将“MyApp”修改为“ChessApp”,将原来的如下代码:

...
		await tester.pumpWidget(MyApp());
		...
		
复制

修改为:

...
		await tester.pumpWidget(ChessApp());
...
复制

在 vscode 上按F5运行代码,你将看代表我们棋盘的「木板」显示在画面上了,这离我们显示棋盘的目标近了一点点!

代码棋盘的小木板显示出来

绘制棋盘上的网格

现在,我们开始创建 CustomPaint 组件,在 lib/board 文件夹下,我们新建一个 board-painter.dart 文件,我们要在其中实现 BoardPainter 类:

import 'package:flutter/material.dart';
		
		import 'board-widget.dart';
		
		class BoardPainter extends CustomPainter {
		  // 棋盘的宽度, 横盘上线格的总宽度,每一个格子的边长
		  final double width, gridWidth, squareSide;
		  final thePaint = Paint();
		
		  BoardPainter({@required this.width})
		      : gridWidth = (width - BoardWidget.Padding * 2) * 8 / 9,
		        squareSide = (width - BoardWidget.Padding * 2) / 9;
		
		  @override
		  void paint(Canvas canvas, Size size) {
		    //
		  }
		
		  @override
		  bool shouldRepaint(CustomPainter oldDelegate) {
		    return false;
		  }
		}
		
复制

在 BoardWidget 类中,我们为代码「木板」的 Container 设置一个 CustomPaint 的 child,并将 CustomPaint 的 painter 指向 BoardPainter 对象:

...
		
		class BoardWidget extends StatelessWidget {
		  //
		  ...
		
		  Widget build(BuildContext context) {
		    //
		    return Container(
		      width: width,
		      height: height,
		      decoration: BoxDecoration(
		        borderRadius: BorderRadius.circular(5),
		        color: ColorConsts.BoardBackground,
		      ),
		      child: CustomPaint(
		        painter: BoardPainter(width: width),
		      ),
		    );
		  }
		}
		
复制

现在,我们把 CustomPaint 对象和 BoardPainter 关联起来了,只是这个 Painter 还什么都没有绘制。

我们下来就开始绘制基本的网格吧:

我们在 BoardPainter 类中添加一个 static 方法 doPaint:

  static doPaint(
		    Canvas canvas,
		    Paint paint,
		    double gridWidth,
		    double squareSide, {
		    double offsetX,
		    double offsetY,
		  }) {
		    //
		  }
		
复制

之所以使用一个静态方法来包裹所有的绘制动作,是考虑到这个绘制动作可能被重用。

将来在分享局面图片时,需要将棋盘绘制在内存画布上时,那会儿就可以直接在此类之外通过静态方法调用这绘制代码了。

接着,我们在 BoardPainter 类的 paint 方法中调用 doPaint 方法:

 @override
		 void paint(Canvas canvas, Size size) {
		   doPaint(
		     canvas,
		     thePaint,
		     gridWidth,
		     squareSide,
		     offsetX: BoardWidget.Padding + squareSide / 2,
		     offsetY: BoardWidget.Padding + BoardWidget.DigitsHeight + squareSide / 2,
		  );
		}
		
复制

真正在 doPaint 方法中绘制棋盘,我分需要分几步走:

第一步,我们绘制外框和纵横线:

  static doPaint(
		    Canvas canvas,
		    Paint paint,
		    double gridWidth,
		    double squareSide, {
		    double offsetX,
		    double offsetY,
		  }) {
		    paint.color = ColorConsts.BoardLine;
		    paint.style = PaintingStyle.stroke;
		
		    var left = offsetX, top = offsetY;
		
		    //棋盘外框
		    paint.strokeWidth = 2;
		    canvas.drawRect(
		      Rect.fromLTWH(left, top, gridWidth, squareSide * 9),
		      paint,
		    );
		
		    paint.strokeWidth = 1; //设置线宽为1
		
		    //8根横线
		    for (var i = 1; i < 9; i++) {
		      canvas.drawLine(
		        Offset(left, top + squareSide * i),
		        Offset(left + gridWidth, top + squareSide * i),
		        paint,
		      );
		    }
		
		    //上下各6根短竖线
		    for (var i = 0; i < 8; i++) {
		      // if (i == 4) continue; //中间拉通的线已经画过
		
		      /* 上边短竖线 */
		      canvas.drawLine(
		        Offset(left + squareSide * i, top),
		        Offset(left + squareSide * i, top + squareSide * 4),
		        paint,
		      );
		
		      /* 下边短竖线 */
		      canvas.drawLine(
		        Offset(left + squareSide * i, top + squareSide * 5),
		        Offset(left + squareSide * i, top + squareSide * 9),
		        paint,
		      );
		    }
		
		    //中轴线,可要可不要,我这边不要
		    // paint.strokeWidth = 1;
		    // canvas.drawLine(
		    //   Offset(left + gridWidth / 2, top),
		    //   Offset(left + gridWidth / 2, top + squareSide * 9),
		    //   paint,
		    // );
		
		  ...
		}
		
复制

第二步,我们绘制上下两个九宫中的四根斜线:

  static doPaint(
		    Canvas canvas,
		    Paint paint,
		    double gridWidth,
		    double squareSide, {
		    double offsetX,
		    double offsetY,
		  }) {
		
		   ...
		
		   //将与帅周围的线-九宫中的斜线
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top),
		      Offset(left + squareSide * 5, top + squareSide * 2),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 2),
		      Offset(left + squareSide * 5, top),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 7),
		      Offset(left + squareSide * 5, top + squareSide * 9),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 9),
		      Offset(left + squareSide * 5, top + squareSide * 7),
		      paint,
		    );
		
		  ...
		}
		
复制

第三步,我们绘制标定炮和兵卒起始位置的标识,我们用小圆圈来做标识,靠边的兵卒位置则用半圆圈来标示:

  static doPaint(
		    Canvas canvas,
		    Paint paint,
		    double gridWidth,
		    double squareSide, {
		    double offsetX,
		    double offsetY,
		  }) {
		
		   ...
		
		   /* 炮、兵架位置指示 */
		    final positions = [
		      // 炮架位置指示
		      Offset(left + squareSide, top + squareSide * 2),
		      Offset(left + squareSide * 7, top + squareSide * 2),
		      Offset(left + squareSide, top + squareSide * 7),
		      Offset(left + squareSide * 7, top + squareSide * 7),
		      // 部分兵架位置指示
		      Offset(left + squareSide * 2, top + squareSide * 3),
		      Offset(left + squareSide * 4, top + squareSide * 3),
		      Offset(left + squareSide * 6, top + squareSide * 3),
		      Offset(left + squareSide * 2, top + squareSide * 6),
		      Offset(left + squareSide * 4, top + squareSide * 6),
		      Offset(left + squareSide * 6, top + squareSide * 6),
		    ];
		    positions.forEach((pos) {
		      canvas.drawCircle(pos, 5, paint);
		    });
		
		    //兵架靠左边位置指示
		    final leftPositons = [
		      Offset(left, top + squareSide * 3),
		      Offset(left, top + squareSide * 6),
		    ];
		    leftPositons.forEach((pos) {
		      var rect = Rect.fromCenter(center: pos, width: 10, height: 10);
		      canvas.drawArc(rect, -pi / 2, pi, true, paint);
		    });
		    //兵架靠右边位置指示
		    final rightPositions = [
		      Offset(left + squareSide * 8, top + squareSide * 3),
		      Offset(left + squareSide * 8, top + squareSide * 6),
		    ];
		    rightPositions.forEach((pos) {
		      var rect = Rect.fromCenter(center: pos, width: 10, height: 10);
		      canvas.drawArc(rect, pi / 2, pi, true, paint);
		    });
		
		  ...
		}
		
复制

由于画炮和兵卒位置的时候要画圆圈,我们需要用到常量值「pi」,所以记得要引入dart 的 math 库:

import 'dart:math'
		
复制

好一段艰苦的工作,不过终于完成了!整个 BoardPainter 的代码是这样的:

import 'dart:math';
		
		import 'package:chinese_chess/common/color-consts.dart';
		import 'package:flutter/material.dart';
		import 'board-widget.dart';
		
		class BoardPainter extends CustomPainter {
		  //棋盘的宽度, 横盘上线格的总宽度,每一个格子的边长
		  final double width, gridWidth, squareSide;
		  final thePaint = Paint(); //画笔
		
		  BoardPainter({@required this.width})
		      : gridWidth = (width - BoardWidget.Padding * 2) / 9 * 8, //计算横盘上线格的总宽度
		        squareSide = (width - BoardWidget.Padding * 2) / 9; //计算每一个格子的边长
		
		  static doPaint(
		    Canvas canvas,
		    Paint paint,
		    double gridWidth,
		    double squareSide, {
		    double offsetX,
		    double offsetY,
		  }) {
		    paint.color = ColorConsts.BoardLine;
		    paint.style = PaintingStyle.stroke;
		
		    var left = offsetX, top = offsetY;
		
		    //棋盘外框
		    paint.strokeWidth = 2;
		    canvas.drawRect(
		      Rect.fromLTWH(left, top, gridWidth, squareSide * 9),
		      paint,
		    );
		
		    paint.strokeWidth = 1; //设置线宽为1
		
		    //8根横线
		    for (var i = 1; i < 9; i++) {
		      canvas.drawLine(
		        Offset(left, top + squareSide * i),
		        Offset(left + gridWidth, top + squareSide * i),
		        paint,
		      );
		    }
		
		    //上下各6根短竖线
		    for (var i = 0; i < 8; i++) {
		      // if (i == 4) continue; //中间拉通的线已经画过
		
		      /* 上边短竖线 */
		      canvas.drawLine(
		        Offset(left + squareSide * i, top),
		        Offset(left + squareSide * i, top + squareSide * 4),
		        paint,
		      );
		
		      /* 下边短竖线 */
		      canvas.drawLine(
		        Offset(left + squareSide * i, top + squareSide * 5),
		        Offset(left + squareSide * i, top + squareSide * 9),
		        paint,
		      );
		    }
		
		    //中轴线,可要可不要,我这边不要
		    // paint.strokeWidth = 1;
		    // canvas.drawLine(
		    //   Offset(left + gridWidth / 2, top),
		    //   Offset(left + gridWidth / 2, top + squareSide * 9),
		    //   paint,
		    // );
		
		    //将与帅周围的线-九宫中的斜线
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top),
		      Offset(left + squareSide * 5, top + squareSide * 2),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 2),
		      Offset(left + squareSide * 5, top),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 7),
		      Offset(left + squareSide * 5, top + squareSide * 9),
		      paint,
		    );
		    canvas.drawLine(
		      Offset(left + squareSide * 3, top + squareSide * 9),
		      Offset(left + squareSide * 5, top + squareSide * 7),
		      paint,
		    );
		
		    /* 炮、兵架位置指示 */
		    final positions = [
		      // 炮架位置指示
		      Offset(left + squareSide, top + squareSide * 2),
		      Offset(left + squareSide * 7, top + squareSide * 2),
		      Offset(left + squareSide, top + squareSide * 7),
		      Offset(left + squareSide * 7, top + squareSide * 7),
		      // 部分兵架位置指示
		      Offset(left + squareSide * 2, top + squareSide * 3),
		      Offset(left + squareSide * 4, top + squareSide * 3),
		      Offset(left + squareSide * 6, top + squareSide * 3),
		      Offset(left + squareSide * 2, top + squareSide * 6),
		      Offset(left + squareSide * 4, top + squareSide * 6),
		      Offset(left + squareSide * 6, top + squareSide * 6),
		    ];
		    positions.forEach((pos) {
		      canvas.drawCircle(pos, 5, paint);
		    });
		
		    //兵架靠左边位置指示
		    final leftPositons = [
		      Offset(left, top + squareSide * 3),
		      Offset(left, top + squareSide * 6),
		    ];
		    leftPositons.forEach((pos) {
		      var rect = Rect.fromCenter(center: pos, width: 10, height: 10);
		      canvas.drawArc(rect, -pi / 2, pi, true, paint);
		    });
		    //兵架靠右边位置指示
		    final rightPositions = [
		      Offset(left + squareSide * 8, top + squareSide * 3),
		      Offset(left + squareSide * 8, top + squareSide * 6),
		    ];
		    rightPositions.forEach((pos) {
		      var rect = Rect.fromCenter(center: pos, width: 10, height: 10);
		      canvas.drawArc(rect, pi / 2, pi, true, paint);
		    });
		  }
		
		  @override
		  void paint(Canvas canvas, Size size) {
		    doPaint(
		      canvas,
		      thePaint,
		      gridWidth,
		      squareSide,
		      offsetX: BoardWidget.Padding + squareSide / 2,
		      offsetY: BoardWidget.Padding + BoardWidget.DigitsHeight + squareSide / 2,
		    );
		  }
		
		  @override
		  bool shouldRepaint(CustomPainter oldDelegate) {
		    return false;
		    // throw UnimplementedError();
		  }
		}
		
		
复制

成果是显著的,在 vscode 中按F5,看看效果吧?

棋盘上的路数和河界文字

离这一节的目标,还差一点点了!

我们来添加棋盘上的「路数」指示和棋盘中间的「河界」文字吧。

在 lib/board 文件夹下,我们新建一个文件 words-on-board.dart 文件,在其中建立一个名叫 WordsOnBoard 的 StatelessWidget 类:

import 'package:flutter/material.dart';
		import '../common/color-consts.dart';
		
		class WordsOnBoard extends StatelessWidget {
		  static const DigitsFontSize = 18.0;
		  const WordsOnBoard({Key key}) : super(key: key);
		
		  @override
		  Widget build(BuildContext context) {
		    // 棋盘上的路数指示,红方用大写,黑方用全角的小写字母
		    final blackColumns = '123456789', redColumns = '九八七六五四三二一';
		    final bChildren = [], rChildren = [];
		
		    final digitsStyle = TextStyle(fontSize: DigitsFontSize);
		    final rivierTipsStyle = TextStyle(fontSize: 28.0);
		
		    for (var i = 0; i < 9; i++) {
		      bChildren.add(Text(blackColumns[i], style: digitsStyle));
		      rChildren.add(Text(redColumns[i], style: digitsStyle));
		
		      //每一个数字后边添加一个 Expanded,用于平分布局空间
		      if (i < 8) {
		        bChildren.add(Expanded(child: SizedBox()));
		        rChildren.add(Expanded(child: SizedBox()));
		      }
		    }
		
		    final riverTips = Row(
		      children: [
		        Expanded(child: SizedBox()),
		        Text('楚河', style: rivierTipsStyle),
		        Expanded(child: SizedBox(), flex: 2),
		        Text('汉界', style: rivierTipsStyle),
		        Expanded(child: SizedBox()),
		      ],
		    );
		
		    // 放置上、中、下三部分到一个列布局模式中,上和中、中和下之间使用 Expanded 对象平分垂直方向多出来的布局空间
		    return DefaultTextStyle(
		      child: Column(
		        children: [
		          Row(children: bChildren),
		          Expanded(child: SizedBox()),
		          riverTips,
		          Expanded(child: SizedBox()),
		          Row(children: rChildren),
		        ],
		      ),
		      style: TextStyle(color: ColorConsts.BoardTips),
		    );
		  }
		}
		
		
		
复制

在这个 Widget 中,我们将棋盘顶部的黑棋「路数」指示、底部的红棋「路数」指示以及棋盘中间的「楚河」、「汉界」文字添加到一个 Column 布局中,三者之间使用 Expanded 来撑开空白空间。

我们需要将这个布局作为 child 对象添加到绘制棋盘的 CustomPaint 对象身上,这样它将显示在棋盘 CustomPaint 之上,在 BoardWidget 中为 CustomPaint 添加 child,如下边代码所示:

Widget build(BuildContext context) {
		   //
		   return Container(
		     width: width,
		     height: height,
		     decoration: BoxDecoration(
		       borderRadius: BorderRadius.circular(5),
		       color: ColorConsts.BoardBackground,
		    ),
		     child: CustomPaint(
		       painter: BoardPainter(width: width),
		       child: Container(
		         margin: EdgeInsets.symmetric(
		           vertical: Padding,
		           // 因为棋子是放在交叉线上的,不是放置在格子内,所以棋子左右各有一半在格线之外
		           // 这里先依据横盘的宽度计算出一个格子的边长,再依此决定垂直方向的边距
		           horizontal: (width - Padding * 2) / 9 / 2 + Padding - WordsOnBoard.DigitsFontSize / 2,
		        ),
		         child: WordsOnBoard(),
		      ),
		    ),
		  );
		}
		
复制

到此,我们绘制棋盘的任务就完成了,按 F5查看一下我们的成果吧!

将我们的代码提交 git 仓库,本节的目标完成了!

elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git add .
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git commit -m '绘棋盘'
		[master 63f3063] 绘棋盘
		 6 files changed, 284 insertions(+), 2 deletions(-)
		 create mode 100644 lib/board/board-painter.dart
		 create mode 100644 lib/board/board-widget.dart
		 create mode 100644 lib/board/words-on-board.dart
		 create mode 100644 lib/routes/battle-page.dart
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git push
		枚举对象: 17, 完成.
		对象计数中: 100% (17/17), 完成.
		使用 4 个线程进行压缩
		压缩对象中: 100% (10/10), 完成.
		写入对象中: 100% (12/12), 3.94 KiB | 3.94 MiB/s, 完成.
		总共 12 (差异 3),复用 0 (差异 0)
		remote: RocketGit: Info: == Welcome to RocketGit! ==
		remote: RocketGit: Info: you are connecting from IP 27.47.6.229.
		remote: RocketGit: Info: date/time: 2020-08-23 06:10:10 (UTC), debug id e54754.
		remote: RocketGit: Info: refs/heads/master:
		remote: RocketGit: Info: Because you do not have 'push' rights, your push was
		remote: RocketGit: Info: transformed into a merge request and awaits approval.
		To https://rocketgit.com/user/vagi/chinese_chess
		   edd1e70..63f3063  master -> master
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ sudo git push
		[sudo] elapse 的密码: 
		Username for 'https://rocketgit.com': vagi
		Password for 'https://[email protected]': 
		总共 0 (差异 0),复用 0 (差异 0)
		remote: RocketGit: Info: == Welcome to RocketGit! ==
		remote: RocketGit: Info: you are connecting from IP 27.47.6.229.
		remote: RocketGit: Info: date/time: 2020-08-23 06:12:50 (UTC), debug id 6e3df1.
		To https://rocketgit.com/user/vagi/chinese_chess
		   edd1e70..63f3063  master -> master
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$
		
复制

本节回顾

在这一小节中,我们学习和完成了以下的任务:

  • 学习中国象棋棋盘的基本知识
  • 规划使用CustomPaint绘制棋盘的方案
  • 动态计算并绘制棋盘上的网格
  • 通过为 CustomPaint 添加 child 的方式,布局棋盘上的「河界、路数」文字