|
@@ -47,18 +47,19 @@ enum Action {
|
|
|
class BoardPlay extends StatefulWidget {
|
|
class BoardPlay extends StatefulWidget {
|
|
|
final ListItem item;
|
|
final ListItem item;
|
|
|
final bool firstRun;
|
|
final bool firstRun;
|
|
|
|
|
+ final bool reset;
|
|
|
|
|
|
|
|
- const BoardPlay({super.key, required this.item, this.firstRun = false});
|
|
|
|
|
|
|
+ const BoardPlay({super.key, required this.item, this.firstRun = false, this.reset = false});
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
State<StatefulWidget> createState() {
|
|
State<StatefulWidget> createState() {
|
|
|
return _BoardPlayState();
|
|
return _BoardPlayState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- static PageRouteBuilder buildRoute(ListItem item, {bool firstRun = false}) {
|
|
|
|
|
|
|
+ static PageRouteBuilder buildRoute(ListItem item, {bool firstRun = false, bool reset = false}) {
|
|
|
return PageRouteBuilder(
|
|
return PageRouteBuilder(
|
|
|
pageBuilder: (context, animation, secondaryAnimation) {
|
|
pageBuilder: (context, animation, secondaryAnimation) {
|
|
|
- return BoardPlay(item: item, firstRun: firstRun);
|
|
|
|
|
|
|
+ return BoardPlay(item: item, firstRun: firstRun, reset: reset);
|
|
|
},
|
|
},
|
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
|
return FadeTransition(opacity: animation, child: child);
|
|
return FadeTransition(opacity: animation, child: child);
|
|
@@ -89,12 +90,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
late ConfettiLayer confettiLayer;
|
|
late ConfettiLayer confettiLayer;
|
|
|
|
|
|
|
|
ui.Image? _fingerImage; // 手指形状图片,用于制作引导动画
|
|
ui.Image? _fingerImage; // 手指形状图片,用于制作引导动画
|
|
|
- int _hintCount = 0; // 已经展示的手势指引次数
|
|
|
|
|
OverLayer? _overLayer; // 用于展示手势指引的layer层,采用OverlayEntry方案,置于顶层
|
|
OverLayer? _overLayer; // 用于展示手势指引的layer层,采用OverlayEntry方案,置于顶层
|
|
|
- Timer? _hintTimer;
|
|
|
|
|
- int? _lastInteractionTick;
|
|
|
|
|
- // final int maxHints = 3;
|
|
|
|
|
- final int maxHints = 99; // 无限提示
|
|
|
|
|
|
|
|
|
|
Piece? _draggingPiece;
|
|
Piece? _draggingPiece;
|
|
|
|
|
|
|
@@ -109,6 +105,8 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
late Animation<double> _mergeScaleAnimation;
|
|
late Animation<double> _mergeScaleAnimation;
|
|
|
List<PieceGroup>? _mergeGroups; // 记录当前merge的group
|
|
List<PieceGroup>? _mergeGroups; // 记录当前merge的group
|
|
|
|
|
|
|
|
|
|
+ bool showDealing = true; // 是否需要发牌
|
|
|
|
|
+
|
|
|
late AnimationController _prepareAnimationController; // 预备动画, Opacity透明动画展示核心绘制区
|
|
late AnimationController _prepareAnimationController; // 预备动画, Opacity透明动画展示核心绘制区
|
|
|
|
|
|
|
|
late AnimationController dealingAnimationController; // 发牌动画
|
|
late AnimationController dealingAnimationController; // 发牌动画
|
|
@@ -316,6 +314,15 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
setState(() {});
|
|
setState(() {});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 保存状态,本来是不用保存的,竞品也没有保存
|
|
|
|
|
+ void saveProgress() async {
|
|
|
|
|
+ _log.info('saveProgress');
|
|
|
|
|
+ // 没有完成才需要保存
|
|
|
|
|
+ if (board != null && board!.isAllDone == false) {
|
|
|
|
|
+ await saveJson(widget.item.jsonPath, board!.toJson());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void _successAnimationListener() {
|
|
void _successAnimationListener() {
|
|
|
final delta = _offsetAnimation.value;
|
|
final delta = _offsetAnimation.value;
|
|
|
board!.finalRect = board!.targetRect.translate(0, delta);
|
|
board!.finalRect = board!.targetRect.translate(0, delta);
|
|
@@ -607,7 +614,19 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
|
|
final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
|
|
|
final cardImage = cardFrameInfo.image;
|
|
final cardImage = cardFrameInfo.image;
|
|
|
|
|
|
|
|
- board = Board(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device);
|
|
|
|
|
|
|
+ // 看看有没有缓存
|
|
|
|
|
+ if (widget.reset) {
|
|
|
|
|
+ board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ final jsonFile = await localFile(widget.item.jsonPath);
|
|
|
|
|
+ if (await jsonFile.exists()) {
|
|
|
|
|
+ showDealing = false; // 恢复状态不必发牌
|
|
|
|
|
+ board = await Board.restore(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device, widget.item.jsonPath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
board!.prepare();
|
|
board!.prepare();
|
|
|
|
|
|
|
|
// 首次打开应用,需要新手指引
|
|
// 首次打开应用,需要新手指引
|
|
@@ -616,7 +635,11 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
// **修正:在调用 AnimationController 之前检查 `mounted` 状态**
|
|
// **修正:在调用 AnimationController 之前检查 `mounted` 状态**
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
|
|
|
|
|
- _prepareAnimationController.forward(from: 0.0);
|
|
|
|
|
|
|
+ if (showDealing == false) {
|
|
|
|
|
+ board!.start();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _prepareAnimationController.forward(from: 0.0);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
setState(() {
|
|
setState(() {
|
|
|
_isLoading = false;
|
|
_isLoading = false;
|
|
@@ -647,365 +670,43 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
// 首次打开应用或设置开启提示时,启动自动提示计时器
|
|
// 首次打开应用或设置开启提示时,启动自动提示计时器
|
|
|
if (widget.firstRun) {
|
|
if (widget.firstRun) {
|
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
|
- _lastInteractionTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
-
|
|
|
|
|
- // 每秒检查一次是否需要提示
|
|
|
|
|
- _hintTimer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
|
|
|
|
- if (!mounted) return;
|
|
|
|
|
- int nowTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
- if (_overLayer!.isHinting) {
|
|
|
|
|
- _lastInteractionTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
- }
|
|
|
|
|
- // 超过3秒没动静,且提示次数未超限,则给提示
|
|
|
|
|
- if ((nowTick - _lastInteractionTick!) > 3 * 1000 && _hintCount < maxHints) {
|
|
|
|
|
- hint();
|
|
|
|
|
- _lastInteractionTick = nowTick; // 提示后重置计时
|
|
|
|
|
- _hintCount++;
|
|
|
|
|
- } else if (_hintCount >= maxHints) {
|
|
|
|
|
- // 提示次数达到上限,取消计时器
|
|
|
|
|
- _hintTimer?.cancel();
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ hint();
|
|
|
});
|
|
});
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // board_play.dart (在 _BoardPlayState 中,修正 hint 方法)
|
|
|
|
|
-
|
|
|
|
|
- hint() async {
|
|
|
|
|
- // 使用私有字段 _fingerImage, _overLayer, _hintCount
|
|
|
|
|
- if (_fingerImage == null || _overLayer == null || board == null || board!.status != BoardStatus.playing) return;
|
|
|
|
|
-
|
|
|
|
|
- Piece? p1Ref; // 拖拽起点 piece 的参考 (单碎片或群组的 topLeftPiece)
|
|
|
|
|
- Piece? p2; // 合并目标 piece
|
|
|
|
|
- int bestSize = 0; // 记录找到的最佳群组大小
|
|
|
|
|
-
|
|
|
|
|
- // --- Step 1: 确定所有可移动的实体(单碎片和群组)及其大小 ---
|
|
|
|
|
- final List<Map<String, dynamic>> movableEntities = [];
|
|
|
|
|
- final Set<PieceGroup> seenGroups = {};
|
|
|
|
|
-
|
|
|
|
|
- for (final piece in board!.pieces) {
|
|
|
|
|
- if (piece.isOK) continue; // 已归位的碎片/群组不移动
|
|
|
|
|
-
|
|
|
|
|
- if (piece.group == null) {
|
|
|
|
|
- // 1. 单个碎片
|
|
|
|
|
- movableEntities.add({'ref': piece, 'size': 1});
|
|
|
|
|
- } else if (!seenGroups.contains(piece.group!)) {
|
|
|
|
|
- // 2. 群组:使用 topLeftPiece 作为群组的参考点
|
|
|
|
|
- movableEntities.add({'ref': piece.group!.topLeftPiece, 'size': piece.group!.length});
|
|
|
|
|
- seenGroups.add(piece.group!);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // --- Step 2: 按实体大小降序排列,优先引导大群组 ---
|
|
|
|
|
- // b['size'].compareTo(a['size']) 实现降序排序
|
|
|
|
|
- movableEntities.sort((a, b) => b['size'].compareTo(a['size']));
|
|
|
|
|
-
|
|
|
|
|
- // --- Step 3: 搜索有效的合并机会 (Merge Opportunity) ---
|
|
|
|
|
-
|
|
|
|
|
- for (final entityMap in movableEntities) {
|
|
|
|
|
- final p1RefCandidate = entityMap['ref'] as Piece;
|
|
|
|
|
- final currentSize = entityMap['size'] as int;
|
|
|
|
|
- final movingEntity = p1RefCandidate.group ?? p1RefCandidate;
|
|
|
|
|
- final movingPieces = (movingEntity is PieceGroup) ? movingEntity.pieces : [p1RefCandidate];
|
|
|
|
|
-
|
|
|
|
|
- // 遍历移动实体内的所有碎片 p1,寻找一个可以与外部 p2 合并的边缘碎片
|
|
|
|
|
- for (final p1 in movingPieces) {
|
|
|
|
|
- // 遍历 p1 的原图邻居 p2
|
|
|
|
|
- for (final neighborIndex in p1.getNeighbourIndexes()) {
|
|
|
|
|
- final p2Candidate = board!.getPieceByIndex(neighborIndex);
|
|
|
|
|
-
|
|
|
|
|
- if (p2Candidate == null) continue;
|
|
|
|
|
-
|
|
|
|
|
- // p2Candidate 必须不是正在移动的实体的一部分
|
|
|
|
|
- if (p2Candidate.isSameGroup(p1)) continue;
|
|
|
|
|
-
|
|
|
|
|
- // 1. 检查 p1 和 p2Candidate 的相对位置是否正确 (满足合并的前提条件)
|
|
|
|
|
- final isRelativePositionCorrect =
|
|
|
|
|
- (p1.col == p2Candidate.col && (p1.row - p2Candidate.row) == (p1.curRow - p2Candidate.curRow)) ||
|
|
|
|
|
- (p1.row == p2Candidate.row && (p1.col - p2Candidate.col) == (p1.curCol - p2Candidate.curCol));
|
|
|
|
|
-
|
|
|
|
|
- // 2. 如果它们已经是邻居,则应已自动合并,跳过提示
|
|
|
|
|
- if (p1.isCurNeighbour(p2Candidate)) continue;
|
|
|
|
|
-
|
|
|
|
|
- if (isRelativePositionCorrect) {
|
|
|
|
|
- // --- Step 3.1: 模拟移动并进行碰撞/边界检查 (Validity Check) ---
|
|
|
|
|
-
|
|
|
|
|
- // 计算使 p1 与 p2Candidate 合并所需的位移量 (dMoveRow, dMoveCol)
|
|
|
|
|
-
|
|
|
|
|
- // a. p1 在原图上相对于 p2 的差值
|
|
|
|
|
- final int dRow = p1.row - p2Candidate.row;
|
|
|
|
|
- final int dCol = p1.col - p2Candidate.col;
|
|
|
|
|
-
|
|
|
|
|
- // b. p1 移动后的目标网格坐标
|
|
|
|
|
- final targetP1Row = p2Candidate.curRow + dRow;
|
|
|
|
|
- final targetP1Col = p2Candidate.curCol + dCol;
|
|
|
|
|
-
|
|
|
|
|
- // c. 整个实体所需的移动位移 (从 p1 的当前位置到目标位置的距离)
|
|
|
|
|
- final dMoveRow = targetP1Row - p1.curRow;
|
|
|
|
|
- final dMoveCol = targetP1Col - p1.curCol;
|
|
|
|
|
-
|
|
|
|
|
- // 检查整个实体 (movingPieces) 移动 (dMoveRow, dMoveCol) 是否可行
|
|
|
|
|
- bool canPlace = true;
|
|
|
|
|
- for (final movingPiece in movingPieces) {
|
|
|
|
|
- final newRow = movingPiece.curRow + dMoveRow;
|
|
|
|
|
- final newCol = movingPiece.curCol + dMoveCol;
|
|
|
|
|
-
|
|
|
|
|
- // i. 边界检查
|
|
|
|
|
- if (newRow < 0 || newRow >= board!.rows || newCol < 0 || newCol >= board!.cols) {
|
|
|
|
|
- canPlace = false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ii. 碰撞检查: 目标槽位不能被非本实体内的其他碎片占据
|
|
|
|
|
- final overlapPiece = board!.getPieceByCoordinate(newRow, newCol);
|
|
|
|
|
-
|
|
|
|
|
- // 碰撞条件:目标槽位被占据 且 占据者不属于正在移动的实体
|
|
|
|
|
- if (overlapPiece != null && !movingPieces.contains(overlapPiece)) {
|
|
|
|
|
- canPlace = false;
|
|
|
|
|
- break;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (canPlace) {
|
|
|
|
|
- p1Ref = p1RefCandidate; // 拖拽起点 (群组的参考 Piece)
|
|
|
|
|
- p2 = p2Candidate; // 合并目标 Piece
|
|
|
|
|
- bestSize = currentSize; // 记录大小
|
|
|
|
|
- break; // 找到有效的合并提示,跳出 p2 循环
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (p1Ref != null) break; // 找到有效的合并提示,跳出 p1 循环
|
|
|
|
|
- }
|
|
|
|
|
- if (p1Ref != null) break; // 找到有效的合并提示,跳出实体循环 (因为它涉及当前找到的最大群组)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ----------------------------------------------------
|
|
|
|
|
- // --- Step 4: 执行引导动画 (Merge or Revert) ---
|
|
|
|
|
- // ----------------------------------------------------
|
|
|
|
|
-
|
|
|
|
|
- // 引导参数
|
|
|
|
|
- const double fingerSize = 30.0;
|
|
|
|
|
- HintItem? hintItem;
|
|
|
|
|
-
|
|
|
|
|
- if (p1Ref != null && p2 != null) {
|
|
|
|
|
- // 找到了有效的合并提示 (优先选择的合并操作)
|
|
|
|
|
-
|
|
|
|
|
- // a. 拖拽起点中心点: p1Ref 的群组中心或自身中心
|
|
|
|
|
- final p1Center = p1Ref!.group?.center ?? p1Ref!.currentCenter;
|
|
|
|
|
-
|
|
|
|
|
- // b. 拖拽终点中心点: p1Ref 移动后的目标槽位中心
|
|
|
|
|
-
|
|
|
|
|
- // 重新计算 p1Ref 应该移动到的目标网格坐标 (targetRefRow, targetRefCol)
|
|
|
|
|
- final movingEntity = p1Ref!.group ?? p1Ref;
|
|
|
|
|
- final movingPieces = (movingEntity is PieceGroup) ? movingEntity.pieces : [p1Ref!];
|
|
|
|
|
-
|
|
|
|
|
- // 寻找群组中能与 p2 合并的那个边缘碎片 p1
|
|
|
|
|
- final p1 = movingPieces.firstWhere(
|
|
|
|
|
- (p) =>
|
|
|
|
|
- p.isNeighbour(p2!) &&
|
|
|
|
|
- ((p.col == p2!.col && (p.row - p2!.row) == (p.curRow - p2!.curRow)) || (p.row == p2!.row && (p.col - p2!.col) == (p.curCol - p2!.curCol))),
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- final int dRow = p1.row - p2!.row;
|
|
|
|
|
- final int dCol = p1.col - p2!.col;
|
|
|
|
|
- final targetP1Row = p2!.curRow + dRow;
|
|
|
|
|
- final targetP1Col = p2!.curCol + dCol;
|
|
|
|
|
-
|
|
|
|
|
- final dMoveRow = targetP1Row - p1.curRow;
|
|
|
|
|
- final dMoveCol = targetP1Col - p1.curCol;
|
|
|
|
|
-
|
|
|
|
|
- // 最终目标网格坐标是 p1Ref 的当前坐标加上总位移
|
|
|
|
|
- final targetRefRow = p1Ref!.curRow + dMoveRow;
|
|
|
|
|
- final targetRefCol = p1Ref!.curCol + dMoveCol;
|
|
|
|
|
-
|
|
|
|
|
- // 获取目标槽位的中心点
|
|
|
|
|
- final targetTransform = board!.getTransformByCoordinate(targetRefRow, targetRefCol);
|
|
|
|
|
- final targetCenter = Offset(targetTransform.storage[12] + board!.pieceLogicalWidth / 2, targetTransform.storage[13] + board!.pieceLogicalHeight / 2);
|
|
|
|
|
-
|
|
|
|
|
- // 引导:从当前位置拖拽到目标位置
|
|
|
|
|
- final rectStart = Rect.fromCenter(center: p1Center, width: fingerSize, height: fingerSize);
|
|
|
|
|
- final rectEnd = Rect.fromCenter(center: targetCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
-
|
|
|
|
|
- _log.info(
|
|
|
|
|
- 'Hint: MERGE guidance for largest Entity (size: $bestSize) starting at ${p1Ref!.index} to grid ($targetRefRow, $targetRefCol). Merges with ${p2!.index}',
|
|
|
|
|
- );
|
|
|
|
|
- hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 3. 找不到合并操作,回退到归位引导
|
|
|
|
|
-
|
|
|
|
|
- // 沿用之前的逻辑:找一个未归位的单碎片,提示归位。
|
|
|
|
|
- Piece? pRevert = board!.pieces.firstWhereOrNull((p) => !p.isOK && (p.group == null || p.group!.length == 1));
|
|
|
|
|
-
|
|
|
|
|
- if (pRevert != null) {
|
|
|
|
|
- final pRevertCenter = pRevert.group?.center ?? pRevert.currentCenter;
|
|
|
|
|
-
|
|
|
|
|
- // 归位目标位置 (正确网格槽位的中心点)
|
|
|
|
|
- final targetTransform = board!.getTransformByCoordinate(pRevert.row, pRevert.col);
|
|
|
|
|
- final targetCenter = Offset(targetTransform.storage[12] + board!.pieceLogicalWidth / 2, targetTransform.storage[13] + board!.pieceLogicalHeight / 2);
|
|
|
|
|
-
|
|
|
|
|
- // 如果当前中心点和目标中心点距离很近,不提示归位
|
|
|
|
|
- if ((pRevertCenter - targetCenter).distanceSquared < pow(fingerSize * 2, 2)) {
|
|
|
|
|
- _log.info('Hint: Revert target for Piece ${pRevert.index} too close. Skipping.');
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ Future.delayed(const Duration(seconds: 3), () {
|
|
|
|
|
+ if (!mounted) return;
|
|
|
|
|
+ if (_overLayer != null && _overLayer!.isHinting) {
|
|
|
|
|
+ Fluttertoast.showToast(
|
|
|
|
|
+ msg: AppLocalizations.of(context)!.moveToComplete,
|
|
|
|
|
+ toastLength: Toast.LENGTH_SHORT,
|
|
|
|
|
+ gravity: ToastGravity.BOTTOM,
|
|
|
|
|
+ timeInSecForIosWeb: 1,
|
|
|
|
|
+ backgroundColor: SkinHelper.slotBorderColor,
|
|
|
|
|
+ textColor: Colors.white,
|
|
|
|
|
+ fontSize: 16.0,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 引导:从当前位置拖拽到正确网格中心
|
|
|
|
|
- final rectStart = Rect.fromCenter(center: pRevertCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
- final rectEnd = Rect.fromCenter(center: targetCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
-
|
|
|
|
|
- _log.info('Hint: Revert guidance for Piece ${pRevert.index}');
|
|
|
|
|
- hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 4. 执行引导动画
|
|
|
|
|
- if (hintItem != null) {
|
|
|
|
|
- _overLayer?.doHint(hintItem);
|
|
|
|
|
- Fluttertoast.showToast(
|
|
|
|
|
- msg: AppLocalizations.of(context)!.moveToComplete,
|
|
|
|
|
- toastLength: Toast.LENGTH_SHORT,
|
|
|
|
|
- gravity: ToastGravity.BOTTOM,
|
|
|
|
|
- timeInSecForIosWeb: 1,
|
|
|
|
|
- backgroundColor: SkinHelper.slotBorderColor,
|
|
|
|
|
- textColor: Colors.white,
|
|
|
|
|
- fontSize: 16.0,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 展现提示 (自动手势引导)
|
|
|
|
|
- hint2() async {
|
|
|
|
|
- _log.info('新手手势提示');
|
|
|
|
|
-
|
|
|
|
|
|
|
+ hint() async {
|
|
|
// 使用私有字段 _fingerImage, _overLayer, _hintCount
|
|
// 使用私有字段 _fingerImage, _overLayer, _hintCount
|
|
|
if (_fingerImage == null || _overLayer == null || board == null || board!.status != BoardStatus.playing) return;
|
|
if (_fingerImage == null || _overLayer == null || board == null || board!.status != BoardStatus.playing) return;
|
|
|
|
|
|
|
|
- // 1. 尝试寻找一个可以触发合并 (merge) 的拖拽操作 (p1 拖向 p2 的邻居槽位)
|
|
|
|
|
- Piece? p1; // 拖拽起点 piece
|
|
|
|
|
- Piece? p2; // 拖拽目标 piece (p1 将拖到 p2 的邻居槽位,从而实现合并)
|
|
|
|
|
-
|
|
|
|
|
- // 遍历所有碎片,寻找可以作为拖拽起点 p1 的候选碎片:
|
|
|
|
|
- // 仅考虑未归位的、单个碎片或群组的边缘碎片。
|
|
|
|
|
- // 我们依然使用 (p.group == null || length == 1) 的逻辑来简化起点筛选,即只引导单个碎片。
|
|
|
|
|
- for (final piece in board!.pieces) {
|
|
|
|
|
- // 限制 p1 为未归位的单个碎片/群组 (修正后的 null/length == 1 检查)
|
|
|
|
|
- if (piece.isOK || (piece.group != null && piece.group!.length > 1)) {
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 找到 piece 在原图上的所有邻居 (p2 的候选)
|
|
|
|
|
- for (final neighborIndex in piece.getNeighbourIndexes()) {
|
|
|
|
|
- final neighbor = board!.getPieceByIndex(neighborIndex);
|
|
|
|
|
-
|
|
|
|
|
- if (neighbor == null) continue;
|
|
|
|
|
-
|
|
|
|
|
- // 检查 p1 和 p2 是否满足合并的“原图条件”和“相对位置条件”
|
|
|
|
|
- // canMerge() 依赖 isNeighbour(),所以 p1 和 p2 必须是原图邻居。
|
|
|
|
|
- // 注意:canMerge() 也会检查 isCurNeighbour()。
|
|
|
|
|
-
|
|
|
|
|
- // 核心逻辑:如果满足 canMerge,说明当前已经合并或即将自动合并,不需要提示。
|
|
|
|
|
- // 我们需要找的是:原图相邻且相对位置正确,但 *当前不相邻* 的碎片。
|
|
|
|
|
-
|
|
|
|
|
- // p1 和 p2 必须是原图邻居
|
|
|
|
|
- if (!piece.isNeighbour(neighbor)) continue;
|
|
|
|
|
-
|
|
|
|
|
- // 检查 p1 和 p2 的相对位置是否正确(确保可以合并)
|
|
|
|
|
- final isRelativePositionCorrect =
|
|
|
|
|
- (piece.col == neighbor.col && (piece.row - neighbor.row) == (piece.curRow - neighbor.curRow)) ||
|
|
|
|
|
- (piece.row == neighbor.row && (piece.col - neighbor.col) == (piece.curCol - neighbor.curCol));
|
|
|
|
|
-
|
|
|
|
|
- // 检查它们当前是否相邻
|
|
|
|
|
- final isCurrentlyNeighbor = piece.isCurNeighbour(neighbor);
|
|
|
|
|
-
|
|
|
|
|
- // 提示条件:原图是邻居 AND 相对位置正确 AND 当前不相邻
|
|
|
|
|
- if (isRelativePositionCorrect && !isCurrentlyNeighbor) {
|
|
|
|
|
- p1 = piece;
|
|
|
|
|
- p2 = neighbor;
|
|
|
|
|
- break; // 找到第一个非相邻的合并机会即可
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (p1 != null) break;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 引导参数
|
|
|
|
|
- const double fingerSize = 30.0;
|
|
|
|
|
- HintItem? hintItem;
|
|
|
|
|
-
|
|
|
|
|
- if (p1 != null && p2 != null) {
|
|
|
|
|
- // 2. 执行“连接”引导 (p1 拖向 p2 所在的网格槽位,使其相邻)
|
|
|
|
|
-
|
|
|
|
|
- // a. 拖拽起点:p1 群组的当前中心点
|
|
|
|
|
- final p1Center = p1.group?.center ?? p1.currentCenter;
|
|
|
|
|
-
|
|
|
|
|
- // b. 拖拽终点:p1 拖动后应到达的网格槽位的中心点。
|
|
|
|
|
- // 这个目标槽位是 p2 当前所占网格槽位旁边的一个空槽位,该槽位应与 p1 在原图上的相对位置一致。
|
|
|
|
|
|
|
+ double fingerSize = board!.pieceLogicalWidth / 2.5;
|
|
|
|
|
|
|
|
- // 确定 p1 应该移动到的目标网格坐标 (row, col)
|
|
|
|
|
- int targetRow = p2.curRow;
|
|
|
|
|
- int targetCol = p2.curCol;
|
|
|
|
|
-
|
|
|
|
|
- // 根据 p1 和 p2 在原图上的相对位置,计算 p1 移动后应占领的网格槽位
|
|
|
|
|
- // 目标网格坐标 = p2 的当前网格坐标 + (p1 的正确坐标 - p2 的正确坐标)
|
|
|
|
|
- // 假设 p1(R:1, C:2) 和 p2(R:1, C:3) 是原图邻居。
|
|
|
|
|
- // 相对位移: dR = 0, dC = -1.
|
|
|
|
|
- // 如果 p2 当前在 (curR: 5, curC: 5), 那么 p1 应该移动到 (5, 5 + (-1)) = (5, 4)。
|
|
|
|
|
-
|
|
|
|
|
- final int dRow = p1.row - p2.row; // p1 相对于 p2 的行差值 (-1, 0, 1)
|
|
|
|
|
- final int dCol = p1.col - p2.col; // p1 相对于 p2 的列差值 (-1, 0, 1)
|
|
|
|
|
-
|
|
|
|
|
- targetRow = p2.curRow + dRow;
|
|
|
|
|
- targetCol = p2.curCol + dCol;
|
|
|
|
|
-
|
|
|
|
|
- // 检查目标网格是否溢出边界(理论上不需要,因为 p2 在板上,dRow/dCol 只有 +/-1 或 0)
|
|
|
|
|
- if (targetRow < 0 || targetRow >= board!.rows || targetCol < 0 || targetCol >= board!.cols) {
|
|
|
|
|
- // 目标网格无效,跳过本次提示
|
|
|
|
|
- _log.warning('Hint target coordinate ($targetRow, $targetCol) out of bounds. Skipping.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 获取目标槽位的变换矩阵 (左上角坐标)
|
|
|
|
|
- final targetTransform = board!.getTransformByCoordinate(targetRow, targetCol);
|
|
|
|
|
- final targetCenter = Offset(targetTransform.storage[12] + board!.pieceLogicalWidth / 2, targetTransform.storage[13] + board!.pieceLogicalHeight / 2);
|
|
|
|
|
-
|
|
|
|
|
- // 拖拽起点 Rect (中心在 p1Center)
|
|
|
|
|
- final rectStart = Rect.fromCenter(center: p1Center, width: fingerSize, height: fingerSize);
|
|
|
|
|
- // 拖拽终点 Rect (中心在 targetCenter)
|
|
|
|
|
- final rectEnd = Rect.fromCenter(center: targetCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
-
|
|
|
|
|
- _log.info('Hint: Merge guidance for Piece ${p1.index} to neighbour of Piece ${p2.index} at grid ($targetRow, $targetCol)');
|
|
|
|
|
- hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 3. 找不到合并操作,尝试执行“归位”引导 (将未归位的 piece 拖向正确槽位)
|
|
|
|
|
- // 沿用之前的逻辑:找一个未归位的单碎片,提示归位。
|
|
|
|
|
- Piece? pRevert = board!.pieces.firstWhereOrNull((p) => !p.isOK && (p.group == null || p.group!.length == 1));
|
|
|
|
|
-
|
|
|
|
|
- if (pRevert != null) {
|
|
|
|
|
- final pRevertCenter = pRevert.group?.center ?? pRevert.currentCenter;
|
|
|
|
|
-
|
|
|
|
|
- // 归位目标位置 (正确网格槽位的中心点)
|
|
|
|
|
- final targetTransform = board!.getTransformByCoordinate(pRevert.row, pRevert.col);
|
|
|
|
|
- final targetCenter = Offset(targetTransform.storage[12] + board!.pieceLogicalWidth / 2, targetTransform.storage[13] + board!.pieceLogicalHeight / 2);
|
|
|
|
|
-
|
|
|
|
|
- // 如果当前中心点和目标中心点距离很近,不提示归位
|
|
|
|
|
- if ((pRevertCenter - targetCenter).distanceSquared < pow(fingerSize * 2, 2)) {
|
|
|
|
|
- _log.info('Hint: Revert target for Piece ${pRevert.index} too close. Skipping.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 固定的位置提示,因为首关我们已经编排好了, 不是随机排序
|
|
|
|
|
+ final Offset centerStart = Offset(
|
|
|
|
|
+ board!.targetRect.topLeft.dx + board!.pieceLogicalWidth,
|
|
|
|
|
+ board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 5 / 2,
|
|
|
|
|
+ );
|
|
|
|
|
+ final Offset centerEnd = Offset(board!.targetRect.topLeft.dx + board!.pieceLogicalWidth, board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 3 / 2);
|
|
|
|
|
|
|
|
- // 引导:从当前位置拖拽到正确网格中心
|
|
|
|
|
- final rectStart = Rect.fromCenter(center: pRevertCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
- final rectEnd = Rect.fromCenter(center: targetCenter, width: fingerSize, height: fingerSize);
|
|
|
|
|
|
|
+ final rectStart = Rect.fromCenter(center: centerStart, width: fingerSize, height: fingerSize);
|
|
|
|
|
+ final rectEnd = Rect.fromCenter(center: centerEnd, width: fingerSize, height: fingerSize);
|
|
|
|
|
|
|
|
- _log.info('Hint: Revert guidance for Piece ${pRevert.index}');
|
|
|
|
|
- hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 4. 执行引导动画
|
|
|
|
|
- if (hintItem != null) {
|
|
|
|
|
- _overLayer?.doHint(hintItem);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ final hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
|
|
|
|
|
+ _overLayer?.doHint(hintItem);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
@@ -1016,6 +717,8 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
|
|
|
|
|
@override
|
|
@override
|
|
|
dispose() {
|
|
dispose() {
|
|
|
|
|
+ _log.info('dispose');
|
|
|
|
|
+
|
|
|
timer.cancel();
|
|
timer.cancel();
|
|
|
itemLoader.progress.removeListener(_onProgressUpdate);
|
|
itemLoader.progress.removeListener(_onProgressUpdate);
|
|
|
|
|
|
|
@@ -1055,55 +758,80 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
super.dispose();
|
|
super.dispose();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @override
|
|
|
|
|
+ onInactive() {
|
|
|
|
|
+ super.onInactive();
|
|
|
|
|
+ // 游戏进入后台,保存一下状态
|
|
|
|
|
+ saveProgress();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/// gallery页面加载的时候,可能广告模块还没有初始化完毕
|
|
/// gallery页面加载的时候,可能广告模块还没有初始化完毕
|
|
|
Future<bool> _bannerReadyAndShouldShow() async {
|
|
Future<bool> _bannerReadyAndShouldShow() async {
|
|
|
bool ready = await adSDKReady();
|
|
bool ready = await adSDKReady();
|
|
|
return ready && shouldShowBannerAd(data.currentLevel);
|
|
return ready && shouldShowBannerAd(data.currentLevel);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void _onWillPop(bool didPop, dynamic result) async {
|
|
|
|
|
+ _log.info('board play will pop, dipPop=$didPop, result=$result');
|
|
|
|
|
+ if (didPop) {
|
|
|
|
|
+ // 页面已经退出了
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 页面尚未退出
|
|
|
|
|
+ Future.delayed(const Duration(milliseconds: 100)).then((_) {
|
|
|
|
|
+ saveProgress();
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!mounted) return;
|
|
|
|
|
+ Navigator.of(context).pop();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@override
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
Widget build(BuildContext context) {
|
|
|
Device device = context.read<Device>();
|
|
Device device = context.read<Device>();
|
|
|
|
|
|
|
|
- return Scaffold(
|
|
|
|
|
- body: Stack(
|
|
|
|
|
- children: <Widget>[
|
|
|
|
|
- if (!_isLoading) Positioned.fill(child: _buildPuzzleCanvas(device.screenSize.width, device.screenSize.height)),
|
|
|
|
|
- if (board == null || board!.status != BoardStatus.success) Positioned(top: 0, left: 0, right: 0, child: appBar),
|
|
|
|
|
- // Positioned(top: 0, left: 0, right: 0, child: appBar),
|
|
|
|
|
- Positioned(
|
|
|
|
|
- bottom: 0,
|
|
|
|
|
- left: 0,
|
|
|
|
|
- right: 0,
|
|
|
|
|
- child: SafeArea(
|
|
|
|
|
- child: SizedBox(
|
|
|
|
|
- // 始终预留一个固定的高度,防止布局跳变
|
|
|
|
|
- height: context.read<Device>().bannerHeight,
|
|
|
|
|
- width: double.infinity,
|
|
|
|
|
- child: FutureBuilder<bool>(
|
|
|
|
|
- future: _bannerReadyAndShouldShow(),
|
|
|
|
|
- builder: (context, snapshot) {
|
|
|
|
|
- if (snapshot.hasData && snapshot.data == true) {
|
|
|
|
|
- return adBanner;
|
|
|
|
|
- }
|
|
|
|
|
- return Container(
|
|
|
|
|
- // color: Colors.grey.shade100,
|
|
|
|
|
- );
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ return PopScope(
|
|
|
|
|
+ canPop: false,
|
|
|
|
|
+ onPopInvokedWithResult: _onWillPop,
|
|
|
|
|
+ child: Scaffold(
|
|
|
|
|
+ body: Stack(
|
|
|
|
|
+ children: <Widget>[
|
|
|
|
|
+ if (!_isLoading) Positioned.fill(child: _buildPuzzleCanvas(device.screenSize.width, device.screenSize.height)),
|
|
|
|
|
+ if (board == null || board!.status != BoardStatus.success) Positioned(top: 0, left: 0, right: 0, child: appBar),
|
|
|
|
|
+ // Positioned(top: 0, left: 0, right: 0, child: appBar),
|
|
|
|
|
+ Positioned(
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ left: 0,
|
|
|
|
|
+ right: 0,
|
|
|
|
|
+ child: SafeArea(
|
|
|
|
|
+ child: SizedBox(
|
|
|
|
|
+ // 始终预留一个固定的高度,防止布局跳变
|
|
|
|
|
+ height: context.read<Device>().bannerHeight,
|
|
|
|
|
+ width: double.infinity,
|
|
|
|
|
+ child: FutureBuilder<bool>(
|
|
|
|
|
+ future: _bannerReadyAndShouldShow(),
|
|
|
|
|
+ builder: (context, snapshot) {
|
|
|
|
|
+ if (snapshot.hasData && snapshot.data == true) {
|
|
|
|
|
+ return adBanner;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Container(
|
|
|
|
|
+ // color: Colors.grey.shade100,
|
|
|
|
|
+ );
|
|
|
|
|
+ },
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- successBanner,
|
|
|
|
|
- nextButton,
|
|
|
|
|
- if (_isLoading)
|
|
|
|
|
- Positioned.fill(
|
|
|
|
|
- child: Container(
|
|
|
|
|
- color: SkinHelper.wholeBgColor,
|
|
|
|
|
- child: const Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Colors.white))),
|
|
|
|
|
|
|
+ successBanner,
|
|
|
|
|
+ nextButton,
|
|
|
|
|
+ if (_isLoading)
|
|
|
|
|
+ Positioned.fill(
|
|
|
|
|
+ child: Container(
|
|
|
|
|
+ color: SkinHelper.wholeBgColor,
|
|
|
|
|
+ child: const Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Colors.white))),
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
- ),
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
),
|
|
),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -1134,6 +862,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
padding: EdgeInsets.zero, // 清除默认内边距,确保按钮尺寸准确
|
|
padding: EdgeInsets.zero, // 清除默认内边距,确保按钮尺寸准确
|
|
|
onPressed: () {
|
|
onPressed: () {
|
|
|
audio.playSfx(SfxType.click);
|
|
audio.playSfx(SfxType.click);
|
|
|
|
|
+ saveProgress(); // 有可能会返回,保存一下进度
|
|
|
Navigator.push(context, SettingsDialog.buildRoute(showReturn: true, showRestart: true, item: widget.item));
|
|
Navigator.push(context, SettingsDialog.buildRoute(showReturn: true, showRestart: true, item: widget.item));
|
|
|
},
|
|
},
|
|
|
style: ButtonStyle(
|
|
style: ButtonStyle(
|
|
@@ -1231,13 +960,22 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
|
|
gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
|
|
|
onPressed: () async {
|
|
onPressed: () async {
|
|
|
audio.playSfx(SfxType.click);
|
|
audio.playSfx(SfxType.click);
|
|
|
- ///////////////// 播放插屏广告 //////////////////
|
|
|
|
|
- audio.pauseMusic();
|
|
|
|
|
- await showInterstitialAd('level_exit', widget.item.id, data.currentLevel);
|
|
|
|
|
- audio.startMusic();
|
|
|
|
|
|
|
+ bool adResult = false;
|
|
|
|
|
+ if (data.currentLevel % 25 != 0) {
|
|
|
|
|
+ // 如果是合集最后的关卡, 这个时候不要展示插屏,因为接下来的合集解锁动画比较密集
|
|
|
|
|
+ adResult = await showInterstitialAd('level_done', widget.item.id, data.currentLevel - 1);
|
|
|
|
|
+ }
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
- //////////////// 插屏广告结束 /////////////////
|
|
|
|
|
- Navigator.pop(context, true);
|
|
|
|
|
|
|
+ if (adResult) {
|
|
|
|
|
+ // 广告播放结束,延迟一下再返回
|
|
|
|
|
+ Future.delayed(Duration(milliseconds: 100), () {
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ Navigator.pop(context, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Navigator.pop(context, true);
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
child: Text(AppLocalizations.of(context)!.next, style: TextStyle(color: Colors.white, fontSize: 20)),
|
|
child: Text(AppLocalizations.of(context)!.next, style: TextStyle(color: Colors.white, fontSize: 20)),
|
|
|
),
|
|
),
|
|
@@ -1268,7 +1006,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
children: [
|
|
children: [
|
|
|
// 1. 图片充满容器(与容器尺寸一致)
|
|
// 1. 图片充满容器(与容器尺寸一致)
|
|
|
Image.asset(
|
|
Image.asset(
|
|
|
- 'assets/images/banner3.png',
|
|
|
|
|
|
|
+ 'assets/images/banner.png',
|
|
|
width: double.infinity, // 图片宽=容器宽
|
|
width: double.infinity, // 图片宽=容器宽
|
|
|
height: double.infinity, // 图片高=容器高
|
|
height: double.infinity, // 图片高=容器高
|
|
|
fit: BoxFit.cover, // 图片填充容器(不拉伸,超出部分裁剪)
|
|
fit: BoxFit.cover, // 图片填充容器(不拉伸,超出部分裁剪)
|
|
@@ -1303,7 +1041,6 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
void _onPanStart(DragStartDetails details) {
|
|
void _onPanStart(DragStartDetails details) {
|
|
|
_log.info('_onPanStart');
|
|
_log.info('_onPanStart');
|
|
|
|
|
|
|
|
- _lastInteractionTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
_overLayer?.stopHint();
|
|
_overLayer?.stopHint();
|
|
|
|
|
|
|
|
if (board!.status != BoardStatus.playing) {
|
|
if (board!.status != BoardStatus.playing) {
|
|
@@ -1387,7 +1124,6 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _onPanUpdate(DragUpdateDetails details) {
|
|
void _onPanUpdate(DragUpdateDetails details) {
|
|
|
- _lastInteractionTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
_overLayer?.stopHint();
|
|
_overLayer?.stopHint();
|
|
|
|
|
|
|
|
if (_draggingPiece == null) return;
|
|
if (_draggingPiece == null) return;
|
|
@@ -1410,7 +1146,6 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
|
|
|
void _onPanEnd(DragEndDetails details) {
|
|
void _onPanEnd(DragEndDetails details) {
|
|
|
_log.info('_onPanEnd');
|
|
_log.info('_onPanEnd');
|
|
|
|
|
|
|
|
- _lastInteractionTick = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
|
_overLayer?.stopHint();
|
|
_overLayer?.stopHint();
|
|
|
|
|
|
|
|
if (_draggingPiece == null) {
|
|
if (_draggingPiece == null) {
|