✨ 我是 Muzi 的「文章捕手」,擅长在文字的星海中打捞精华。每当新的篇章诞生,我就会像整理贝壳一样,将思想的闪光点串成珍珠项链~
本文详细介绍了宠物养成应用第二天的开发进展,重点实现了核心养成功能和沉浸式手势交互。技术创新包括基于go_router的路由守卫实现未登录重定向,利用ShellRoute构建底部导航栏,以及设计宠物状态随时间衰减的机制。交互方式由传统按钮升级为结合GestureDetector和DragTarget的手势操作,支持点击、长按、双击和拖拽,实现抚摸、休息、玩耍、喂食和清洁等自然交互体验。背包系统支持道具拖拽,配合动画反馈提升用户体验。文章还分享了Firestore序列化兼容性处理和手势与拖拽冲突的解决方案,具备较强的实用价值和开发参考意义。
# 前言
今天是开发的第二天,效率爆棚!不仅完成了核心养成功能,还把交互方式从传统按钮升级为沉浸式手势交互。用户现在可以通过点击、长按、双击、拖拽等方式与宠物互动,体验更加自然。
# 上午:核心功能开发
# 1. 路由守卫实现
使用 go_router 的 redirect 回调实现认证保护:
GoRouter(
refreshListenable: GoRouterRefreshStream(authStateStream),
redirect: (context, state) {
final isLoggedIn = authState.valueOrNull != null;
final isAuthPage = ['/login', '/register', '/'].contains(state.uri.path);
if (!isLoggedIn && !isAuthPage) return '/login';
if (isLoggedIn && isAuthPage) return '/pet-room';
return null;
},
// ...
);
这样未登录用户访问任何受保护页面都会被重定向到登录页。
# 2. 底部导航栏
使用 ShellRoute 实现导航容器:
ShellRoute(
builder: (context, state, child) => MainShellPage(child: child),
routes: [
GoRoute(path: '/pet-room', builder: ...),
GoRoute(path: '/community', builder: ...),
GoRoute(path: '/profile', builder: ...),
],
)
三个 Tab:首页(宠物房间)、社区、我的。
# 3. 状态衰减机制
这是养成游戏的核心——宠物状态会随时间衰减:
class StatusDecayCalculator {
// 每小时衰减速率
static const Map<String, double> decayRates = {
'happiness': 2.0, // 心情
'hunger': 3.0, // 饱腹度
'energy': 1.5, // 精力
'cleanliness': 2.0, // 清洁度
'health': 0.5, // 健康
};
static PetStatus calculateDecay(PetModel pet) {
final hoursPassed = DateTime.now()
.difference(pet.lastInteractionAt).inHours
.clamp(0, 24); // 最多计算24小时
// 纪念模式不衰减
if (pet.isMemorial) return pet.status;
return PetStatus(
happiness: (pet.status.happiness - hoursPassed * 2).clamp(0, 100),
hunger: (pet.status.hunger - hoursPassed * 3).clamp(0, 100),
// ...
);
}
}
# 4. Firestore 序列化踩坑
freezed 生成的 toJson() 和 Firestore 不太兼容,花了不少时间调试:
问题:嵌套对象序列化失败、DateTime 格式不对、枚举值不匹配
解决方案:手动构建 Firestore 数据结构
Map<String, dynamic> _petToFirestore(PetModel pet) {
return {
'id': pet.id,
'name': pet.name,
'species': pet.species.name, // 枚举转字符串
'status': {
'happiness': pet.status.happiness,
'hunger': pet.status.hunger,
// ...
},
'createdAt': Timestamp.fromDate(pet.createdAt), // DateTime 转 Timestamp
// ...
};
}
# 下午:沉浸式交互重构
这是今天最兴奋的部分!把无聊的按钮交互改成手势交互。
# 交互设计
| 交互 | 手势 | 效果 |
|---|---|---|
| 抚摸 | 单击宠物 | 心情+15, 亲密度+10 |
| 休息 | 长按宠物 2秒 | 精力+30 |
| 玩耍 | 双击宠物 | 心情+20, 精力-10 |
| 喂食 | 拖拽食物到宠物 | 饱腹度+N |
| 清洁 | 拖拽毛巾到宠物 | 清洁度+N |
# InteractivePetWidget 实现
核心是组合使用 GestureDetector 和 DragTarget:
class InteractivePetWidget extends StatefulWidget {
final PetModel pet;
final VoidCallback? onPet;
final VoidCallback? onRest;
final VoidCallback? onPlay;
final void Function(ItemModel)? onItemDropped;
// ...
}
class _InteractivePetWidgetState extends State<InteractivePetWidget> {
@override
Widget build(BuildContext context) {
return DragTarget<ItemModel>(
onAcceptWithDetails: (details) => _handleItemDropped(details.data),
builder: (context, candidateData, rejectedData) {
return GestureDetector(
onTap: _handleTap, // 抚摸
onLongPress: _handleLongPress, // 休息
onDoubleTap: _handleDoubleTap, // 玩耍
child: _buildPetAvatar(),
);
},
);
}
void _handleTap() {
HapticFeedback.lightImpact(); // 触感反馈
_playFeedbackAnimation(); // 视觉反馈
widget.onPet?.call();
}
}
# 背包系统
实现了可拖拽的道具卡片:
class DraggableItemCard extends StatelessWidget {
final ItemModel item;
@override
Widget build(BuildContext context) {
return Draggable<ItemModel>(
data: item,
onDragStarted: () => HapticFeedback.selectionClick(),
feedback: _buildCard(isDragging: true), // 拖拽时的样式
childWhenDragging: Opacity(opacity: 0.3, child: _buildCard()),
child: _buildCard(),
);
}
}
# 动画反馈
添加了缩放和弹跳动画,让交互更有"手感":
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _bounceAnimation;
void _playFeedbackAnimation() {
_animationController.forward().then((_) {
_animationController.reverse();
});
}
// 在 build 中使用
Transform.translate(
offset: Offset(0, -_bounceAnimation.value),
child: Transform.scale(
scale: _scaleAnimation.value,
child: petAvatar,
),
)
# 今日成果
# 功能完成
- ✅ 路由守卫(未登录重定向)
- ✅ 底部导航栏(ShellRoute)
- ✅ 个人中心页面 + 设置页面
- ✅ 清洁互动功能
- ✅ 状态衰减机制
- ✅ 沉浸式手势交互
- 点击抚摸
- 长按休息
- 双击玩耍
- 拖拽喂食/清洁
- ✅ 背包系统(悬浮按钮 + 弹窗 + 可拖拽道具)
# 新增文件
lib/
├── core/utils/
│ └── status_decay_calculator.dart # 状态衰减计算
├── providers/
│ └── inventory_provider.dart # 背包状态管理
├── presentation/
│ ├── pages/
│ │ ├── main/main_shell_page.dart # 导航容器
│ │ ├── community/community_page.dart
│ │ └── profile/
│ │ ├── profile_page.dart
│ │ └── settings_page.dart
│ └── widgets/
│ ├── pet/
│ │ └── interactive_pet_widget.dart # 可交互宠物
│ └── inventory/
│ ├── inventory_fab.dart # 背包按钮
│ ├── inventory_popup.dart # 道具弹窗
│ └── draggable_item_card.dart # 可拖拽道具
# 效果展示
交互流程:
- 点击右下角背包按钮 → 弹出道具面板
- 拖拽"小鱼干"到宠物身上
- 宠物区域高亮 + 提示"松开使用"
- 松手 → 宠物播放进食动画 + 显示"饱腹度 +20"
- 道具数量 -1
手势交互让整个体验更像是在"撸猫",而不是在点按钮!
# 遇到的问题
# 1. DragTarget 和 GestureDetector 冲突
一开始把 GestureDetector 放在 DragTarget 外面,导致拖拽无法触发。
解决:DragTarget 在外,GestureDetector 在内。
# 2. 动态类型问题
Firestore 返回的数据是 dynamic,直接用会报类型错误。
解决:显式类型转换
final int coins = (user?.coins as int?) ?? 0;
# 3. withOpacity 废弃警告
Flutter 新版本废弃了 Color.withOpacity(),建议用 withValues()。暂时忽略,不影响功能。
# 明日计划
# 心得体会
今天的开发体验非常棒!手势交互的实现比想象中简单,Flutter 的 GestureDetector 和 Draggable 组合起来非常强大。
最满意的是拖拽喂食的交互——从背包拖出食物,悬停在宠物上看到高亮反馈,松手后看到宠物"吃东西"的动画。这种交互比点按钮有趣多了!
下一步计划学习 Rive 动画,把静态图标替换成真正的动画。想象一下,点击宠物时它会眯起眼睛享受抚摸,长按时会慢慢躺下睡觉... 期待!
# 项目进度
| 模块 | 进度 | 说明 |
|---|---|---|
| 认证系统 | 100% | 邮箱 + Google 登录 |
| 宠物创建 | 100% | 预设形象 |
| 核心养成 | 90% | 待:状态持久化 |
| 背包系统 | 70% | 待:Firestore 存储 |
| 商店系统 | 0% | 待开发 |
| AI 生成 | 0% | 待开发 |
| 社区功能 | 0% | 待开发 |
项目地址:Cat Club(待开源)