Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c03ee245 | ||
|
|
a3ac007759 | ||
|
|
525f58c150 | ||
|
|
0a02620564 | ||
|
|
78a33cd4f1 | ||
|
|
e8610b7aed |
No files matched your search
@@ -1,5 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'dev_config.dart';
|
||||
import 'prod_config.dart';
|
||||
|
||||
@@ -9,12 +11,10 @@ import 'prod_config.dart';
|
||||
/// - `dev_config.dart`:[DevConfig](本机联调)
|
||||
/// - `prod_config.dart`:[ProdConfig](生产)
|
||||
///
|
||||
/// 切换环境只改下面这一个布尔值,不用传任何构建参数、也不做网络探测。
|
||||
/// Debug 使用开发环境,Release 自动使用生产环境,不做网络探测。
|
||||
abstract final class AppConfig {
|
||||
/// **环境开关**:true 用开发环境,false 用生产环境。
|
||||
///
|
||||
/// 打生产包前必须改成 false;release 构建若仍为 true 会在启动时直接报错。
|
||||
static const bool useDevConfig = true;
|
||||
/// **环境开关**:Debug 使用开发环境,Release 使用生产环境。
|
||||
static const bool useDevConfig = !kReleaseMode;
|
||||
|
||||
/// 当前选中的环境地址。
|
||||
static String get baseUrl =>
|
||||
|
||||
@@ -18,7 +18,6 @@ class AppBottomNavItem {
|
||||
this.selected = false,
|
||||
this.assetPath,
|
||||
this.selectedAssetPath,
|
||||
this.customIcon,
|
||||
});
|
||||
|
||||
final Key tabKey;
|
||||
@@ -26,7 +25,6 @@ class AppBottomNavItem {
|
||||
final IconData selectedIcon;
|
||||
final String? assetPath;
|
||||
final String? selectedAssetPath;
|
||||
final Widget? customIcon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool selected;
|
||||
@@ -95,31 +93,26 @@ class _NavDestination extends StatelessWidget {
|
||||
duration: const Duration(milliseconds: 160),
|
||||
curve: Curves.easeOut,
|
||||
scale: item.selected ? 1 : 0.96,
|
||||
child:
|
||||
item.customIcon ??
|
||||
(assetPath == null
|
||||
? Icon(
|
||||
child: assetPath == null
|
||||
? Icon(
|
||||
fallbackIcon,
|
||||
size: AppDimens.navIcon,
|
||||
color: color,
|
||||
)
|
||||
: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
assetPath,
|
||||
width: AppDimens.navIcon,
|
||||
height: AppDimens.navIcon,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => Icon(
|
||||
fallbackIcon,
|
||||
size: AppDimens.navIcon,
|
||||
color: color,
|
||||
)
|
||||
: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(
|
||||
color,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
child: Image.asset(
|
||||
assetPath,
|
||||
width: AppDimens.navIcon,
|
||||
height: AppDimens.navIcon,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => Icon(
|
||||
fallbackIcon,
|
||||
size: AppDimens.navIcon,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.navLabelGap),
|
||||
Text(
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_dimens.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import 'app_bottom_nav.dart';
|
||||
import 'root_nav_glyph.dart';
|
||||
|
||||
/// 客户端根导航的固定顺序。
|
||||
enum AppRootTab {
|
||||
@@ -18,13 +16,6 @@ enum AppRootTab {
|
||||
}
|
||||
|
||||
extension AppRootTabUi on AppRootTab {
|
||||
RootNavGlyphKind get glyphKind => switch (this) {
|
||||
AppRootTab.home => RootNavGlyphKind.home,
|
||||
AppRootTab.recommend => RootNavGlyphKind.recommend,
|
||||
AppRootTab.following => RootNavGlyphKind.following,
|
||||
AppRootTab.mine => RootNavGlyphKind.mine,
|
||||
};
|
||||
|
||||
Key get tabKey => switch (this) {
|
||||
AppRootTab.home => const Key('tab-home'),
|
||||
AppRootTab.recommend => const Key('tab-recommend'),
|
||||
@@ -80,11 +71,6 @@ class AppRootBottomNav extends StatelessWidget {
|
||||
selectedIcon: tab.selectedIcon,
|
||||
label: tab.label(l10n),
|
||||
selected: tab == selected,
|
||||
customIcon: RootNavGlyph(
|
||||
kind: tab.glyphKind,
|
||||
selected: tab == selected,
|
||||
size: AppDimens.navIcon,
|
||||
),
|
||||
onTap: () => onSelected(tab),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:绘制根导航使用的饱满品牌插画图标。
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
const double _designSize = 30;
|
||||
|
||||
/// 根导航四个入口对应的图标种类。
|
||||
enum RootNavGlyphKind { home, recommend, following, mine }
|
||||
|
||||
/// 根导航插画图标,根据选中态使用品牌红和中性灰白配色。
|
||||
class RootNavGlyph extends StatelessWidget {
|
||||
const RootNavGlyph({
|
||||
super.key,
|
||||
required this.kind,
|
||||
required this.selected,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final RootNavGlyphKind kind;
|
||||
final bool selected;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
size: Size.square(size),
|
||||
painter: _RootNavGlyphPainter(kind: kind, selected: selected),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RootNavGlyphPainter extends CustomPainter {
|
||||
const _RootNavGlyphPainter({required this.kind, required this.selected});
|
||||
|
||||
final RootNavGlyphKind kind;
|
||||
final bool selected;
|
||||
|
||||
Color get _main => selected ? AppColors.navSelected : const Color(0xFFD0D0D2);
|
||||
Color get _light => selected ? AppColors.homeCrownGold : Colors.white;
|
||||
Color get _shade =>
|
||||
selected ? const Color(0xFF9F2935) : const Color(0xFF858589);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.save();
|
||||
canvas.scale(size.width / _designSize);
|
||||
switch (kind) {
|
||||
case RootNavGlyphKind.home:
|
||||
_paintHome(canvas);
|
||||
case RootNavGlyphKind.recommend:
|
||||
_paintRecommend(canvas);
|
||||
case RootNavGlyphKind.following:
|
||||
_paintFollowing(canvas);
|
||||
case RootNavGlyphKind.mine:
|
||||
_paintMine(canvas);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
void _paintHome(Canvas canvas) {
|
||||
final roof = Path()
|
||||
..moveTo(3, 13)
|
||||
..lineTo(15, 3)
|
||||
..lineTo(27, 13)
|
||||
..lineTo(24, 16)
|
||||
..lineTo(15, 8.5)
|
||||
..lineTo(6, 16)
|
||||
..close();
|
||||
canvas.drawShadow(roof, AppColors.shadow, 2, false);
|
||||
canvas.drawPath(roof, _gradient(const Rect.fromLTWH(3, 3, 24, 13)));
|
||||
final body = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(6, 13, 24, 27),
|
||||
const Radius.circular(3),
|
||||
);
|
||||
canvas.drawRRect(body, _gradient(const Rect.fromLTWH(6, 13, 18, 14)));
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(12, 18, 18, 27),
|
||||
const Radius.circular(2),
|
||||
),
|
||||
Paint()..color = _shade,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(8, 16, 11, 19),
|
||||
const Radius.circular(1),
|
||||
),
|
||||
Paint()..color = _light,
|
||||
);
|
||||
}
|
||||
|
||||
void _paintRecommend(Canvas canvas) {
|
||||
final body = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(6, 4, 24, 27),
|
||||
const Radius.circular(5),
|
||||
);
|
||||
canvas.drawShadow(Path()..addRRect(body), AppColors.shadow, 2, false);
|
||||
canvas.drawRRect(body, _gradient(const Rect.fromLTWH(6, 4, 18, 23)));
|
||||
for (var index = 0; index < 3; index++) {
|
||||
final left = 9.0 + index * 5;
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(left, 7, 3, 2.5),
|
||||
const Radius.circular(1),
|
||||
),
|
||||
Paint()..color = _light,
|
||||
);
|
||||
}
|
||||
final play = Path()
|
||||
..moveTo(12, 13)
|
||||
..quadraticBezierTo(12, 11.5, 13.5, 12.3)
|
||||
..lineTo(19.5, 15.7)
|
||||
..quadraticBezierTo(21, 16.5, 19.5, 17.3)
|
||||
..lineTo(13.5, 20.7)
|
||||
..quadraticBezierTo(12, 21.5, 12, 20)
|
||||
..close();
|
||||
canvas.drawPath(play, Paint()..color = _light);
|
||||
}
|
||||
|
||||
void _paintFollowing(Canvas canvas) {
|
||||
final page = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(5, 4, 25, 27),
|
||||
const Radius.circular(4),
|
||||
);
|
||||
canvas.drawShadow(Path()..addRRect(page), AppColors.shadow, 2, false);
|
||||
canvas.drawRRect(page, _gradient(const Rect.fromLTWH(5, 4, 20, 23)));
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(8, 9, 17, 10.5),
|
||||
const Radius.circular(0.7),
|
||||
),
|
||||
Paint()..color = _light,
|
||||
);
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(
|
||||
const Rect.fromLTRB(8, 13, 16, 14.5),
|
||||
const Radius.circular(0.7),
|
||||
),
|
||||
Paint()..color = _light,
|
||||
);
|
||||
final bookmark = Path()
|
||||
..moveTo(18, 7)
|
||||
..lineTo(22, 7)
|
||||
..lineTo(22, 20)
|
||||
..lineTo(20, 18)
|
||||
..lineTo(18, 20)
|
||||
..close();
|
||||
canvas.drawPath(bookmark, Paint()..color = _light);
|
||||
}
|
||||
|
||||
void _paintMine(Canvas canvas) {
|
||||
final bounds = Rect.fromCircle(center: const Offset(15, 15), radius: 13);
|
||||
canvas.drawShadow(Path()..addOval(bounds), AppColors.shadow, 2, false);
|
||||
canvas.drawCircle(const Offset(15, 15), 13, _gradient(bounds));
|
||||
canvas.drawCircle(const Offset(15, 11), 4, Paint()..color = _light);
|
||||
final shoulders = Path()
|
||||
..moveTo(7, 24)
|
||||
..cubicTo(7.5, 19, 10.5, 17, 15, 17)
|
||||
..cubicTo(19.5, 17, 22.5, 19, 23, 24)
|
||||
..quadraticBezierTo(15, 29, 7, 24)
|
||||
..close();
|
||||
canvas.drawPath(shoulders, Paint()..color = _light);
|
||||
canvas.drawCircle(const Offset(13.5, 11), 0.55, Paint()..color = _shade);
|
||||
canvas.drawCircle(const Offset(16.5, 11), 0.55, Paint()..color = _shade);
|
||||
}
|
||||
|
||||
Paint _gradient(Rect bounds) => Paint()
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [_light, _main, _shade],
|
||||
).createShader(bounds);
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _RootNavGlyphPainter oldDelegate) =>
|
||||
oldDelegate.kind != kind || oldDelegate.selected != selected;
|
||||
}
|
||||
@@ -3,19 +3,8 @@
|
||||
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,
|
||||
@@ -30,124 +19,27 @@ class PlayerLoadingOverlay extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: opacity,
|
||||
duration: _fadeDuration,
|
||||
duration: const Duration(milliseconds: 140),
|
||||
curve: Curves.easeOut,
|
||||
onEnd: onFadeEnd,
|
||||
child: Container(
|
||||
key: const Key('player-loading-surface'),
|
||||
width: _surfaceSize.width,
|
||||
height: _surfaceSize.height,
|
||||
width: AppDimens.refreshIndicatorSize,
|
||||
height: AppDimens.refreshIndicatorSize,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.refreshIndicatorSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.refreshIndicatorRadius),
|
||||
),
|
||||
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,
|
||||
child: const SizedBox.square(
|
||||
dimension: AppDimens.refreshSpinnerSize,
|
||||
child: CircularProgressIndicator(
|
||||
key: Key('player-entry-loading'),
|
||||
color: Colors.white,
|
||||
strokeWidth: AppDimens.refreshSpinnerStroke,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,7 +4,6 @@ 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';
|
||||
@@ -12,7 +11,7 @@ import 'search_gift_glyphs.dart';
|
||||
const double _navHeight = 56;
|
||||
const double _tabGap = 20;
|
||||
const double _tabFont = 17;
|
||||
const double _searchHitSize = AppDimens.tapTarget;
|
||||
const double _searchHitSize = 40;
|
||||
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,12 +50,8 @@ 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);
|
||||
}
|
||||
|
||||
@@ -64,7 +60,7 @@ class _SearchPainter extends CustomPainter {
|
||||
oldDelegate.color != color;
|
||||
}
|
||||
|
||||
/// YMB 品牌礼盒静态图标,使用玫红礼盒与金色丝带。
|
||||
/// emoji 礼盒(红盒金丝带,随系统 emoji 风格渲染)。
|
||||
class GiftGlyph extends StatelessWidget {
|
||||
const GiftGlyph({super.key, required this.size});
|
||||
|
||||
@@ -72,94 +68,11 @@ class GiftGlyph extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
return SizedBox.square(
|
||||
dimension: size,
|
||||
child: FittedBox(
|
||||
child: Text('🎁', style: TextStyle(fontSize: size, height: 1)),
|
||||
),
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
// 应用环境选择测试。
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/config/app_config.dart';
|
||||
import 'package:fruitshorts/core/config/dev_config.dart';
|
||||
import 'package:fruitshorts/core/config/prod_config.dart';
|
||||
|
||||
void main() {
|
||||
test('should_use_environment_matching_build_mode', () {
|
||||
expect(AppConfig.useDevConfig, !kReleaseMode);
|
||||
expect(
|
||||
AppConfig.baseUrl,
|
||||
kReleaseMode ? ProdConfig.baseUrl : DevConfig.baseUrl,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||
import 'package:fruitshorts/core/widgets/app_bottom_nav.dart';
|
||||
import 'package:fruitshorts/core/widgets/root_nav_glyph.dart';
|
||||
import 'package:fruitshorts/pages/following/following_page.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
@@ -139,20 +138,18 @@ void main() {
|
||||
.duration,
|
||||
lessThanOrEqualTo(const Duration(milliseconds: 200)),
|
||||
);
|
||||
final selectedHomeIcon = tester.widget<RootNavGlyph>(
|
||||
final selectedHomeIcon = tester.widget<Icon>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.home_rounded),
|
||||
),
|
||||
);
|
||||
expect(selectedHomeIcon.kind, RootNavGlyphKind.home);
|
||||
expect(selectedHomeIcon.selected, isTrue);
|
||||
expect(selectedHomeIcon.size, AppDimens.navIcon);
|
||||
expect(selectedHomeIcon.color, AppColors.navSelected);
|
||||
expect(
|
||||
tester.getSize(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.home_rounded),
|
||||
),
|
||||
),
|
||||
const Size.square(AppDimens.navIcon),
|
||||
@@ -160,21 +157,21 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-recommend')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.smart_display_outlined),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-follow')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.bookmark_border_rounded),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-mine')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.person_outline_rounded),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
@@ -191,7 +188,7 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-recommend')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.smart_display_rounded),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
@@ -222,20 +219,20 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.home_outlined),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<RootNavGlyph>(
|
||||
.widget<Icon>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.home_outlined),
|
||||
),
|
||||
)
|
||||
.selected,
|
||||
isFalse,
|
||||
.color,
|
||||
AppColors.navUnselected,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -273,7 +270,7 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-follow')),
|
||||
matching: find.byType(RootNavGlyph),
|
||||
matching: find.byIcon(Icons.bookmark_rounded),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/widgets/app_root_tab.dart';
|
||||
import 'package:fruitshorts/core/widgets/root_nav_glyph.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'should_render_four_ordered_glyphs_with_home_selected_when_root_nav_builds',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
bottomNavigationBar: AppRootBottomNav(
|
||||
selected: AppRootTab.home,
|
||||
onSelected: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final glyphs = tester
|
||||
.widgetList<RootNavGlyph>(find.byType(RootNavGlyph))
|
||||
.toList();
|
||||
expect(glyphs.map((glyph) => glyph.kind), RootNavGlyphKind.values);
|
||||
expect(glyphs.map((glyph) => glyph.selected), [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -313,7 +313,7 @@ void main() {
|
||||
.first;
|
||||
expect(loadingOverlay, findsOneWidget);
|
||||
expect(tester.widget<IgnorePointer>(loadingOverlay).ignoring, isTrue);
|
||||
expect(tester.getSize(loadingOverlay), const Size(56, 36));
|
||||
expect(tester.getSize(loadingOverlay), const Size(40, 40));
|
||||
expect(
|
||||
tester
|
||||
.widget<AnimatedOpacity>(
|
||||
@@ -327,29 +327,26 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('player-loading-surface'))),
|
||||
const Size(56, 36),
|
||||
const Size(40, 40),
|
||||
);
|
||||
expect(
|
||||
tester.getSize(find.byKey(const Key('player-entry-loading'))),
|
||||
const Size(48, 29),
|
||||
const Size(20, 20),
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: loadingOverlay,
|
||||
matching: find.byType(CircularProgressIndicator),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.text('Loading'), findsNothing);
|
||||
expect(find.byKey(const Key('player-entry-loading-track')), findsNothing);
|
||||
expect(
|
||||
find.byKey(const Key('player-entry-loading-highlight')),
|
||||
findsNothing,
|
||||
tester
|
||||
.widget<CircularProgressIndicator>(
|
||||
find.byKey(const Key('player-entry-loading')),
|
||||
)
|
||||
.color,
|
||||
Colors.white,
|
||||
);
|
||||
final loadingSurface = tester.widget<Container>(
|
||||
find.byKey(const Key('player-loading-surface')),
|
||||
);
|
||||
expect(loadingSurface.decoration, isNull);
|
||||
final decoration = loadingSurface.decoration! as BoxDecoration;
|
||||
expect(decoration.color, AppColors.refreshIndicatorSurface);
|
||||
expect(decoration.borderRadius, BorderRadius.circular(20));
|
||||
expect(
|
||||
find.descendant(of: loadingOverlay, matching: find.byType(ColoredBox)),
|
||||
findsNothing,
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:验证剧集播放页 YMB 入场 loading 的品牌视觉、运动和减少动画行为。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/player_loading_overlay.dart';
|
||||
|
||||
const _surface = Key('player-loading-surface');
|
||||
const _logo = Key('player-entry-loading');
|
||||
const _motion = Key('player-entry-loading-motion');
|
||||
|
||||
void main() {
|
||||
testWidgets('should_show_ymb_motion_without_circular_spinner', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(tester);
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.byKey(_surface), findsOneWidget);
|
||||
expect(find.byKey(_logo), findsOneWidget);
|
||||
expect(find.text('Loading'), findsNothing);
|
||||
expect(find.byKey(const Key('player-entry-loading-track')), findsNothing);
|
||||
expect(
|
||||
find.byKey(const Key('player-entry-loading-highlight')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(find.byType(ShaderMask), findsOneWidget);
|
||||
expect(tester.getSize(find.byKey(_surface)), const Size(56, 36));
|
||||
expect(tester.getSize(find.byKey(_logo)), const Size(48, 29));
|
||||
expect(tester.widget<Container>(find.byKey(_surface)).decoration, isNull);
|
||||
|
||||
final initialScale = _scaleOf(tester);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(_scaleOf(tester), isNot(initialScale));
|
||||
});
|
||||
|
||||
testWidgets('should_keep_loading_static_when_motion_is_disabled', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(tester, disableAnimations: true);
|
||||
|
||||
final initialScale = _scaleOf(tester);
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(_scaleOf(tester), initialScale);
|
||||
expect(find.byKey(_logo), findsOneWidget);
|
||||
expect(find.text('Loading'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
double _scaleOf(WidgetTester tester) {
|
||||
return tester.widget<Transform>(find.byKey(_motion)).transform.storage.first;
|
||||
}
|
||||
|
||||
Future<void> _pump(
|
||||
WidgetTester tester, {
|
||||
bool disableAnimations = false,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(disableAnimations: disableAnimations),
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: PlayerLoadingOverlay(opacity: 1, onFadeEnd: () {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user