merge: 使用胶片盘图片导航图标
This commit is contained in:
commit
b1963a718e
6 files changed
+132
-47
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1150,3 +1150,9 @@ $ flutter test
|
||||
- `AppRootTab.home` 未选中改为 `theaters_outlined`,选中改为 `theaters`。
|
||||
- 其余三个根导航图标、26 尺寸及白/灰颜色规则保持不变;测试断言同步。
|
||||
- 按当前流程未执行 analyze/test。
|
||||
|
||||
## 2026-10-22 首页圆形胶片盘图标
|
||||
|
||||
- `AppRootTab` 新增可选 `assetPath`,首页使用 `assets/icon/film_reel.png`。
|
||||
- 共享导航通过 `ColorFiltered + Image.asset` 按选中态着色,尺寸保持 26,缺图回退胶片 Material 图标。
|
||||
- 推荐、追剧、我的图标不变;素材与测试断言同步,按当前流程未执行 analyze/test。
|
||||
@@ -14,11 +14,13 @@ class AppBottomNavItem {
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.selected = false,
|
||||
this.assetPath,
|
||||
});
|
||||
|
||||
final Key tabKey;
|
||||
final IconData icon;
|
||||
final IconData selectedIcon;
|
||||
final String? assetPath;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool selected;
|
||||
@@ -61,49 +63,66 @@ class _NavDestination extends StatelessWidget {
|
||||
final Color unselectedColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: item.selected,
|
||||
label: item.label,
|
||||
child: InkWell(
|
||||
key: item.tabKey,
|
||||
onTap: item.onTap,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedScale(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
curve: Curves.easeOut,
|
||||
scale: item.selected ? 1 : 0.96,
|
||||
child: Icon(
|
||||
item.selected ? item.selectedIcon : item.icon,
|
||||
size: AppDimens.navIcon,
|
||||
color: item.selected
|
||||
? AppColors.navSelected
|
||||
: unselectedColor,
|
||||
Widget build(BuildContext context) {
|
||||
final color = item.selected ? AppColors.navSelected : unselectedColor;
|
||||
final fallbackIcon = item.selected ? item.selectedIcon : item.icon;
|
||||
return Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: item.selected,
|
||||
label: item.label,
|
||||
child: InkWell(
|
||||
key: item.tabKey,
|
||||
onTap: item.onTap,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedScale(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
curve: Curves.easeOut,
|
||||
scale: item.selected ? 1 : 0.96,
|
||||
child: item.assetPath == null
|
||||
? Icon(
|
||||
fallbackIcon,
|
||||
size: AppDimens.navIcon,
|
||||
color: color,
|
||||
)
|
||||
: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
item.assetPath!,
|
||||
width: AppDimens.navIcon,
|
||||
height: AppDimens.navIcon,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => Icon(
|
||||
item.icon,
|
||||
size: AppDimens.navIcon,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimens.navLabelGap),
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: AppDimens.navLabel,
|
||||
fontWeight: item.selected ? FontWeight.w600 : FontWeight.w400,
|
||||
color: item.selected
|
||||
? AppColors.navSelected
|
||||
: unselectedColor,
|
||||
letterSpacing: 0,
|
||||
const SizedBox(height: AppDimens.navLabelGap),
|
||||
Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: AppDimens.navLabel,
|
||||
fontWeight: item.selected
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
color: color,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,16 @@ import '../../l10n/app_localizations.dart';
|
||||
import 'app_bottom_nav.dart';
|
||||
|
||||
/// 客户端根导航的固定顺序。
|
||||
enum AppRootTab { home, recommend, following, mine }
|
||||
enum AppRootTab {
|
||||
home(assetPath: 'assets/icon/film_reel.png'),
|
||||
recommend(),
|
||||
following(),
|
||||
mine();
|
||||
|
||||
const AppRootTab({this.assetPath});
|
||||
|
||||
final String? assetPath;
|
||||
}
|
||||
|
||||
extension AppRootTabUi on AppRootTab {
|
||||
Key get tabKey => switch (this) {
|
||||
@@ -62,6 +71,7 @@ class AppRootBottomNav extends StatelessWidget {
|
||||
tabKey: tab.tabKey,
|
||||
icon: tab.icon,
|
||||
selectedIcon: tab.selectedIcon,
|
||||
assetPath: tab.assetPath,
|
||||
label: tab.label(l10n),
|
||||
selected: tab == selected,
|
||||
onTap: () => onSelected(tab),
|
||||
|
||||
@@ -179,4 +179,29 @@ void main() {
|
||||
findsWidgets,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_fallback_when_asset_icon_is_missing', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
bottomNavigationBar: AppBottomNav(
|
||||
items: [
|
||||
AppBottomNavItem(
|
||||
tabKey: const Key('tab-home'),
|
||||
icon: Icons.theaters_outlined,
|
||||
selectedIcon: Icons.theaters,
|
||||
assetPath: 'assets/icon/missing_film_reel.png',
|
||||
label: 'Home',
|
||||
selected: true,
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.theaters_outlined), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -191,7 +191,7 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byIcon(Icons.theaters),
|
||||
matching: find.byType(Image),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
@@ -218,14 +218,28 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<Icon>(
|
||||
.widget<ColorFiltered>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byIcon(Icons.theaters),
|
||||
matching: find.byType(ColorFiltered),
|
||||
),
|
||||
)
|
||||
.color,
|
||||
AppColors.navSelected,
|
||||
.colorFilter,
|
||||
const ColorFilter.mode(AppColors.navSelected, BlendMode.srcIn),
|
||||
);
|
||||
final selectedHomeImage = tester.widget<Image>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(Image),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
(selectedHomeImage.image as AssetImage).assetName,
|
||||
'assets/icon/film_reel.png',
|
||||
);
|
||||
expect(
|
||||
Size(selectedHomeImage.width!, selectedHomeImage.height!),
|
||||
const Size.square(AppDimens.navIcon),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||
@@ -271,10 +285,21 @@ void main() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byIcon(Icons.theaters_outlined),
|
||||
matching: find.byType(Image),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<ColorFiltered>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(ColorFiltered),
|
||||
),
|
||||
)
|
||||
.colorFilter,
|
||||
const ColorFilter.mode(AppColors.navUnselected, BlendMode.srcIn),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_map_four_tabs_to_home_recommend_following_and_me', (
|
||||
|
||||
Reference in new issue
Block a user