2048
GitHub仓库:xxbaizero0/game_2048: Flutter game (github.com)
AspectRatio参考文章
- Flutter 约束宽高比的控件 AspectRatio - 掘金 (juejin.cn)
- flutter系列之:按比例缩放的AspectRatio和FractionallySizedBox - 掘金 (juejin.cn)
GridView参考文章Flutter网格型布局 - GridView篇 - 掘金 (juejin.cn)
GestureDetector参考文章 Flutter 手势系列教程—GestureDetector - 掘金 (juejin.cn)
shared_preferences参考文章 Flutter shared_preferences的基本使用、源码分析、封装 - 掘金 (juejin.cn)
privide参考文章
- Flutter Provider状态管理—八种提供者使用分析 - 掘金 (juejin.cn)
- https://flyingstudio.feishu.cn/wiki/E2TSwTtZ9ik92EkCLpHcjAvbnRu?from=from_copylink
预览
- UI界面
- Header部分
- Panel部分
- 游戏逻辑
- 数据源与游戏初始化
- 滑动手势识别
- 数字块的移动与合并
- 产生新的数字块
- 判断游戏结束
- 重新开始游戏
UI界面
Header部分
简单分析:
- 主要是Row结构,children左右均为Column结构
- 代码:
1 |
|
1 | Container newGame() { |
Panel部分
分析:
- 首先实现底部的深色背景,接着数组映射到每一个小方块,其中映射通过
GridView
组件快速的构建出4x4的网格,而GridView
常与AspectRatio
组件一起搭配(GridView作为AR的child)
在GridView 中,要控制住每一张图片或方块的宽高比。
如果没有AspectRatio 控件则比较难实现,因为要算间距之类的。
但是有了 AspectRatio,我们的代码就会简单很多
知识点
AspectRatio参考文章
- Flutter 约束宽高比的控件 AspectRatio - 掘金 (juejin.cn)
- flutter系列之:按比例缩放的AspectRatio和FractionallySizedBox - 掘金 (juejin.cn)
GridView参考文章Flutter网格型布局 - GridView篇 - 掘金 (juejin.cn)
AspectRatio 控件
AspectRatio构造函数
1 | const AspectRatio({ |
AspectRatio属性和说明
字段 | 属性 | 描述 |
---|---|---|
aspectRatio | double | 纵横比例 |
child | Widget | 子组件 |
aspectRatio、child
aspectRatio
主要用来设定子组件的纵横比例,而child
就是需要被设定纵横比例的子组件。
GridView组件
GridView
一共有5个构造函数:GridView
,GridView.builder
,GridView.count
,GridView.extent
和GridView.custom
我在游戏里用的是GridView.builder
来看下GridView
构造函数(已省略不常用属性):
1 | GridView({ |
重点关注下gridDelegate
这个参数,它其实是GridView
组件如何控制排列子元素的一个委托。跟踪源码我们可以在scroll_view.dart中看到,gridDelegate
的类型是SliverGridDelegate
,进一步跟踪进sliver_grid.dart可以看到SliverGridDelegate
其实是一个抽象类,而且一共有两个实现类:
SliverGridDelegateWithFixedCrossAxisCount
:用于固定列数的场景;SliverGridDelegateWithMaxCrossAxisExtent
:用于子元素有最大宽度限制的场景;
来看下其构造函数:
1 | 复制代码SliverGridDelegateWithFixedCrossAxisCount({ |
crossAxisCount
:列数,即一行有几个子元素;mainAxisSpacing
:主轴方向上的空隙间距;crossAxisSpacing
:次轴方向上的空隙间距;childAspectRatio
:子元素的宽高比例。
想必看到上面的示例图,你就秒懂其中各个参数的含义了。不过,这里有一点需要特别注意:如果你的子元素宽高比例不为1,那么你一定要设置childAspectRatio
属性。
还记得之前GridView
的各种构造函数吗?其实:
GridView
默认构造函数可以类比于ListView
默认构造函数,适用于有限个数子元素的场景,因为GridView
组件会一次性全部渲染children
中的子元素组件;GridView.builder
构造函数可以类比于ListView.builder
构造函数,适用于长列表的场景,因为GridView
组件会根据子元素是否出现在屏幕内而动态创建销毁,减少内存消耗,更高效渲染;GridView.count
构造函数是GrdiView
使用SliverGridDelegateWithFixedCrossAxisCount
的简写(语法糖),效果完全一致;GridView.extent
构造函数式GridView
使用SliverGridDelegateWithMaxCrossAxisExtent
的简写(语法糖),效果完全一致。
先来看一个简单的例子,它使用到GridView.count
构造函数模仿美团外卖首页服务列表(服务菜单项的代码可以看这里,也算是对基础组件使用的进一步巩固):
代码(文件地址)
1 | GridView.count( |
预览
再来看一个模仿喜马拉雅中相声列表用到GridView.builder
创建网格布局的具体例子(相声卡片的代码可以看这里):
代码(文件地址)
1 | GridView.builder( |
预览
我的实现:
1 | AspectRatio( |
游戏逻辑
颜色配置
1 |
|
用字典将数字与数字背景色和数字字体色对应起来,方便映射渲染。
基本框架
1 |
|
数据源与游戏初始化
本游戏二维列表为数据源,通过_buildGameCell
方法映射渲染
1 | Widget _buildGameCell(int value) { |
初始化
1 |
|
滑动手势识别
GestureDetector参考文章 Flutter 手势系列教程—GestureDetector - 掘金 (juejin.cn)
其分为
- 单击手势
- 双击手势
- 长按手势
- 垂直滑动手势
- 水平滑动手势
- 拖动手势
- 缩放手势
- 其他手势
本来我是使用垂直滑动和水平滑动手势的,但是一次手势只能对应一次滑动,索性直接用拖动手势判断了
拖动手势总共有五种,分别如下:
字段 | 属性 | 描述 |
---|---|---|
onPanDown | GestureDragDownCallback | 手指按下时的回调函数 |
onPanStart | GestureDragStartCallback | 手指开始拖动时的回调函数 |
onPanUpdate | GestureDragUpdateCallback | 手指移动时的回调函数 |
onPanEnd | GestureDragEndCallback | 手指抬起时的回调函数 |
onPanCancel | GestureDragCancelCallback | 手指取消拖动时的回调函数 |
代码:
1 | Offset lastPosition = Offset.zero; |
1 | void setMoveState(Side side) { |
数字块的移动与合并
失败的构想
我们可以设置一个Side类,它枚举四个方位,初始方向是北
我们只需要实现向上滑的逻辑,然后通过改变视角,即数组的旋转,使其套用之前实现好的向上滑的逻辑,就可以实现一套代码多次使用。但是改变视角的逻辑我没写出来。。。。
移动合并的逻辑
1 | bool tilt(Side side) { |
1 | void moveTilesUp() { |
分数的更新
privide参考文章
- Flutter Provider状态管理—八种提供者使用分析 - 掘金 (juejin.cn)
- https://flyingstudio.feishu.cn/wiki/E2TSwTtZ9ik92EkCLpHcjAvbnRu?from=from_copylink
因为我Header和Panel是分开的,导致分数的传递很麻烦,于是我用了Flutter的Provider状态管理
首先我在main函数添加了ChangeNotifierProvider组件
1 | void main() { |
模组Score为:
1 | class Score with ChangeNotifier { |
这样子在Panel中合并时,调用setScore
,这样子notifyListeners()
就会通知Hearder中的分数更新
1 | Container( |
最高分数的导入
shared_preferences参考文章 Flutter shared_preferences的基本使用、源码分析、封装 - 掘金 (juejin.cn)
导入插件shared_preferences: ^2.0.7
获取实例对象
1 | SharedPreferences? sharedPreferences = await SharedPreferences.getInstance(); |
设置持久化数据
我们可以通过sharedPreferences
的实例化对象调用对应的set
方法设置持久化数据
1 | SharedPreferences? sharedPreferences; |
读取持久化数据
我们可以通过sharedPreferences
的实例化对象调用对应的get
方法读取持久化数据
1 | Text("名字: ${sharedPreferences?.getString("name") ?? ""}", |
代码
1 | static const GAME_2048_HIGHEST_SCORE = "game_2048_highest_score"; |
判断游戏结束
1 |
|
重新开始游戏
privide参考文章
- Flutter Provider状态管理—八种提供者使用分析 - 掘金 (juejin.cn)
- https://flyingstudio.feishu.cn/wiki/E2TSwTtZ9ik92EkCLpHcjAvbnRu?from=from_copylink
因为我Header和Panel是分开的,所以又有一个头疼的问题:NEW GAME按钮按下后,列表被清零了,但是 Panel页面并不会得到更新的通知,所以为了解决这个问题,我又使用了Provider
Header
1 | onTap: () { |
Panel
1 | void NewGame() { |
1 | AspectRatio( |
不知道为什么只有listen设为true后才有用