feat(flutter): 压缩首页剧卡并把播放次数上封面
This commit is contained in:
1 parent
8e2cd5c838
commit
124ba75ab8
3 files changed
+104
-58
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../core/data/models/drama.dart';
|
import '../../../core/data/models/drama.dart';
|
||||||
|
import '../../../core/formatting/count_format.dart';
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
import '../../../core/theme/app_dimens.dart';
|
import '../../../core/theme/app_dimens.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
@@ -12,9 +13,12 @@ import '../../../l10n/app_localizations.dart';
|
|||||||
const int _columns = 2;
|
const int _columns = 2;
|
||||||
const double _columnGap = 10;
|
const double _columnGap = 10;
|
||||||
const double _cardGap = 10;
|
const double _cardGap = 10;
|
||||||
const double _coverAspectRatio = 3 / 4;
|
const double _coverAspectRatio = 5 / 6;
|
||||||
const double _addSize = 28;
|
const double _cardPadding = 8;
|
||||||
const double _addIconSize = 18;
|
const double _coverTitleGap = 6;
|
||||||
|
const double _lineGap = 2;
|
||||||
|
const double _viewsIconSize = 10;
|
||||||
|
const double _viewsFontSize = 9;
|
||||||
|
|
||||||
/// 首页浏览态使用的双列剧目网格。
|
/// 首页浏览态使用的双列剧目网格。
|
||||||
class HomeDramaGrid extends StatelessWidget {
|
class HomeDramaGrid extends StatelessWidget {
|
||||||
@@ -89,16 +93,16 @@ class HomeDramaCard extends StatelessWidget {
|
|||||||
key: Key('home-drama-card-${drama.id}'),
|
key: Key('home-drama-card-${drama.id}'),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(AppDimens.spaceSm),
|
padding: const EdgeInsets.all(_cardPadding),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_cover(),
|
_cover(),
|
||||||
const SizedBox(height: AppDimens.spaceSm),
|
const SizedBox(height: _coverTitleGap),
|
||||||
_title(),
|
_title(),
|
||||||
const SizedBox(height: AppDimens.spaceXs),
|
const SizedBox(height: _lineGap),
|
||||||
_genre(),
|
_genre(),
|
||||||
const SizedBox(height: AppDimens.spaceXs),
|
const SizedBox(height: _lineGap),
|
||||||
_metadata(),
|
_metadata(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -124,6 +128,7 @@ class HomeDramaCard extends StatelessWidget {
|
|||||||
Positioned(top: 8, left: 8, child: _tagBadge()),
|
Positioned(top: 8, left: 8, child: _tagBadge()),
|
||||||
if (_lockedCount > 0)
|
if (_lockedCount > 0)
|
||||||
Positioned(top: 8, right: 8, child: _lockedBadge()),
|
Positioned(top: 8, right: 8, child: _lockedBadge()),
|
||||||
|
Positioned(left: 8, bottom: 8, child: _viewsBadge()),
|
||||||
Positioned(right: 8, bottom: 8, child: _episodeBadge()),
|
Positioned(right: 8, bottom: 8, child: _episodeBadge()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -210,6 +215,37 @@ class HomeDramaCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 播放次数角标:深色半透明胶囊 + 白色小播放图标。
|
||||||
|
Widget _viewsBadge() {
|
||||||
|
return Container(
|
||||||
|
key: Key('home-drama-views-${drama.id}'),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.playerOverlayPill,
|
||||||
|
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.play_arrow_rounded,
|
||||||
|
color: AppColors.onPrimary,
|
||||||
|
size: _viewsIconSize,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
|
formatInteractionCount(drama.viewCount),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.onPrimary,
|
||||||
|
fontSize: _viewsFontSize,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _title() {
|
Widget _title() {
|
||||||
return Text(
|
return Text(
|
||||||
drama.title,
|
drama.title,
|
||||||
@@ -239,47 +275,13 @@ class HomeDramaCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _metadata() {
|
Widget _metadata() {
|
||||||
return Row(
|
return Text(
|
||||||
children: [
|
l10n.episodeCount(drama.episodeCount),
|
||||||
Expanded(
|
key: Key('home-drama-episode-count-${drama.id}'),
|
||||||
child: Wrap(
|
style: const TextStyle(
|
||||||
spacing: AppDimens.spaceSm,
|
color: AppColors.subtle,
|
||||||
runSpacing: AppDimens.spaceXs,
|
fontSize: AppDimens.cardViews,
|
||||||
children: [
|
),
|
||||||
Text(
|
|
||||||
l10n.episodeCount(drama.episodeCount),
|
|
||||||
key: Key('home-drama-episode-count-${drama.id}'),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.subtle,
|
|
||||||
fontSize: AppDimens.cardViews,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
l10n.viewCountLabel(drama.viewCount),
|
|
||||||
key: Key('home-drama-views-${drama.id}'),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.subtle,
|
|
||||||
fontSize: AppDimens.cardViews,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
key: Key('home-drama-add-${drama.id}'),
|
|
||||||
width: _addSize,
|
|
||||||
height: _addSize,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: AppColors.blue,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.add_rounded,
|
|
||||||
color: AppColors.onPrimary,
|
|
||||||
size: _addIconSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import 'package:fruitshorts/l10n/app_localizations.dart';
|
|||||||
import 'package:fruitshorts/pages/home/widgets/home_drama_grid.dart';
|
import 'package:fruitshorts/pages/home/widgets/home_drama_grid.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('should_render_warm_orange_cover_badges_and_plus_action', (
|
testWidgets('should_render_compact_cover_badges_without_plus_action', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -20,13 +20,15 @@ void main() {
|
|||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: Builder(
|
body: SingleChildScrollView(
|
||||||
builder: (context) => SizedBox(
|
child: Builder(
|
||||||
width: 180,
|
builder: (context) => SizedBox(
|
||||||
child: HomeDramaCard(
|
width: 180,
|
||||||
l10n: AppLocalizations.of(context)!,
|
child: HomeDramaCard(
|
||||||
drama: _drama,
|
l10n: AppLocalizations.of(context)!,
|
||||||
onTap: () {},
|
drama: _drama,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -54,7 +56,29 @@ void main() {
|
|||||||
expect(find.text('Free'), findsOneWidget);
|
expect(find.text('Free'), findsOneWidget);
|
||||||
expect(find.text('18 locked'), findsOneWidget);
|
expect(find.text('18 locked'), findsOneWidget);
|
||||||
expect(find.text('All 24'), findsOneWidget);
|
expect(find.text('All 24'), findsOneWidget);
|
||||||
expect(find.byKey(const Key('home-drama-add-style-drama')), findsOneWidget);
|
expect(find.byKey(const Key('home-drama-add-style-drama')), findsNothing);
|
||||||
|
|
||||||
|
// 播放次数角标落在封面内,并按万位格式化。
|
||||||
|
final views = find.byKey(const Key('home-drama-views-style-drama'));
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('home-drama-cover-style-drama')),
|
||||||
|
matching: views,
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(of: views, matching: find.text('1.2万')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 压缩后卡片高度接近「卡宽 × 1.5」,明显矮于旧版 3:4 封面。
|
||||||
|
final cardSize = tester.getSize(card);
|
||||||
|
expect(cardSize.height, lessThan(cardSize.width * 1.6));
|
||||||
|
final coverSize = tester.getSize(
|
||||||
|
find.byKey(const Key('home-drama-cover-style-drama')),
|
||||||
|
);
|
||||||
|
expect(coverSize.height / coverSize.width, closeTo(6 / 5, 0.02));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,4 +90,5 @@ const _drama = Drama(
|
|||||||
genre: 'Romance',
|
genre: 'Romance',
|
||||||
totalEpisodes: 24,
|
totalEpisodes: 24,
|
||||||
freeEpisodes: 6,
|
freeEpisodes: 6,
|
||||||
|
viewCount: 12400,
|
||||||
);
|
);
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:fruitshorts/app.dart';
|
import 'package:fruitshorts/app.dart';
|
||||||
|
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||||
import 'package:fruitshorts/core/data/models/home_feed.dart';
|
import 'package:fruitshorts/core/data/models/home_feed.dart';
|
||||||
import 'package:fruitshorts/core/data/models/watch_progress.dart';
|
import 'package:fruitshorts/core/data/models/watch_progress.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||||
@@ -104,7 +105,23 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_pin_home_channels_when_scrolling', (tester) async {
|
testWidgets('should_pin_home_channels_when_scrolling', (tester) async {
|
||||||
await _pumpApp(tester);
|
// 多放几部剧,保证内容高度足够滚过吸顶阈值。
|
||||||
|
await _pumpApp(
|
||||||
|
tester,
|
||||||
|
withBanner: false,
|
||||||
|
dramas: [
|
||||||
|
for (var index = 0; index < 10; index++)
|
||||||
|
Drama(
|
||||||
|
id: 'scroll-$index',
|
||||||
|
title: 'Scroll Drama $index',
|
||||||
|
synopsis: 'synopsis',
|
||||||
|
posterUrl: '/files/scroll-$index.png',
|
||||||
|
genre: 'Drama',
|
||||||
|
totalEpisodes: 10,
|
||||||
|
freeEpisodes: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
final before = tester.getRect(find.byKey(const Key('home-channel-hot')));
|
final before = tester.getRect(find.byKey(const Key('home-channel-hot')));
|
||||||
expect(find.byKey(const Key('home-channels-pinned')), findsNothing);
|
expect(find.byKey(const Key('home-channels-pinned')), findsNothing);
|
||||||
|
|
||||||
@@ -278,12 +295,14 @@ Future<void> _pumpApp(
|
|||||||
WatchHistoryRepository? history,
|
WatchHistoryRepository? history,
|
||||||
Size size = const Size(390, 844),
|
Size size = const Size(390, 844),
|
||||||
bool withBanner = true,
|
bool withBanner = true,
|
||||||
|
List<Drama>? dramas,
|
||||||
}) async {
|
}) async {
|
||||||
tester.view.physicalSize = size;
|
tester.view.physicalSize = size;
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
addTearDown(tester.view.resetPhysicalSize);
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
addTearDown(tester.view.resetDevicePixelRatio);
|
addTearDown(tester.view.resetDevicePixelRatio);
|
||||||
final catalog = FakeCatalogRepository(
|
final catalog = FakeCatalogRepository(
|
||||||
|
dramas: dramas,
|
||||||
banners: withBanner
|
banners: withBanner
|
||||||
? const [
|
? const [
|
||||||
FeedBanner(
|
FeedBanner(
|
||||||
|
|||||||
Reference in new issue
Block a user