rtc_prd/airhub_app/lib/widgets/animated_gradient_background.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

327 lines
10 KiB
Dart
Raw 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 'dart:math' as math;
import 'package:flutter/material.dart';
/// 动态渐变背景 — 精确复刻 HTML styles.css 中的 Animated Gradient Background
/// 5 层渐变 + 5 组不同节奏的有机流动动画
class AnimatedGradientBackground extends StatefulWidget {
const AnimatedGradientBackground({super.key});
@override
State<AnimatedGradientBackground> createState() =>
_AnimatedGradientBackgroundState();
}
class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
with TickerProviderStateMixin {
// 5 个不同速度的动画控制器,对应 HTML 的 organicFlow1~5
late final List<AnimationController> _controllers;
@override
void initState() {
super.initState();
_controllers = [
AnimationController(vsync: this, duration: const Duration(seconds: 20)),
AnimationController(vsync: this, duration: const Duration(seconds: 25)),
AnimationController(vsync: this, duration: const Duration(seconds: 30)),
AnimationController(vsync: this, duration: const Duration(seconds: 35)),
AnimationController(vsync: this, duration: const Duration(seconds: 28)),
];
for (final c in _controllers) {
c.repeat();
}
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: Container(
color: const Color(0xFFFEFEFE),
child: Stack(
children: [
// Layer 1: Soft pink from bottom-left (organicFlow1, 20s)
_buildLayer(
controller: _controllers[0],
gradients: const [
_GradientDef(
center: Alignment(-0.6, 0.6),
radiusX: 1.2,
radiusY: 0.8,
color: Color(0x99FFC8DC), // rgba(255, 200, 220, 0.6)
stops: [0.0, 0.5],
),
_GradientDef(
center: Alignment(-0.4, 0.2),
radiusX: 0.8,
radiusY: 0.6,
color: Color(0x66FFB4C8), // rgba(255, 180, 200, 0.4)
stops: [0.0, 0.4],
),
],
keyframes: const [
_Keyframe(0.00, 0, 0, 0, 1.0),
_Keyframe(0.25, 0.05, -0.08, 2, 1.02),
_Keyframe(0.50, -0.03, 0.05, -1, 0.98),
_Keyframe(0.75, 0.08, 0.03, 1, 1.01),
_Keyframe(1.00, 0, 0, 0, 1.0),
],
),
// Layer 2: Mint/Cyan from top-right (organicFlow2, 25s)
_buildLayer(
controller: _controllers[1],
gradients: const [
_GradientDef(
center: Alignment(0.6, -0.6),
radiusX: 1.0,
radiusY: 0.7,
color: Color(0x80B4F0F0), // rgba(180, 240, 240, 0.5)
stops: [0.0, 0.45],
),
_GradientDef(
center: Alignment(0.4, -0.2),
radiusX: 0.7,
radiusY: 0.9,
color: Color(0x66C8F5F5), // rgba(200, 245, 245, 0.4)
stops: [0.0, 0.4],
),
],
keyframes: const [
_Keyframe(0.00, 0, 0, 0, 1.0),
_Keyframe(0.33, -0.08, 0.06, -2, 1.03),
_Keyframe(0.66, 0.06, -0.05, 2, 0.97),
_Keyframe(1.00, 0, 0, 0, 1.0),
],
),
// Layer 3: Lavender/Purple diagonal (organicFlow3, 30s)
_buildLayer(
controller: _controllers[2],
gradients: const [
_GradientDef(
center: Alignment(0.2, 0.0),
radiusX: 0.9,
radiusY: 0.8,
color: Color(0x73E6D2FA), // rgba(230, 210, 250, 0.45)
stops: [0.0, 0.45],
),
_GradientDef(
center: Alignment(-0.2, 0.4),
radiusX: 0.6,
radiusY: 0.5,
color: Color(0x59F0DCFF), // rgba(240, 220, 255, 0.35)
stops: [0.0, 0.35],
),
],
keyframes: const [
_Keyframe(0.0, 0, 0, 0, 1.0),
_Keyframe(0.2, 0.04, 0.07, 1, 1.02),
_Keyframe(0.4, -0.06, -0.04, -2, 0.98),
_Keyframe(0.6, 0.07, -0.06, 2, 1.01),
_Keyframe(0.8, -0.04, 0.05, -1, 0.99),
_Keyframe(1.0, 0, 0, 0, 1.0),
],
),
// Layer 4 (::before): Warm pink top-left + cool cyan bottom-right (organicFlow4, 35s)
_buildLayer(
controller: _controllers[3],
sizeMultiplier: 1.5,
gradients: const [
_GradientDef(
center: Alignment(-0.7, -0.4),
radiusX: 0.6,
radiusY: 0.4,
color: Color(0x80FFE6F0), // rgba(255, 230, 240, 0.5)
stops: [0.0, 0.4],
),
_GradientDef(
center: Alignment(0.7, 0.2),
radiusX: 0.5,
radiusY: 0.7,
color: Color(0x66DCFAFA), // rgba(220, 250, 250, 0.4)
stops: [0.0, 0.35],
),
],
keyframes: const [
_Keyframe(0.0, 0, 0, 0, 1.0),
_Keyframe(0.25, -0.05, 0.07, -1, 1.01),
_Keyframe(0.50, 0.06, -0.04, 2, 0.99),
_Keyframe(0.75, -0.03, 0.05, -1, 1.02),
_Keyframe(1.0, 0, 0, 0, 1.0),
],
),
// Layer 5 (::after): Pink bottom + Cyan top-right (organicFlow5, 28s)
_buildLayer(
controller: _controllers[4],
sizeMultiplier: 1.2,
gradients: const [
_GradientDef(
center: Alignment(0.0, 0.8),
radiusX: 0.7,
radiusY: 0.5,
color: Color(0x59FFD2E6), // rgba(255, 210, 230, 0.35)
stops: [0.0, 0.4],
),
_GradientDef(
center: Alignment(0.8, -0.8),
radiusX: 0.4,
radiusY: 0.6,
color: Color(0x4DC8F0FF), // rgba(200, 240, 255, 0.3)
stops: [0.0, 0.35],
),
],
keyframes: const [
_Keyframe(0.0, 0, 0, 0, 1.0),
_Keyframe(0.33, 0.04, -0.06, 1, 1.02),
_Keyframe(0.66, -0.06, 0.04, -2, 0.98),
_Keyframe(1.0, 0, 0, 0, 1.0),
],
),
],
),
),
);
}
Widget _buildLayer({
required AnimationController controller,
required List<_GradientDef> gradients,
required List<_Keyframe> keyframes,
double sizeMultiplier = 2.0,
}) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final t = controller.value;
final frame = _interpolateKeyframes(keyframes, t);
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..translate(
frame.tx * MediaQuery.of(context).size.width,
frame.ty * MediaQuery.of(context).size.height,
)
..rotateZ(frame.rotate * math.pi / 180)
..scale(frame.scale),
child: child,
);
},
child: SizedBox.expand(
child: CustomPaint(
painter: _GradientLayerPainter(gradients, sizeMultiplier),
),
),
);
}
_Keyframe _interpolateKeyframes(List<_Keyframe> keyframes, double t) {
// 找到 t 所在的两个 keyframe 之间
int i = 0;
for (i = 0; i < keyframes.length - 1; i++) {
if (t <= keyframes[i + 1].time) break;
}
if (i >= keyframes.length - 1) i = keyframes.length - 2;
final a = keyframes[i];
final b = keyframes[i + 1];
final segT = (b.time - a.time) == 0
? 0.0
: (t - a.time) / (b.time - a.time);
// ease-in-out 缓动
final eased = _easeInOut(segT);
return _Keyframe(
t,
a.tx + (b.tx - a.tx) * eased,
a.ty + (b.ty - a.ty) * eased,
a.rotate + (b.rotate - a.rotate) * eased,
a.scale + (b.scale - a.scale) * eased,
);
}
double _easeInOut(double t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
}
class _Keyframe {
final double time; // 0~1
final double tx; // translateX比例
final double ty; // translateY比例
final double rotate; // degrees
final double scale;
const _Keyframe(this.time, this.tx, this.ty, this.rotate, this.scale);
}
class _GradientDef {
final Alignment center;
final double radiusX;
final double radiusY;
final Color color;
final List<double> stops;
const _GradientDef({
required this.center,
required this.radiusX,
required this.radiusY,
required this.color,
required this.stops,
});
}
class _GradientLayerPainter extends CustomPainter {
final List<_GradientDef> gradients;
final double sizeMultiplier;
_GradientLayerPainter(this.gradients, this.sizeMultiplier);
@override
void paint(Canvas canvas, Size size) {
final w = size.width * sizeMultiplier;
final h = size.height * sizeMultiplier;
final offsetX = (size.width - w) / 2;
final offsetY = (size.height - h) / 2;
for (final g in gradients) {
final cx = offsetX + w * (g.center.x + 1) / 2;
final cy = offsetY + h * (g.center.y + 1) / 2;
final rx = w * g.radiusX / 2;
final ry = h * g.radiusY / 2;
final r = math.max(rx, ry);
final gradient = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: [g.color, g.color.withOpacity(0)],
stops: g.stops,
);
final rect = Rect.fromCenter(
center: Offset(cx, cy),
width: rx * 2,
height: ry * 2,
);
final paint = Paint()
..shader = gradient.createShader(rect)
..blendMode = BlendMode.srcOver;
canvas.drawOval(rect, paint);
}
}
@override
bool shouldRepaint(covariant _GradientLayerPainter oldDelegate) => false;
}