feat(ui): 更新底栏图标与播放加载视觉
This commit is contained in:
1 parent
8ec6df2be5
commit
12b1a11bd3
11 files changed
+584
-70
No files matched your search
@@ -3,8 +3,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.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,
|
||||
@@ -19,27 +30,124 @@ class PlayerLoadingOverlay extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: opacity,
|
||||
duration: const Duration(milliseconds: 140),
|
||||
duration: _fadeDuration,
|
||||
curve: Curves.easeOut,
|
||||
onEnd: onFadeEnd,
|
||||
child: Container(
|
||||
key: const Key('player-loading-surface'),
|
||||
width: AppDimens.refreshIndicatorSize,
|
||||
height: AppDimens.refreshIndicatorSize,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.refreshIndicatorSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.refreshIndicatorRadius),
|
||||
),
|
||||
width: _surfaceSize.width,
|
||||
height: _surfaceSize.height,
|
||||
alignment: Alignment.center,
|
||||
child: const SizedBox.square(
|
||||
dimension: AppDimens.refreshSpinnerSize,
|
||||
child: CircularProgressIndicator(
|
||||
key: Key('player-entry-loading'),
|
||||
color: Colors.white,
|
||||
strokeWidth: AppDimens.refreshSpinnerStroke,
|
||||
),
|
||||
),
|
||||
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();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/data/models/content_category.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../core/widgets/animated_nav_label.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'search_gift_glyphs.dart';
|
||||
@@ -11,7 +12,7 @@ import 'search_gift_glyphs.dart';
|
||||
const double _navHeight = 56;
|
||||
const double _tabGap = 20;
|
||||
const double _tabFont = 17;
|
||||
const double _searchHitSize = 40;
|
||||
const double _searchHitSize = AppDimens.tapTarget;
|
||||
const double _searchIconSize = 26;
|
||||
const double _giftIconSize = 26;
|
||||
const double _underlineWidth = 26;
|
||||
|
||||
@@ -39,7 +39,7 @@ class RecommendTopNav extends StatelessWidget {
|
||||
/// 首页横向页面的连续位置,用于文字和下划线跟手过渡。
|
||||
final double? pagePosition;
|
||||
|
||||
/// 右上角金色礼物盒(钱包入口);为空时不显示。
|
||||
/// 右上角礼包入口;为空时不显示。
|
||||
final VoidCallback? onGift;
|
||||
|
||||
@override
|
||||
|
||||
@@ -50,8 +50,12 @@ class _SearchPainter extends CustomPainter {
|
||||
final center = Offset(circleDiameter / 2, circleDiameter / 2);
|
||||
canvas.drawCircle(center, circleDiameter / 2 - stroke / 2, paint);
|
||||
|
||||
final start = center + Offset.fromDirection(direction, circleDiameter / 2 - stroke * 0.2);
|
||||
final end = center + Offset.fromDirection(direction, circleDiameter / 2 + size.width * 0.12);
|
||||
final start =
|
||||
center +
|
||||
Offset.fromDirection(direction, circleDiameter / 2 - stroke * 0.2);
|
||||
final end =
|
||||
center +
|
||||
Offset.fromDirection(direction, circleDiameter / 2 + size.width * 0.12);
|
||||
canvas.drawLine(start, end, paint);
|
||||
}
|
||||
|
||||
@@ -60,7 +64,7 @@ class _SearchPainter extends CustomPainter {
|
||||
oldDelegate.color != color;
|
||||
}
|
||||
|
||||
/// emoji 礼盒(红盒金丝带,随系统 emoji 风格渲染)。
|
||||
/// YMB 品牌礼盒静态图标,使用玫红礼盒与金色丝带。
|
||||
class GiftGlyph extends StatelessWidget {
|
||||
const GiftGlyph({super.key, required this.size});
|
||||
|
||||
@@ -68,11 +72,94 @@ class GiftGlyph extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: FittedBox(
|
||||
child: Text('🎁', style: TextStyle(fontSize: size, height: 1)),
|
||||
),
|
||||
);
|
||||
return CustomPaint(size: Size.square(size), painter: _GiftPainter());
|
||||
}
|
||||
}
|
||||
|
||||
class _GiftPainter extends CustomPainter {
|
||||
const _GiftPainter();
|
||||
|
||||
static const double _designSize = 26;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.save();
|
||||
canvas.scale(size.width / _designSize);
|
||||
_paintBox(canvas);
|
||||
_paintRibbon(canvas);
|
||||
_paintBow(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void _paintBox(Canvas canvas) {
|
||||
final body = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(4, 13, 22, 23),
|
||||
const Radius.circular(2),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
body,
|
||||
Paint()
|
||||
..shader = const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFFFF5C82), Color(0xFFEF315D)],
|
||||
).createShader(const Rect.fromLTRB(4, 13, 22, 23)),
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(3, 10, 23, 14),
|
||||
const Radius.circular(1.5),
|
||||
),
|
||||
Paint()
|
||||
..shader = const LinearGradient(
|
||||
colors: [Color(0xFFFF7190), Color(0xFFF52E5C)],
|
||||
).createShader(const Rect.fromLTRB(3, 10, 23, 14)),
|
||||
);
|
||||
}
|
||||
|
||||
void _paintRibbon(Canvas canvas) {
|
||||
final ribbon = Paint()
|
||||
..shader = const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFFFFA8), Color(0xFFFFD52F), Color(0xFFFFB900)],
|
||||
).createShader(const Rect.fromLTRB(10.5, 10, 15.5, 23));
|
||||
canvas.drawRect(const Rect.fromLTRB(10.5, 13, 15.5, 23), ribbon);
|
||||
canvas.drawRect(const Rect.fromLTRB(3, 10.5, 23, 14), ribbon);
|
||||
canvas.drawLine(
|
||||
const Offset(11, 14),
|
||||
const Offset(11, 22),
|
||||
Paint()
|
||||
..color = const Color(0xFFFFFFAE)
|
||||
..strokeWidth = 0.6,
|
||||
);
|
||||
}
|
||||
|
||||
void _paintBow(Canvas canvas) {
|
||||
final gold = Paint()
|
||||
..shader = const LinearGradient(
|
||||
colors: [Color(0xFFFFFFA8), Color(0xFFFFD52F), Color(0xFFFFB900)],
|
||||
).createShader(const Rect.fromLTRB(5, 1, 21, 12));
|
||||
final tails = Path()
|
||||
..moveTo(11.8, 7)
|
||||
..lineTo(15, 7)
|
||||
..lineTo(17, 13)
|
||||
..lineTo(14.1, 12)
|
||||
..lineTo(12.6, 14)
|
||||
..lineTo(10.8, 8)
|
||||
..close();
|
||||
canvas.drawPath(tails, gold);
|
||||
final bow = Path()
|
||||
..moveTo(13, 7)
|
||||
..cubicTo(5.2, 0, 5.2, 4.8, 7.5, 6.7)
|
||||
..cubicTo(9, 8, 11.5, 8, 13, 7)
|
||||
..cubicTo(14.5, 8, 17, 8, 18.5, 6.7)
|
||||
..cubicTo(20.8, 4.8, 20.8, 0, 13, 7)
|
||||
..close();
|
||||
canvas.drawPath(bow, gold);
|
||||
canvas.drawCircle(const Offset(13, 7), 1.55, gold);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _GiftPainter oldDelegate) => false;
|
||||
}
|
||||
Reference in new issue
Block a user