160 lines
4.8 KiB
Dart
160 lines
4.8 KiB
Dart
// ============================================================
|
||
// 闲言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;
|
||
}
|
||
}
|