Flutter卡牌动画实现全解析(1)(放大上升翻转+三角形布局)
1.给每张牌贴标签
步骤:
- 标签1:记录每张牌是否上升
- 标签2:记录每张牌是否翻转
- 初始化每张牌
注意: 牌是有很多张的,我们不能只用一个变量来记录所有牌,这也是第一种写法错误的原因
//写法1
bool isCardUp = false;
//写法2
List<bool> isCardUp = [];
//写法3
Map<int,bool> isCardUp = {};
选择2还是3,首先要弄懂List和Map的区别
List是按位置顺序找东西,没有名字(列表)
Map是按名字找东西,也就是键值对(映射)
如下
Map<int,bool> isCardUp = {
// 记录“每张牌是否上升”
0: false, // 0号牌:不上升
1: true, // 1号牌:要上升
2: false, // 2号牌:不上升
3: true, // 3号牌:要上升
};
这里我们的核心需求是按照牌的编号记录状态,而不是按顺序记录
比如我们记录22号牌上升,用Map只写isCardUp[22]=true,这一行就行,但是用List,要先给前22号填值,至少写22行
所以选写法3
class _DrawTarotcardState extends State<DrawTarotcard> {
// 【关键1:给每张牌贴2个标签】
// 标签1:记录每张牌“是否上升”(true=上升,false=不上升)
Map<int, bool> isCardUp = {};
// 标签2:记录每张牌“是否翻转”(true=翻转,false=不翻转)
Map<int, bool> isCardTurn = {};
void initState() {
super.initState();//执行父类的基础初始化
// 初始化:给0-9号牌(共10张)都贴“不上升、不翻转”的标签
for (int i = 0; i < 10; i++) {
isCardUp[i] = false;
isCardTurn[i] = false;
}
Widget build(BuildContext context) {
// 这里没有重置逻辑,状态改了就改了
return Scaffold(/* 你的卡牌页面 */);
}
为什么for写在initState里面,不写在build里面?
原因:initState是页面的开机初始化函数,只执行一次,而build()会被flutter频繁调用,例如setState后本来isCardUp[2]=true,但是setState刷新页面后,又调用build(),把isCardUp[2]变成false了
2.点击卡牌要做的事
步骤:
- 先上升
- 后翻转
卡牌是先上升,等上升动画做完以后,再翻转,所以我们的这个函数要加上async,它就是用来标记这个函数有要等待的操作的
async必须和await配对使用
void TapCard(int index) async {
//上升
setState((){
isCardUp[index] = true;
});
//等待300毫秒
await Future.delayed(const Duration(milliseconds:300));
//翻转
setState((){
IsCardTurn[index] = true;
});
}
3.AnimatedContainer实现上升变大(隐式)
变大很简单,但是怎么上升呢?
用margin!margin是组件和父容器/其他组件之间的空隙,通过top:负数,可以实现上升
AnimatedContainer(
duration:Duration(millisecond:300),
margin:EdgeInsets.only(top:isSelected!?-200:0,left:10),
width: isSelected ? 300 : 120,
height: isSelected ? 460 : 240
)
4.AnimatedSwitcher实现翻转(隐式)
AnimatedSwitcher(
duration: const Duration(milliseconds: 500), // 翻转时长
// 翻转效果(固定写法,不用懂,直接用)
transitionBuilder: (child, anim) {
return Transform.rotate(angle: anim.value * 3.14, child: child);
},
// 核心:根据“翻转标签”换图片/颜色
child: isCardTurn[i]!
? Container(key: ValueKey('front'), ...) // 正面
: Container(key: ValueKey('back'), ...), // 背面
)
Plus:抽出的三张牌牌按三角形性质固定
步骤
-
- 定义三角形的位置+把抽到的牌数和位置对应
具体也就是要实现第一张放顶部,第二张放左下,第三张放右下–>也就是说我们要给每张牌绑定抽取序号
例如selected = [3,7,12]
通过selected.asMap()
变成{0:3,1:7, 2:12}
selected.asMap()的作用是把只有值的列表(List),变成带有序号的"键(Key)值(Value)对集合"(Map)
现在我们得到的是Map()这个整体,但是我们要实现把这三张牌和三个位置分别绑定,如何拿到单个的牌呢?
通过Map的方法extries,extries可以把整体拆分成独立的键值对对象,然后我们再用一个map(),这个是遍历方法,可以逐个处理上面单独的Entry对象,类似for
selected.asMap().extries.map((entry){}).toList()l
//得到三个Entry对象
//Entry(key:0,value:3)
// Entry(key:1, value:7)
// Entry(key:2, value:12)
如果上面的这个代码被嵌套在Stack的children里面,结尾要带上**.toList()**,children里面只能是列表,而用了map()以后,处理完就是一个对象,不是列表了,使用加.toList()把结果变成列表
Widget buildSimpleTriangleCards() {
// 直接定义3张牌的固定位置(单位:像素),想改位置只动这里!
// key=选中顺序(0/1/2),value=[left, top] (left=距离屏幕左,top=距离屏幕顶)
final trianglePos = {
0: [180.0, 200.0], // 第1张:顶部居中(手机屏幕宽约390,180=中间偏左,200=顶部往下200)
1: [80.0, 350.0], // 第2张:左下(左80,顶350)
2: [280.0, 350.0] // 第3张:右下(左280,顶350)
};
return Stack(
children: selected.asMap().entries.map((entry) {
int selectOrder = entry.key; // 选中顺序:0=第1张、1=第2张、2=第3张
int cardIndex = entry.value; // 抽中的牌索引
// 取当前牌的固定坐标
double left = trianglePos[selectOrder]![0];
double top = trianglePos[selectOrder]![1];
// 固定位置渲染牌
return Positioned(
left: left,
top: top,
child: Image.asset(
BackImage[cardIndex]!,
width: 120, // 牌的宽度固定
height: 200, // 牌的高度固定
fit: BoxFit.cover,
),
);
}).toList(),
);
}
-
- 满足条件就执行三角布局(条件渲染)
// Stack的children里
if (selected.isNotEmpty) buildSimpleTriangleCards(),
更多推荐


所有评论(0)