feat(home): 支持滑动切换首页分类
This commit is contained in:
1 parent
b921f79a71
commit
5282194ca5
3 files changed
+86
-8
No files matched your search
@@ -1112,6 +1112,7 @@ $ flutter test
|
|||||||
## 2026-10-22 首页布局对齐修正
|
## 2026-10-22 首页布局对齐修正
|
||||||
|
|
||||||
- 一级分类未选 18/w400、选中 20/w700;二级 chips 间距 10、左右 8、15/w600,并以稳定 AnimatedContainer 切换。
|
- 一级分类未选 18/w400、选中 20/w700;二级 chips 间距 10、左右 8、15/w600,并以稳定 AnimatedContainer 切换。
|
||||||
|
- 首页内容区改为横向 PageView:点击与滑动共用 PageController,各 tab 保留纵向滚动位置。
|
||||||
- 四类卡片去底板/描边,封面统一 3:4、8 圆角、行距 10、封底标题间距 6;状态角标为右上角紧凑异形丝带。
|
- 四类卡片去底板/描边,封面统一 3:4、8 圆角、行距 10、封底标题间距 6;状态角标为右上角紧凑异形丝带。
|
||||||
- 标题下方恢复左对齐题材文字,使用 `|` 分隔、灰色小字,与封面状态角标保持独立。
|
- 标题下方恢复左对齐题材文字,使用 `|` 分隔、灰色小字,与封面状态角标保持独立。
|
||||||
- 首页搜索栏到一级、一级到二级、二级到网格间距为 8/8/10;非精选 tab 直接以 10 衔接网格。
|
- 首页搜索栏到一级、一级到二级、二级到网格间距为 8/8/10;非精选 tab 直接以 10 衔接网格。
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class HomeFeedGrid extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomeFeedGridState extends State<HomeFeedGrid> {
|
class _HomeFeedGridState extends State<HomeFeedGrid> {
|
||||||
HomePrimaryTab _selectedPrimary = HomePrimaryTab.featured;
|
HomePrimaryTab _selectedPrimary = HomePrimaryTab.featured;
|
||||||
|
late final PageController _pageController;
|
||||||
late final Map<HomePrimaryTab, ScrollController> _scrollControllers;
|
late final Map<HomePrimaryTab, ScrollController> _scrollControllers;
|
||||||
final Map<HomePrimaryTab, String?> _selectedTags = {
|
final Map<HomePrimaryTab, String?> _selectedTags = {
|
||||||
for (final tab in HomePrimaryTab.values) tab: null,
|
for (final tab in HomePrimaryTab.values) tab: null,
|
||||||
@@ -51,6 +52,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_pageController = PageController(initialPage: _selectedPrimary.index);
|
||||||
_scrollControllers = {
|
_scrollControllers = {
|
||||||
for (final tab in HomePrimaryTab.values) tab: ScrollController(),
|
for (final tab in HomePrimaryTab.values) tab: ScrollController(),
|
||||||
};
|
};
|
||||||
@@ -58,6 +60,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
for (final controller in _scrollControllers.values) {
|
for (final controller in _scrollControllers.values) {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
}
|
}
|
||||||
@@ -93,19 +96,30 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
selectedPrimary: _selectedPrimary,
|
selectedPrimary: _selectedPrimary,
|
||||||
selectedTag: activeTag,
|
selectedTag: activeTag,
|
||||||
onPrimarySelected: (tab) =>
|
onPrimarySelected: _selectPrimary,
|
||||||
setState(() => _selectedPrimary = tab),
|
|
||||||
onTagSelected: (tag) =>
|
onTagSelected: (tag) =>
|
||||||
setState(() => _selectedTags[_selectedPrimary] = tag),
|
setState(() => _selectedTags[_selectedPrimary] = tag),
|
||||||
walletBadge: walletBadge,
|
walletBadge: walletBadge,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: IndexedStack(
|
child: PageView.builder(
|
||||||
index: _selectedPrimary.index,
|
key: const Key('home-primary-page-view'),
|
||||||
children: [
|
controller: _pageController,
|
||||||
for (final tab in HomePrimaryTab.values)
|
itemCount: HomePrimaryTab.values.length,
|
||||||
_tabGrid(context, l10n, tab: tab, shortcuts: shortcuts),
|
onPageChanged: (index) => setState(
|
||||||
],
|
() => _selectedPrimary = HomePrimaryTab.values[index],
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) => _KeepAlivePage(
|
||||||
|
key: PageStorageKey(
|
||||||
|
'home-primary-page-${HomePrimaryTab.values[index].name}',
|
||||||
|
),
|
||||||
|
child: _tabGrid(
|
||||||
|
context,
|
||||||
|
l10n,
|
||||||
|
tab: HomePrimaryTab.values[index],
|
||||||
|
shortcuts: shortcuts,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -114,6 +128,21 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectPrimary(HomePrimaryTab tab) {
|
||||||
|
if (tab == _selectedPrimary) return;
|
||||||
|
if (!_pageController.hasClients) {
|
||||||
|
setState(() => _selectedPrimary = tab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(
|
||||||
|
_pageController.animateToPage(
|
||||||
|
tab.index,
|
||||||
|
duration: AppDimens.homePrimaryTabAnimationDuration,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _tabGrid(
|
Widget _tabGrid(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
AppLocalizations l10n, {
|
AppLocalizations l10n, {
|
||||||
@@ -329,3 +358,24 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _KeepAlivePage extends StatefulWidget {
|
||||||
|
const _KeepAlivePage({super.key, required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_KeepAlivePage> createState() => _KeepAlivePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeepAlivePageState extends State<_KeepAlivePage>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
|
|||||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||||
|
import 'package:fruitshorts/core/theme/app_dimens.dart';
|
||||||
import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart';
|
import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart';
|
||||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||||
@@ -170,6 +171,32 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_swipe_between_primary_sections', (tester) async {
|
||||||
|
final repository = _PagedCatalogRepository();
|
||||||
|
await _pumpApp(tester, repository);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget);
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('home-primary-page-view')),
|
||||||
|
const Offset(-320, 0),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
||||||
|
final selected = tester.widget<AnimatedDefaultTextStyle>(
|
||||||
|
find.byKey(const Key('home-primary-label-newReleases')),
|
||||||
|
);
|
||||||
|
expect(selected.style.fontSize, AppDimens.homePrimaryTabSelectedFont);
|
||||||
|
expect(selected.style.fontWeight, FontWeight.w700);
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('home-primary-page-view')),
|
||||||
|
const Offset(320, 0),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('home-secondary-tags')), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_load_next_page_without_expanding_page_size', (
|
testWidgets('should_load_next_page_without_expanding_page_size', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
Reference in new issue
Block a user