feat(home): 使用胶片盘图片导航图标
This commit is contained in:
1 parent
c24518c1a4
commit
f47e4dd298
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`。
|
- `AppRootTab.home` 未选中改为 `theaters_outlined`,选中改为 `theaters`。
|
||||||
- 其余三个根导航图标、26 尺寸及白/灰颜色规则保持不变;测试断言同步。
|
- 其余三个根导航图标、26 尺寸及白/灰颜色规则保持不变;测试断言同步。
|
||||||
- 按当前流程未执行 analyze/test。
|
- 按当前流程未执行 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.label,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.selected = false,
|
this.selected = false,
|
||||||
|
this.assetPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Key tabKey;
|
final Key tabKey;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final IconData selectedIcon;
|
final IconData selectedIcon;
|
||||||
|
final String? assetPath;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
@@ -61,49 +63,66 @@ class _NavDestination extends StatelessWidget {
|
|||||||
final Color unselectedColor;
|
final Color unselectedColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Expanded(
|
Widget build(BuildContext context) {
|
||||||
child: Semantics(
|
final color = item.selected ? AppColors.navSelected : unselectedColor;
|
||||||
button: true,
|
final fallbackIcon = item.selected ? item.selectedIcon : item.icon;
|
||||||
selected: item.selected,
|
return Expanded(
|
||||||
label: item.label,
|
child: Semantics(
|
||||||
child: InkWell(
|
button: true,
|
||||||
key: item.tabKey,
|
selected: item.selected,
|
||||||
onTap: item.onTap,
|
label: item.label,
|
||||||
child: ConstrainedBox(
|
child: InkWell(
|
||||||
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
|
key: item.tabKey,
|
||||||
child: Column(
|
onTap: item.onTap,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: ConstrainedBox(
|
||||||
children: [
|
constraints: const BoxConstraints(minHeight: AppDimens.navHeight),
|
||||||
AnimatedScale(
|
child: Column(
|
||||||
duration: const Duration(milliseconds: 160),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
curve: Curves.easeOut,
|
children: [
|
||||||
scale: item.selected ? 1 : 0.96,
|
AnimatedScale(
|
||||||
child: Icon(
|
duration: const Duration(milliseconds: 160),
|
||||||
item.selected ? item.selectedIcon : item.icon,
|
curve: Curves.easeOut,
|
||||||
size: AppDimens.navIcon,
|
scale: item.selected ? 1 : 0.96,
|
||||||
color: item.selected
|
child: item.assetPath == null
|
||||||
? AppColors.navSelected
|
? Icon(
|
||||||
: unselectedColor,
|
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),
|
||||||
const SizedBox(height: AppDimens.navLabelGap),
|
Text(
|
||||||
Text(
|
item.label,
|
||||||
item.label,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: AppDimens.navLabel,
|
||||||
fontSize: AppDimens.navLabel,
|
fontWeight: item.selected
|
||||||
fontWeight: item.selected ? FontWeight.w600 : FontWeight.w400,
|
? FontWeight.w600
|
||||||
color: item.selected
|
: FontWeight.w400,
|
||||||
? AppColors.navSelected
|
color: color,
|
||||||
: unselectedColor,
|
letterSpacing: 0,
|
||||||
letterSpacing: 0,
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,16 @@ import '../../l10n/app_localizations.dart';
|
|||||||
import 'app_bottom_nav.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 {
|
extension AppRootTabUi on AppRootTab {
|
||||||
Key get tabKey => switch (this) {
|
Key get tabKey => switch (this) {
|
||||||
@@ -62,6 +71,7 @@ class AppRootBottomNav extends StatelessWidget {
|
|||||||
tabKey: tab.tabKey,
|
tabKey: tab.tabKey,
|
||||||
icon: tab.icon,
|
icon: tab.icon,
|
||||||
selectedIcon: tab.selectedIcon,
|
selectedIcon: tab.selectedIcon,
|
||||||
|
assetPath: tab.assetPath,
|
||||||
label: tab.label(l10n),
|
label: tab.label(l10n),
|
||||||
selected: tab == selected,
|
selected: tab == selected,
|
||||||
onTap: () => onSelected(tab),
|
onTap: () => onSelected(tab),
|
||||||
|
|||||||
@@ -179,4 +179,29 @@ void main() {
|
|||||||
findsWidgets,
|
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(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byKey(const Key('tab-home')),
|
of: find.byKey(const Key('tab-home')),
|
||||||
matching: find.byIcon(Icons.theaters),
|
matching: find.byType(Image),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
@@ -218,14 +218,28 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<Icon>(
|
.widget<ColorFiltered>(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byKey(const Key('tab-home')),
|
of: find.byKey(const Key('tab-home')),
|
||||||
matching: find.byIcon(Icons.theaters),
|
matching: find.byType(ColorFiltered),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.color,
|
.colorFilter,
|
||||||
AppColors.navSelected,
|
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')));
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
||||||
@@ -271,10 +285,21 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
of: find.byKey(const Key('tab-home')),
|
of: find.byKey(const Key('tab-home')),
|
||||||
matching: find.byIcon(Icons.theaters_outlined),
|
matching: find.byType(Image),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
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', (
|
testWidgets('should_map_four_tabs_to_home_recommend_following_and_me', (
|
||||||
|
|||||||
Reference in new issue
Block a user