Files
xianyan/lib/shared/widgets/shader_card_background.dart
2026-05-22 02:04:46 +08:00

160 lines
4.8 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.
// ============================================================
// 闲言APP — Shader卡片背景组件
// 创建时间: 2026-05-20
// 更新时间: 2026-05-22
// 作用: Fragment Shader流体渐变背景
// 上次更新: 集成PerformanceOrchestrator帧率节流+前后台暂停Ticker
// ============================================================
import 'dart:ui' as ui;
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:xianyan/core/services/performance/performance_orchestrator.dart';
import 'package:xianyan/core/utils/logger.dart';
/// Shader卡片背景 — Fragment Shader 流体渐变
///
/// ⚠️ 性能警告: 此组件使用 Ticker 持续驱动动画,
/// 不应在列表项中使用会导致N个Ticker同时运行造成卡死
/// 仅适用于单例场景(如首页每日推荐卡片)。
///
/// 性能优化:
/// - 帧率节流: full=60fps, balanced=30fps, saver=20fps
/// - 前后台感知: App后台时自动暂停Ticker
/// - RepaintBoundary: 隔离重绘范围
class ShaderCardBackground extends StatefulWidget {
const ShaderCardBackground({super.key, this.touchOffset});
final Offset? touchOffset;
@override
State<ShaderCardBackground> createState() => _ShaderCardBackgroundState();
}
class _ShaderCardBackgroundState extends State<ShaderCardBackground>
with SingleTickerProviderStateMixin, WidgetsBindingObserver {
late Ticker _ticker;
final ValueNotifier<double> _timeNotifier = ValueNotifier(0);
ui.FragmentProgram? _program;
bool _loaded = false;
bool _isVisible = true; // ignore: prefer_final_fields
late FrameThrottleCallback _shouldRenderFrame;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_shouldRenderFrame = PerformanceOrchestrator.instance.createFrameThrottle();
_ticker = createTicker(_onTick);
_ticker.start();
_loadShader();
PerformanceOrchestrator.instance.onForeground(_resumeTicker);
PerformanceOrchestrator.instance.onBackground(_pauseTicker);
}
Future<void> _loadShader() async {
try {
_program = await ui.FragmentProgram.fromAsset('shaders/fluid.frag');
if (mounted) setState(() => _loaded = true);
} catch (e) {
Log.e('ShaderCardBackground load error', e);
}
}
void _onTick(Duration elapsed) {
if (!_shouldRenderFrame()) return;
_timeNotifier.value = elapsed.inMicroseconds / 1000000.0;
}
void _pauseTicker() {
if (_ticker.isActive) _ticker.stop();
}
void _resumeTicker() {
if (!_ticker.isActive && _isVisible) _ticker.start();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.paused) {
_pauseTicker();
} else if (state == AppLifecycleState.resumed && _isVisible) {
_resumeTicker();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
PerformanceOrchestrator.instance.removeCallbacks(_resumeTicker);
PerformanceOrchestrator.instance.removeCallbacks(_pauseTicker);
_ticker.dispose();
_timeNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_loaded || _program == null) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF6699EE).withValues(alpha: 0.6),
const Color(0xFFB366CC).withValues(alpha: 0.6),
],
),
),
);
}
return RepaintBoundary(
child: CustomPaint(
painter: _ShaderPainter(
program: _program!,
timeNotifier: _timeNotifier,
touch: widget.touchOffset ?? Offset.zero,
),
),
);
}
}
/// Shader画笔 — 监听 ValueNotifier 驱动重绘
///
/// 通过 addListener/removeListener 监听 timeNotifier
/// 仅触发 CustomPaint 重绘,不触发上层 widget rebuild。
class _ShaderPainter extends CustomPainter {
_ShaderPainter({
required this.program,
required this.timeNotifier,
required this.touch,
}) : super(repaint: timeNotifier);
final ui.FragmentProgram program;
final ValueNotifier<double> timeNotifier;
final Offset touch;
@override
void paint(Canvas canvas, Size size) {
final shader = program.fragmentShader();
shader.setFloat(0, timeNotifier.value);
shader.setFloat(1, size.width);
shader.setFloat(2, size.height);
shader.setFloat(3, touch.dx);
shader.setFloat(4, touch.dy);
final paint = Paint()..shader = shader;
canvas.drawRect(Offset.zero & size, paint);
}
@override
bool shouldRepaint(_ShaderPainter oldDelegate) {
return oldDelegate.timeNotifier != timeNotifier ||
oldDelegate.touch != touch;
}
}