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

604 lines
18 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
// 注意:运行前需要在 pubspec.yaml 添加 image_gallery_saver 和 permission_handler
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../core/app_ads_tools.dart';
/// 粒子效果类型枚举
enum EffectType {
fireflies, // 萤火虫/漂浮粒子
geometric, // 几何连线
snow, // 飘雪
galaxy, // 旋转星系
}
class ParticleHomePage extends StatefulWidget {
const ParticleHomePage({super.key});
@override
State<ParticleHomePage> createState() => _ParticleHomePageState();
}
class _ParticleHomePageState extends State<ParticleHomePage>
with SingleTickerProviderStateMixin {
// 用于动画循环的控制器
late AnimationController _controller;
// 粒子列表
final List<Particle> _particles = [];
// 随机数生成器
final Random _random = Random();
// 当前选择的效果
EffectType _currentEffect = EffectType.fireflies;
// 用于截屏的 Key
final GlobalKey _repaintKey = GlobalKey();
// 屏幕尺寸缓存
Size _screenSize = Size.zero;
@override
void initState() {
super.initState();
// 初始化动画控制器,无限循环
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(); // 驱动界面刷新
// 监听动画帧,更新粒子状态
_controller.addListener(_updateParticles);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
/// 初始化或重置粒子系统
void _initParticles(Size size) {
_particles.clear();
int count = 0;
// 根据不同效果设置粒子数量
switch (_currentEffect) {
case EffectType.fireflies:
count = 100;
break;
case EffectType.geometric:
count = 60;
break;
case EffectType.snow:
count = 150;
break;
case EffectType.galaxy:
count = 200;
break;
}
for (int i = 0; i < count; i++) {
_particles.add(Particle.random(size, _random, _currentEffect));
}
}
/// 每一帧更新粒子位置
void _updateParticles() {
if (_screenSize == Size.zero) return;
for (var particle in _particles) {
particle.update(_screenSize, _currentEffect);
}
// 触发重绘
setState(() {});
}
/// 切换效果
void _switchEffect() {
setState(() {
int nextIndex = (_currentEffect.index + 1) % EffectType.values.length;
_currentEffect = EffectType.values[nextIndex];
_initParticles(_screenSize);
});
}
/// 保存截图到相册
Future<void> _saveToGallery() async {
try {
// 1. 请求存储权限
var status = await Permission.storage.request();
// Android 13+ 可能需要 photos 权限,这里做简单处理,实际需更严谨判断
if (status.isDenied) {
status = await Permission.photos.request();
}
if (status.isGranted ||
await Permission.storage.isGranted ||
await Permission.photos.isGranted) {
// 2. 获取 RenderRepaintBoundary
RenderRepaintBoundary? boundary =
_repaintKey.currentContext?.findRenderObject()
as RenderRepaintBoundary?;
if (boundary == null) return;
// 3. 转换为图片数据 (高像素密度,保证壁纸清晰度)
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData = await image.toByteData(
format: ui.ImageByteFormat.png,
);
if (byteData != null) {
final result = await ImageGallerySaver.saveImage(
byteData.buffer.asUint8List(),
quality: 100,
name: "particle_wallpaper_${DateTime.now().millisecondsSinceEpoch}",
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
result['isSuccess'] ? 'Saved to Gallery!' : 'Failed to save.',
),
backgroundColor: result['isSuccess']
? Colors.green
: Colors.red,
),
);
}
}
} else {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Permission denied.')));
}
}
} catch (e) {
debugPrint(e.toString());
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
}
}
}
@override
Widget build(BuildContext context) {
// 获取屏幕尺寸并在第一次时初始化粒子
final size = MediaQuery.of(context).size;
if (_screenSize != size) {
_screenSize = size;
_initParticles(size);
}
return Scaffold(
body: Stack(
children: [
// 1. 绘图层 (被 RepaintBoundary 包裹以用于截图)
RepaintBoundary(
key: _repaintKey,
child: Container(
color: Colors.black, // 背景色
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: ParticlePainter(
particles: _particles,
effect: _currentEffect,
),
),
),
),
// 2. 左上角返回按钮
Positioned(
top: MediaQuery.of(context).padding.top + 8,
left: 8,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.purple.withValues(alpha: 0.3),
Colors.blue.withValues(alpha: 0.3),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.purple.withValues(alpha: 0.3),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context),
tooltip: 'Back',
),
),
),
// 顶部标题卡片
Positioned(
top: MediaQuery.of(context).padding.top + 16,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.purple.withValues(alpha: 0.3),
Colors.blue.withValues(alpha: 0.3),
],
),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.purple.withValues(alpha: 0.2),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.auto_awesome,
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
const Text(
'AI Generator',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
),
),
// 3. UI 控制层 (半透明,不影响视觉)
Positioned(
bottom: 40,
left: 16,
right: 16,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 效果选择器
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.purple.withValues(alpha: 0.3),
Colors.blue.withValues(alpha: 0.3),
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.purple.withValues(alpha: 0.3),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: Column(
children: [
Row(
children: [
const Icon(
Icons.palette,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'Effect: ${_currentEffect.name.toUpperCase()}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildActionButton(
icon: Icons.refresh,
label: 'Switch Effect',
onTap: _switchEffect,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildActionButton(
icon: Icons.download,
label: 'Save',
onTap: ()async{
final bool adShown = await AppAdsTools.instance.showAd(
AdPlacement.interstitial3,
onAdClosed: () {
_saveToGallery();
},
);
if (!adShown) {
_saveToGallery();
}
},
isPrimary: true,
),
),
],
),
],
),
),
],
),
),
],
),
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool isPrimary = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
gradient: isPrimary
? LinearGradient(
colors: [Colors.purple.shade400, Colors.blue.shade400],
)
: null,
color: isPrimary ? null : Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isPrimary
? Colors.white.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.2),
width: 1,
),
boxShadow: isPrimary
? [
BoxShadow(
color: Colors.purple.withValues(alpha: 0.4),
blurRadius: 12,
spreadRadius: 1,
),
]
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: 20),
const SizedBox(width: 6),
Flexible(
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 13,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}
/// 粒子数据模型
class Particle {
double x;
double y;
double vx; // X轴速度
double vy; // Y轴速度
double size;
Color color;
double life; // 生命周期 (0.0 - 1.0)
double angle; // 用于旋转效果
Particle({
required this.x,
required this.y,
required this.vx,
required this.vy,
required this.size,
required this.color,
this.life = 1.0,
this.angle = 0.0,
});
/// 生成随机粒子
factory Particle.random(Size screenSize, Random random, EffectType type) {
Color randomColor() {
final colors = [
Colors.cyanAccent,
Colors.purpleAccent,
Colors.blueAccent,
Colors.pinkAccent,
Colors.tealAccent,
];
return colors[random.nextInt(colors.length)];
}
double x = random.nextDouble() * screenSize.width;
double y = random.nextDouble() * screenSize.height;
double vx = (random.nextDouble() - 0.5) * 2;
double vy = (random.nextDouble() - 0.5) * 2;
double size = random.nextDouble() * 3 + 1;
Color color = randomColor().withValues(
alpha: random.nextDouble() * 0.5 + 0.3,
);
if (type == EffectType.snow) {
vy = random.nextDouble() * 2 + 1; // 向下落
vx = (random.nextDouble() - 0.5) * 0.5; // 轻微左右飘
color = Colors.white.withValues(alpha: random.nextDouble() * 0.8 + 0.2);
} else if (type == EffectType.galaxy) {
// 这里的 x, y 会在 update 中根据中心点重新计算,只需初始化角度
x = screenSize.width / 2;
y = screenSize.height / 2;
}
return Particle(
x: x,
y: y,
vx: vx,
vy: vy,
size: size,
color: color,
angle: random.nextDouble() * pi * 2,
life: random.nextDouble(),
);
}
/// 更新粒子位置和状态
void update(Size size, EffectType type) {
if (type == EffectType.galaxy) {
// 星系模式:围绕中心旋转
double centerX = size.width / 2;
double centerY = size.height / 2;
angle += 0.01 * (vx.sign == 0 ? 1 : vx.sign); // 旋转速度
double radius = this.size * 30 + (life * 150); // 半径
x = centerX + cos(angle) * radius;
y = centerY + sin(angle) * radius;
return;
}
x += vx;
y += vy;
// 边界检测:超出屏幕则反弹或重置
if (type == EffectType.snow) {
if (y > size.height) {
y = -10;
x = Random().nextDouble() * size.width;
}
} else {
if (x < 0 || x > size.width) vx = -vx;
if (y < 0 || y > size.height) vy = -vy;
}
}
}
/// 核心绘制逻辑
class ParticlePainter extends CustomPainter {
final List<Particle> particles;
final EffectType effect;
ParticlePainter({required this.particles, required this.effect});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..strokeCap = StrokeCap.round;
for (var i = 0; i < particles.length; i++) {
var p = particles[i];
// 1. 绘制粒子本体
paint.color = p.color;
paint.strokeWidth = p.size;
// 不同的绘制形状
if (effect == EffectType.snow) {
// 雪花带一点模糊
paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2);
canvas.drawCircle(Offset(p.x, p.y), p.size, paint);
} else {
canvas.drawCircle(Offset(p.x, p.y), p.size / 2, paint);
}
// 2. 几何模式下的连线效果
if (effect == EffectType.geometric) {
_drawConnections(canvas, p, i, size);
}
}
}
/// 绘制连线:如果两个粒子距离够近,就画一条线
void _drawConnections(
Canvas canvas,
Particle p1,
int currentIndex,
Size size,
) {
Paint linePaint = Paint()..strokeWidth = 1.0;
double connectDistance = 100.0; // 连线阈值
for (var j = currentIndex + 1; j < particles.length; j++) {
var p2 = particles[j];
double dx = p1.x - p2.x;
double dy = p1.y - p2.y;
double dist = sqrt(dx * dx + dy * dy);
if (dist < connectDistance) {
// 距离越近,线条越不透明
double opacity = 1.0 - (dist / connectDistance);
linePaint.color = Colors.cyanAccent.withValues(alpha: opacity * 0.5);
canvas.drawLine(Offset(p1.x, p1.y), Offset(p2.x, p2.y), linePaint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // 总是重绘以实现动画
}
}