merge: 合并 K31-D 首页频道吸顶
This commit is contained in:
commit
8e2cd5c838
2 files changed
+104
-44
No files matched your search
@@ -25,7 +25,6 @@ const double _channelGap = 20;
|
|||||||
const double _channelUnderlineWidth = 18;
|
const double _channelUnderlineWidth = 18;
|
||||||
const double _channelUnderlineHeight = 2;
|
const double _channelUnderlineHeight = 2;
|
||||||
const double _channelUnderlineGap = 4;
|
const double _channelUnderlineGap = 4;
|
||||||
const double _channelOverlayTop = 8;
|
|
||||||
const double _channelFontSize = 15;
|
const double _channelFontSize = 15;
|
||||||
const List<Shadow> _channelTextShadows = [
|
const List<Shadow> _channelTextShadows = [
|
||||||
Shadow(color: Colors.black54, blurRadius: 5, offset: Offset(0, 1)),
|
Shadow(color: Colors.black54, blurRadius: 5, offset: Offset(0, 1)),
|
||||||
@@ -58,11 +57,14 @@ class HomeBrowseView extends StatefulWidget {
|
|||||||
|
|
||||||
class _HomeBrowseViewState extends State<HomeBrowseView> {
|
class _HomeBrowseViewState extends State<HomeBrowseView> {
|
||||||
late final HomeBrowseLogic logic;
|
late final HomeBrowseLogic logic;
|
||||||
|
final ScrollController _scroll = ScrollController();
|
||||||
|
bool _channelsPinned = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
logic = HomeBrowseLogic(history: context.read<WatchHistoryRepository>());
|
logic = HomeBrowseLogic(history: context.read<WatchHistoryRepository>());
|
||||||
|
_scroll.addListener(_onScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -73,10 +75,33 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_scroll.removeListener(_onScroll);
|
||||||
|
_scroll.dispose();
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 滚过 hero 后频道行切成吸顶态,未过阈值时仍叠在轮播图上。
|
||||||
|
void _onScroll() {
|
||||||
|
if (!_scroll.hasClients) return;
|
||||||
|
final pinned =
|
||||||
|
_scroll.offset >= AppDimens.heroBannerHeight - _channelRowHeight;
|
||||||
|
if (pinned != _channelsPinned) {
|
||||||
|
setState(() => _channelsPinned = pinned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 频道行只横向滚动,纵向拖动转交给页面列表,避免挡住上滑。
|
||||||
|
void _scrollPageBy(DragUpdateDetails details) {
|
||||||
|
if (!_scroll.hasClients) return;
|
||||||
|
final position = _scroll.position;
|
||||||
|
_scroll.jumpTo(
|
||||||
|
(_scroll.offset - details.delta.dy)
|
||||||
|
.clamp(position.minScrollExtent, position.maxScrollExtent)
|
||||||
|
.toDouble(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
@@ -103,19 +128,63 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _body(AppLocalizations l10n, CatalogController catalog) {
|
Widget _body(AppLocalizations l10n, CatalogController catalog) {
|
||||||
return RefreshIndicator(
|
final source = catalog.dramas;
|
||||||
key: const Key('home-refresh'),
|
final banners = catalog.banners.isEmpty
|
||||||
color: AppColors.blue,
|
? _fallbackBanners(source)
|
||||||
backgroundColor: AppColors.card,
|
: catalog.banners;
|
||||||
onRefresh: () async {
|
final hasBanner = banners.isNotEmpty;
|
||||||
await Future.wait([catalog.load(), logic.refreshRecent()]);
|
final pinned = !hasBanner || _channelsPinned;
|
||||||
},
|
return Stack(
|
||||||
child: ListView(
|
children: [
|
||||||
key: const Key('home-browse-page'),
|
Positioned.fill(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
child: RefreshIndicator(
|
||||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
key: const Key('home-refresh'),
|
||||||
children: _sections(l10n, catalog),
|
color: AppColors.blue,
|
||||||
|
backgroundColor: AppColors.card,
|
||||||
|
onRefresh: () async {
|
||||||
|
await Future.wait([catalog.load(), logic.refreshRecent()]);
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
key: const Key('home-browse-page'),
|
||||||
|
controller: _scroll,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||||
|
children: _sections(l10n, catalog),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: _channelBar(
|
||||||
|
l10n,
|
||||||
|
pinned: pinned,
|
||||||
|
overImage: hasBanner && !pinned,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _channelBar(
|
||||||
|
AppLocalizations l10n, {
|
||||||
|
required bool pinned,
|
||||||
|
required bool overImage,
|
||||||
|
}) {
|
||||||
|
final content = GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onVerticalDragUpdate: _scrollPageBy,
|
||||||
|
child: _channels(l10n, overImage: overImage),
|
||||||
|
);
|
||||||
|
if (!pinned) return content;
|
||||||
|
return DecoratedBox(
|
||||||
|
key: const Key('home-channels-pinned'),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.ink,
|
||||||
|
border: Border(bottom: BorderSide(color: AppColors.border)),
|
||||||
),
|
),
|
||||||
|
child: content,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,10 +196,7 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
|||||||
? _fallbackBanners(source)
|
? _fallbackBanners(source)
|
||||||
: catalog.banners;
|
: catalog.banners;
|
||||||
return [
|
return [
|
||||||
if (banners.isNotEmpty)
|
if (banners.isNotEmpty) _hero(banners, source),
|
||||||
_heroWithChannels(l10n, banners, source)
|
|
||||||
else
|
|
||||||
_channels(l10n),
|
|
||||||
if (continueItem != null) ...[
|
if (continueItem != null) ...[
|
||||||
_continueHeader(l10n),
|
_continueHeader(l10n),
|
||||||
ContinueWatchingCard(
|
ContinueWatchingCard(
|
||||||
@@ -195,29 +261,6 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 频道行叠加在 hero 轮播之上;无 banner 时作为普通行显示。
|
|
||||||
Widget _heroWithChannels(
|
|
||||||
AppLocalizations l10n,
|
|
||||||
List<FeedBanner> banners,
|
|
||||||
List<Drama> dramas,
|
|
||||||
) {
|
|
||||||
return SizedBox(
|
|
||||||
key: const Key('home-hero-section'),
|
|
||||||
height: AppDimens.heroBannerHeight,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(child: _hero(banners, dramas)),
|
|
||||||
Positioned(
|
|
||||||
top: _channelOverlayTop,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: _channels(l10n, overImage: true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _channels(AppLocalizations l10n, {bool overImage = false}) {
|
Widget _channels(AppLocalizations l10n, {bool overImage = false}) {
|
||||||
final entries = [
|
final entries = [
|
||||||
(HomeChannel.hot, l10n.hot),
|
(HomeChannel.hot, l10n.hot),
|
||||||
|
|||||||
@@ -90,16 +90,33 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(card.borderRadius, BorderRadius.zero);
|
expect(card.borderRadius, BorderRadius.zero);
|
||||||
|
|
||||||
// 频道行在 hero Stack 内、位于轮播顶部 8 的位置上。
|
// 频道行是固定层,不在可滚动列表里,初始仍压在轮播顶部。
|
||||||
expect(
|
expect(
|
||||||
find.ancestor(
|
find.ancestor(
|
||||||
of: find.byKey(const Key('home-channels')),
|
of: find.byKey(const Key('home-channels')),
|
||||||
matching: find.byKey(const Key('home-hero-section')),
|
matching: find.byKey(const Key('home-browse-page')),
|
||||||
),
|
),
|
||||||
findsOneWidget,
|
findsNothing,
|
||||||
);
|
);
|
||||||
final channels = tester.getRect(find.byKey(const Key('home-channels')));
|
final channels = tester.getRect(find.byKey(const Key('home-channels')));
|
||||||
expect(channels.top, closeTo(hero.top + 8, 1));
|
expect(channels.top, closeTo(hero.top, 1));
|
||||||
|
expect(find.byKey(const Key('home-channels-pinned')), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_pin_home_channels_when_scrolling', (tester) async {
|
||||||
|
await _pumpApp(tester);
|
||||||
|
final before = tester.getRect(find.byKey(const Key('home-channel-hot')));
|
||||||
|
expect(find.byKey(const Key('home-channels-pinned')), findsNothing);
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('home-browse-page')),
|
||||||
|
const Offset(0, -300),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('home-channels-pinned')), findsOneWidget);
|
||||||
|
final after = tester.getRect(find.byKey(const Key('home-channel-hot')));
|
||||||
|
expect(after.top, closeTo(before.top, 0.5));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_open_theater_search_when_home_search_is_tapped', (
|
testWidgets('should_open_theater_search_when_home_search_is_tapped', (
|
||||||
|
|||||||
Reference in new issue
Block a user