Flutter for OpenHarmony游戏集合App实战之蜘蛛纸牌牌堆叠放
本文介绍了Flutter实现蜘蛛纸牌拖拽功能的关键技术。主要使用LongPressDraggable和DragTarget组件,通过100毫秒延迟避免误触。拖拽时检查牌面是否朝上及是否为连续递减序列,确保符合游戏规则。拖拽过程中显示半透明原位置牌和堆叠的牌组视觉反馈。目标列接收时验证顶部牌是否比拖拽牌大1或为空列。完整实现了蜘蛛纸牌的核心拖牌逻辑,包括拖拽开始、过程反馈和放下验证等完整流程。
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
蜘蛛纸牌最核心的操作就是拖牌。把一叠牌从一列拖到另一列,按照规则堆叠起来。
这个功能涉及到Flutter的拖拽系统:Draggable和DragTarget。这篇就来聊聊怎么实现牌的拖拽和堆叠。
拖拽的基本概念
Flutter的拖拽系统有两个核心组件:
- Draggable: 可以被拖动的东西
- DragTarget: 可以接收拖动物的目标
拖拽流程是:用户按住Draggable开始拖,拖到DragTarget上方松手,DragTarget决定是否接收。
蜘蛛纸牌里,牌是Draggable,列是DragTarget。
LongPressDraggable
我们用的是LongPressDraggable而不是普通的Draggable:
return LongPressDraggable<Map<String, int>>(
data: {'col': colIndex, 'index': cardIndex},
delay: const Duration(milliseconds: 100),
为什么用长按拖拽
普通Draggable一碰就开始拖,太灵敏了。玩家可能只是想点击看看牌,结果不小心拖动了。
LongPressDraggable需要长按一小段时间才开始拖,避免误触。
delay参数
delay: const Duration(milliseconds: 100),
长按100毫秒后才开始拖拽。这个时间不长,不会让人觉得反应慢,但足够区分点击和拖拽。
💡 100毫秒是试出来的。一开始用默认的500毫秒,感觉太慢了,改成100刚好。
data参数
data: {'col': colIndex, 'index': cardIndex},
拖拽时携带的数据,告诉DragTarget这张牌来自哪一列的第几张。
用Map存储两个信息:列索引和牌索引。DragTarget收到后可以知道要移动哪些牌。
拖拽开始
onDragStarted: () => _onDragStart(colIndex, cardIndex),
拖拽开始时调用_onDragStart:
void _onDragStart(int colIndex, int cardIndex) {
if (!columns[colIndex][cardIndex].isFaceUp) return;
if (!_isValidDragSequence(colIndex, cardIndex)) return;
setState(() {
dragSourceColumn = colIndex;
dragStartIndex = cardIndex;
draggingCards = columns[colIndex].sublist(cardIndex);
});
}
前置检查
if (!columns[colIndex][cardIndex].isFaceUp) return;
背面的牌不能拖。只有翻开的牌才能移动。
if (!_isValidDragSequence(colIndex, cardIndex)) return;
检查从这张牌开始到列底的所有牌是否形成有效序列。蜘蛛纸牌的规则是:只能拖连续递减的牌。
记录拖拽状态
setState(() {
dragSourceColumn = colIndex;
dragStartIndex = cardIndex;
draggingCards = columns[colIndex].sublist(cardIndex);
});
记录三个信息:
- dragSourceColumn: 来源列
- dragStartIndex: 从第几张开始拖
- draggingCards: 正在拖的牌列表
sublist(cardIndex)获取从cardIndex到末尾的所有牌。蜘蛛纸牌拖一张牌时,它下面的牌也要跟着一起拖。
有效序列检查
bool _isValidDragSequence(int colIndex, int startIndex) {
List<PlayingCard> col = columns[colIndex];
for (int i = startIndex; i < col.length; i++) {
if (!col[i].isFaceUp) return false;
}
for (int i = startIndex; i < col.length - 1; i++) {
if (col[i].value != col[i + 1].value + 1) return false;
}
return true;
}
两个检查:
全部正面朝上
for (int i = startIndex; i < col.length; i++) {
if (!col[i].isFaceUp) return false;
}
从起始位置到列底,所有牌都必须是正面朝上的。
连续递减
for (int i = startIndex; i < col.length - 1; i++) {
if (col[i].value != col[i + 1].value + 1) return false;
}
相邻两张牌,上面的点数必须比下面的大1。比如8-7-6-5是有效的,8-7-5-4就不行(跳过了6)。
这是蜘蛛纸牌的核心规则:只有连续递减的序列才能一起移动。
拖拽时的视觉反馈
feedback: Material(
color: Colors.transparent,
child: _buildDragFeedback(colIndex, cardIndex, width, height),
),
feedback是拖拽时跟随手指移动的Widget。
_buildDragFeedback
Widget _buildDragFeedback(int colIndex, int cardIndex, double width, double height) {
List<PlayingCard> cards = columns[colIndex].sublist(cardIndex);
double offset = 25.0;
return SizedBox(
width: width,
height: height + (cards.length - 1) * offset,
child: Stack(
children: List.generate(cards.length, (i) {
return Positioned(
top: i * offset,
child: _buildCard(cards[i], width, height, true),
);
}),
),
);
}
拖拽反馈要显示所有被拖的牌,不只是点击的那一张。
用Stack堆叠,每张牌偏移25像素,和原来的布局一样。
注意_buildCard的最后一个参数是true,表示抬起状态,阴影会更明显。
Material包裹
feedback: Material(
color: Colors.transparent,
feedback必须用Material包裹,不然Text等Widget会报错。设置透明背景,不影响牌的显示。
原位置的显示
childWhenDragging: Opacity(opacity: 0.3, child: _buildCard(card, width, height, false)),
拖拽时,原位置显示一个半透明的牌,提示玩家牌是从这里拖出去的。
opacity: 0.3让牌变得很淡,和正在拖的牌形成对比。
DragTarget接收
每一列都是一个DragTarget:
return SizedBox(
width: colWidth,
child: DragTarget<Map<String, int>>(
onWillAcceptWithDetails: (details) {
if (draggingCards.isEmpty) return false;
return _canDropOn(colIndex, draggingCards.first);
},
onWillAcceptWithDetails
onWillAcceptWithDetails: (details) {
if (draggingCards.isEmpty) return false;
return _canDropOn(colIndex, draggingCards.first);
},
当拖拽物悬停在DragTarget上方时调用,返回true表示可以接收,false表示不能。
先检查是否有正在拖的牌,然后调用_canDropOn检查规则。
_canDropOn规则检查
bool _canDropOn(int targetCol, PlayingCard card) {
if (columns[targetCol].isEmpty) return true;
PlayingCard topCard = columns[targetCol].last;
return topCard.isFaceUp && topCard.value == card.value + 1;
}
两种情况可以放:
- 目标列是空的:任何牌都可以放到空列
- 目标列顶部的牌比要放的牌大1:比如顶部是8,可以放7
这就是蜘蛛纸牌的堆叠规则:递减排列。
放下牌
onAcceptWithDetails: (details) => _onDragEnd(colIndex),
当用户松手且onWillAcceptWithDetails返回true时,调用_onDragEnd:
void _onDragEnd(int? targetCol) {
if (dragSourceColumn == null || dragStartIndex == null) return;
if (targetCol != null &&
targetCol != dragSourceColumn &&
draggingCards.isNotEmpty &&
_canDropOn(targetCol, draggingCards.first)) {
条件检查
if (targetCol != null &&
targetCol != dragSourceColumn &&
draggingCards.isNotEmpty &&
_canDropOn(targetCol, draggingCards.first)) {
四个条件:
- targetCol不为空(有目标列)
- 目标列不是来源列(不能放回原处)
- 有正在拖的牌
- 符合放置规则
执行移动
setState(() {
columns[dragSourceColumn!].removeRange(dragStartIndex!, columns[dragSourceColumn!].length);
columns[targetCol].addAll(draggingCards);
moves++;
从来源列移除牌,添加到目标列。移动步数加1。
removeRange移除从startIndex到末尾的所有牌,addAll把它们加到目标列。
翻开新的顶牌
if (columns[dragSourceColumn!].isNotEmpty && !columns[dragSourceColumn!].last.isFaceUp) {
columns[dragSourceColumn!].last.isFaceUp = true;
}
移走牌后,来源列可能露出一张背面的牌。把它翻开。
这是蜘蛛纸牌的规则:列顶的牌总是正面朝上。
检查是否完成一套
_checkColumnForCompleteSuit(targetCol);
});
放下牌后检查目标列是否形成了完整的一套(A到K)。
完整套牌检查
void _checkColumnForCompleteSuit(int col) {
if (columns[col].length < 13) return;
int startIndex = columns[col].length - 13;
bool isComplete = true;
for (int i = 0; i < 13; i++) {
if (!columns[col][startIndex + i].isFaceUp || columns[col][startIndex + i].value != 13 - i) {
isComplete = false;
break;
}
}
长度检查
if (columns[col].length < 13) return;
一套牌有13张(A到K),列里不够13张肯定不完整。
序列检查
for (int i = 0; i < 13; i++) {
if (!columns[col][startIndex + i].isFaceUp || columns[col][startIndex + i].value != 13 - i) {
从列底往上数13张,检查是否是K-Q-J-10-9-8-7-6-5-4-3-2-A。
13 - i:i=0时是13(K),i=12时是1(A)。
收走完整套牌
if (isComplete) {
setState(() {
columns[col].removeRange(startIndex, columns[col].length);
completedSuits++;
if (columns[col].isNotEmpty && !columns[col].last.isFaceUp) {
columns[col].last.isFaceUp = true;
}
if (completedSuits == 8) _showWinDialog();
});
}
完整的一套牌会被移除,completedSuits加1。
8套牌全部完成就赢了。蜘蛛纸牌用8副牌(只用黑桃),所以是8套。
高亮提示
builder: (context, candidateData, rejectedData) {
bool isHighlighted = candidateData.isNotEmpty;
DragTarget的builder有三个参数:
- candidateData: 可以接收的拖拽数据列表
- rejectedData: 不能接收的拖拽数据列表
candidateData.isNotEmpty表示有牌正在往这里拖,而且可以放下。
border: Border.all(color: isHighlighted ? Colors.yellow : Colors.white24, width: isHighlighted ? 2 : 1),
高亮时边框变黄变粗,提示玩家这里可以放。
清理拖拽状态
setState(() {
dragSourceColumn = null;
dragStartIndex = null;
draggingCards = [];
});
无论拖拽成功还是取消,最后都要清理状态。
拖拽取消
onDragEnd: (details) {
if (dragSourceColumn != null) _onDragEnd(null);
},
如果用户把牌拖到无效位置松手,onDragEnd会被调用。
传入null作为targetCol,_onDragEnd里的条件检查会失败,牌不会移动,只是清理状态。
牌会"弹回"原位,因为我们没有真正修改数据。
小结
这篇讲了蜘蛛纸牌的拖拽堆叠,核心知识点:
- LongPressDraggable:长按开始拖拽,避免误触
- DragTarget:接收拖拽物的目标
- data参数:拖拽时携带的数据
- feedback:拖拽时的视觉反馈
- childWhenDragging:原位置的占位显示
- onWillAcceptWithDetails:判断是否可以接收
- onAcceptWithDetails:接收后的处理
- 序列验证:只有连续递减的牌才能一起拖
- 堆叠规则:只能放在比自己大1的牌上面
- 完整套牌检测:K到A连续13张自动收走
拖拽是蜘蛛纸牌最复杂的部分,理解了这些,游戏的核心玩法就实现了。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)