// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/theme/app_colors.dart'; const _fadeDuration = Duration(milliseconds: 140); const _motionDuration = Duration(milliseconds: 1350); const _surfaceSize = Size(56, 36); const _logoSize = Size(48, 29); const _minimumScale = 0.98; const _staticProgress = 0.5; const _waveMultiplier = 2.0; const _sweepStart = -1.2; const _sweepTravel = 3.4; const _sweepFeather = 0.16; /// 剧集播放页首帧出现前的非阻塞 YMB 品牌加载层。 class PlayerLoadingOverlay extends StatelessWidget { const PlayerLoadingOverlay({ super.key, required this.opacity, required this.onFadeEnd, }); final double opacity; final VoidCallback onFadeEnd; @override Widget build(BuildContext context) { return AnimatedOpacity( opacity: opacity, duration: _fadeDuration, curve: Curves.easeOut, onEnd: onFadeEnd, child: Container( key: const Key('player-loading-surface'), width: _surfaceSize.width, height: _surfaceSize.height, alignment: Alignment.center, child: const _YmbLoadingMark(), ), ); } } class _YmbLoadingMark extends StatefulWidget { const _YmbLoadingMark(); @override State<_YmbLoadingMark> createState() => _YmbLoadingMarkState(); } class _YmbLoadingMarkState extends State<_YmbLoadingMark> with SingleTickerProviderStateMixin, WidgetsBindingObserver { late final AnimationController _controller; bool _disableAnimations = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _controller = AnimationController(vsync: this, duration: _motionDuration); } @override void didChangeDependencies() { super.didChangeDependencies(); final disableAnimations = MediaQuery.disableAnimationsOf(context); if (_disableAnimations == disableAnimations && _controller.isAnimating) { return; } _disableAnimations = disableAnimations; _syncMotion(WidgetsBinding.instance.lifecycleState); } @override void didChangeAppLifecycleState(AppLifecycleState state) { _syncMotion(state); } void _syncMotion(AppLifecycleState? state) { if (_disableAnimations) { _controller ..stop() ..value = _staticProgress; return; } if (state == null || state == AppLifecycleState.resumed) { if (!_controller.isAnimating) { _controller.repeat(); } return; } _controller.stop(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { final progress = _controller.value; return _buildLogo(progress); }, ); } Widget _buildLogo(double progress) { final pulse = Curves.easeInOut.transform(_triangleWave(progress)); final scale = _minimumScale + (1 - _minimumScale) * pulse; return Transform.scale( key: const Key('player-entry-loading-motion'), scale: scale, child: ShaderMask( blendMode: BlendMode.srcATop, shaderCallback: (bounds) => _logoGradient(progress).createShader(bounds), child: Image.asset( 'assets/icon/splash_logo.png', key: const Key('player-entry-loading'), width: _logoSize.width, height: _logoSize.height, fit: BoxFit.contain, ), ), ); } LinearGradient _logoGradient(double progress) { final center = _sweepStart + progress * _sweepTravel; return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: const [ Colors.white, Colors.white, AppColors.brandRed, Colors.white, ], stops: [0, (center - _sweepFeather).clamp(0, 1), center.clamp(0, 1), 1], ); } double _triangleWave(double value) => 1 - (value * _waveMultiplier - 1).abs(); }