MoodCanvas/lib/screens/puzzle/drag_puzzle_screen.dart
fengshengxiong 91b7eebbf2 接入TopON
2026-01-22 16:34:55 +08:00

595 lines
18 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:aesthetica_wallpaper/models/drag_puzzle_game.dart';
import 'package:aesthetica_wallpaper/providers/drag_puzzle_provider.dart';
import 'package:aesthetica_wallpaper/screens/puzzle/puzzle_complete_screen.dart';
/// 拖拽式拼图游戏界面
class DragPuzzleScreen extends StatefulWidget {
final String imagePath;
final DragPuzzleDifficulty difficulty;
const DragPuzzleScreen({
super.key,
required this.imagePath,
required this.difficulty,
});
@override
State<DragPuzzleScreen> createState() => _DragPuzzleScreenState();
}
class _DragPuzzleScreenState extends State<DragPuzzleScreen> {
late DragPuzzleProvider _puzzleProvider;
@override
void initState() {
super.initState();
_puzzleProvider = DragPuzzleProvider();
// 创建游戏
WidgetsBinding.instance.addPostFrameCallback((_) {
_puzzleProvider.createDragPuzzle(
imagePath: widget.imagePath,
difficulty: widget.difficulty,
);
});
}
@override
void dispose() {
_puzzleProvider.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: ChangeNotifierProvider.value(
value: _puzzleProvider,
child: Consumer<DragPuzzleProvider>(
builder: (context, provider, child) {
final game = provider.currentGame;
if (game == null) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
'Preparing puzzle...',
style: TextStyle(color: Colors.white),
),
],
),
);
}
// 游戏完成后跳转
if (game.isComplete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PuzzleCompleteScreen(game: game),
),
);
});
}
return Column(
children: [
// 顶部信息栏
_buildTopBar(game),
// 拼图区域(大幅增加)
Expanded(
flex: 6, // 大幅增加拼图区域
child: _buildPuzzleArea(game, provider),
),
// 分隔线
Container(
height: 2,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(horizontal: 16),
),
// 拼图块区域(单行横向排列)
Container(
height: 100, // 固定高度,单行显示
child: _buildPiecesArea(game, provider),
),
// 底部工具栏(简化)
_buildBottomBar(game, provider),
],
);
},
),
),
),
);
}
Widget _buildTopBar(DragPuzzleGame game) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade900,
border: Border(
bottom: BorderSide(color: Colors.grey.shade800, width: 1),
),
),
child: Row(
children: [
// 返回按钮
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
const Spacer(),
// 进度显示
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.blue.withValues(alpha: 0.5)),
),
child: Text(
'${game.placedPieces.length}/${game.pieces.length}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
const SizedBox(width: 8),
// 计时器
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green.withValues(alpha: 0.5)),
),
child: Row(
children: [
const Icon(Icons.timer, size: 16, color: Colors.white),
const SizedBox(width: 4),
Text(
_formatDuration(game.elapsedTime),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 8),
// 移动次数
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.orange.withValues(alpha: 0.5)),
),
child: Row(
children: [
const Icon(Icons.touch_app, size: 16, color: Colors.white),
const SizedBox(width: 4),
Text(
'${game.moves}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
],
),
);
}
Widget _buildPuzzleArea(DragPuzzleGame game, DragPuzzleProvider provider) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(12),
),
child: _buildPuzzleGrid(game, provider), // 直接显示拼图网格
);
}
Widget _buildPuzzleGrid(DragPuzzleGame game, DragPuzzleProvider provider) {
return LayoutBuilder(
builder: (context, constraints) {
// 使用可用的最大空间
final maxWidth = constraints.maxWidth;
final maxHeight = constraints.maxHeight;
// 计算合适的网格大小
// 对于3行2列整体比例应该是 (2/3) * (9/16) = 3/8 = 0.375
// 即宽度是高度的0.375倍
final gridAspectRatio = (game.gridCols / game.gridRows) * (9.0 / 16.0);
double gridWidth, gridHeight;
// 根据可用空间计算网格尺寸
if (maxWidth / maxHeight < gridAspectRatio) {
// 宽度受限
gridWidth = maxWidth;
gridHeight = gridWidth / gridAspectRatio;
} else {
// 高度受限
gridHeight = maxHeight;
gridWidth = gridHeight * gridAspectRatio;
}
// 每个格子的宽高比单个格子是9:16
final cellAspectRatio = 9.0 / 16.0;
return Center(
child: Container(
width: gridWidth,
height: gridHeight,
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.5),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: game.gridCols,
childAspectRatio: cellAspectRatio,
mainAxisSpacing: 1,
crossAxisSpacing: 1,
),
itemCount: game.gridRows * game.gridCols,
itemBuilder: (context, index) {
final row = index ~/ game.gridCols;
final col = index % game.gridCols;
return _buildDropTarget(game, provider, row, col);
},
),
),
),
);
},
);
}
Widget _buildDropTarget(
DragPuzzleGame game,
DragPuzzleProvider provider,
int row,
int col,
) {
final piece = game.getPieceAt(row, col);
final isCorrectPosition = piece?.isCorrectlyPlaced ?? false;
return DragTarget<DragPuzzlePiece>(
onAccept: (draggedPiece) {
provider.placePiece(draggedPiece.id, row, col);
},
builder: (context, candidateData, rejectedData) {
return Container(
margin: const EdgeInsets.all(1),
decoration: BoxDecoration(
color: candidateData.isNotEmpty
? Colors.blue.withValues(alpha: 0.3)
: Colors.grey.shade700,
border: Border.all(
color: isCorrectPosition ? Colors.green : Colors.grey.shade600,
width: isCorrectPosition ? 2 : 1,
),
),
child: piece != null
? GestureDetector(
onTap: () => provider.removePiece(piece.id),
child: Stack(
fit: StackFit.expand,
children: [
Image(image: piece.image, fit: BoxFit.cover),
if (isCorrectPosition)
Positioned(
top: 4,
right: 4,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.white,
size: 12,
),
),
),
],
),
)
: Center(
child: Text(
'${row * game.gridCols + col + 1}',
style: TextStyle(color: Colors.grey.shade500, fontSize: 12),
),
),
);
},
);
}
Widget _buildPiecesArea(DragPuzzleGame game, DragPuzzleProvider provider) {
final unplacedPieces = game.unplacedPieces;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade900,
border: Border(top: BorderSide(color: Colors.grey.shade800, width: 1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pieces (${unplacedPieces.length})',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Expanded(
child: unplacedPieces.isEmpty
? const Center(
child: Text(
'All pieces placed!',
style: TextStyle(color: Colors.grey, fontSize: 14),
),
)
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: unplacedPieces.length,
itemBuilder: (context, index) {
final piece = unplacedPieces[index];
return Container(
width: 60,
height: 60,
margin: const EdgeInsets.only(right: 8),
child: _buildDraggablePiece(piece),
);
},
),
),
],
),
);
}
Widget _buildDraggablePiece(DragPuzzlePiece piece) {
return Draggable<DragPuzzlePiece>(
data: piece,
feedback: Material(
elevation: 8,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(image: piece.image, fit: BoxFit.cover),
),
),
),
childWhenDragging: Container(
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.grey.shade700,
style: BorderStyle.solid,
),
),
child: const Center(
child: Icon(Icons.drag_indicator, color: Colors.grey),
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image(image: piece.image, fit: BoxFit.cover),
),
),
);
}
Widget _buildBottomBar(DragPuzzleGame game, DragPuzzleProvider provider) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade900,
border: Border(top: BorderSide(color: Colors.grey.shade800, width: 1)),
),
child: Row(
children: [
// 重新开始
_buildToolButton(
icon: Icons.refresh,
label: 'Restart',
color: Colors.orange,
onPressed: () => _showRestartDialog(provider),
),
const Spacer(),
// 预览图(右下角)
GestureDetector(
onTap: () => _showPreviewDialog(game),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Stack(
fit: StackFit.expand,
children: [
Image.asset(game.imagePath, fit: BoxFit.cover),
Container(
color: Colors.black.withValues(alpha: 0.3),
child: const Icon(
Icons.search,
color: Colors.white,
size: 24,
),
),
],
),
),
),
),
],
),
);
}
Widget _buildToolButton({
required IconData icon,
required String label,
required Color color,
required VoidCallback onPressed,
}) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: color.withValues(alpha: 0.3),
shape: BoxShape.circle,
border: Border.all(color: color.withValues(alpha: 0.5)),
),
child: IconButton(
icon: Icon(icon, color: Colors.white),
onPressed: onPressed,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(fontSize: 12, color: Colors.grey.shade400),
),
],
);
}
void _showRestartDialog(DragPuzzleProvider provider) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Restart Game'),
content: const Text(
'Are you sure you want to restart? Current progress will be lost.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
provider.restartGame();
Navigator.pop(context);
},
child: const Text('Restart'),
),
],
),
);
}
void _showPreviewDialog(DragPuzzleGame game) {
showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Preview',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Image.asset(game.imagePath, fit: BoxFit.contain, height: 300),
const SizedBox(height: 16),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
),
),
);
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes.toString().padLeft(2, '0');
final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}