Compare commits
2
Commits
cfc41efddc
...
8e2cd5c838
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2cd5c838 | ||
|
|
7cc65b7b09 |
No files matched your search
@@ -25,7 +25,6 @@ const double _channelGap = 20;
|
||||
const double _channelUnderlineWidth = 18;
|
||||
const double _channelUnderlineHeight = 2;
|
||||
const double _channelUnderlineGap = 4;
|
||||
const double _channelOverlayTop = 8;
|
||||
const double _channelFontSize = 15;
|
||||
const List<Shadow> _channelTextShadows = [
|
||||
Shadow(color: Colors.black54, blurRadius: 5, offset: Offset(0, 1)),
|
||||
@@ -58,11 +57,14 @@ class HomeBrowseView extends StatefulWidget {
|
||||
|
||||
class _HomeBrowseViewState extends State<HomeBrowseView> {
|
||||
late final HomeBrowseLogic logic;
|
||||
final ScrollController _scroll = ScrollController();
|
||||
bool _channelsPinned = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
logic = HomeBrowseLogic(history: context.read<WatchHistoryRepository>());
|
||||
_scroll.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -73,10 +75,33 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scroll.removeListener(_onScroll);
|
||||
_scroll.dispose();
|
||||
logic.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
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
@@ -103,19 +128,63 @@ class _HomeBrowseViewState extends State<HomeBrowseView> {
|
||||
}
|
||||
|
||||
Widget _body(AppLocalizations l10n, CatalogController catalog) {
|
||||
return RefreshIndicator(
|
||||
key: const Key('home-refresh'),
|
||||
color: AppColors.blue,
|
||||
backgroundColor: AppColors.card,
|
||||
onRefresh: () async {
|
||||
await Future.wait([catalog.load(), logic.refreshRecent()]);
|
||||
},
|
||||
child: ListView(
|
||||
key: const Key('home-browse-page'),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
|
||||
children: _sections(l10n, catalog),
|
||||
final source = catalog.dramas;
|
||||
final banners = catalog.banners.isEmpty
|
||||
? _fallbackBanners(source)
|
||||
: catalog.banners;
|
||||
final hasBanner = banners.isNotEmpty;
|
||||
final pinned = !hasBanner || _channelsPinned;
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: RefreshIndicator(
|
||||
key: const Key('home-refresh'),
|
||||
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)
|
||||
: catalog.banners;
|
||||
return [
|
||||
if (banners.isNotEmpty)
|
||||
_heroWithChannels(l10n, banners, source)
|
||||
else
|
||||
_channels(l10n),
|
||||
if (banners.isNotEmpty) _hero(banners, source),
|
||||
if (continueItem != null) ...[
|
||||
_continueHeader(l10n),
|
||||
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}) {
|
||||
final entries = [
|
||||
(HomeChannel.hot, l10n.hot),
|
||||
|
||||
@@ -90,16 +90,33 @@ void main() {
|
||||
);
|
||||
expect(card.borderRadius, BorderRadius.zero);
|
||||
|
||||
// 频道行在 hero Stack 内、位于轮播顶部 8 的位置上。
|
||||
// 频道行是固定层,不在可滚动列表里,初始仍压在轮播顶部。
|
||||
expect(
|
||||
find.ancestor(
|
||||
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')));
|
||||
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', (
|
||||
|
||||
Reference in new issue
Block a user