Files
ReelShort/lib/pages/player/widgets/player_loading_overlay.dart

154 lines
4.0 KiB
Dart

// 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();
}