8 Commits
Author SHA1 Message Date
Zhou Weidong 7b90bd59b9 style(home): 观看全集按钮轮廓进一步压淡
flutter-ci / test (push) Failing after 19m31s
2026-11-04 15:18:42 +08:00
Zhou Weidong 938435dfb9 style(home): 观看全集按钮改为半透明白玻璃表面
flutter-ci / test (push) Failing after 19m26s
2026-11-04 15:01:36 +08:00
Zhou Weidong b61bfe625c style(home): 观看全集按钮改用深酒红表面
flutter-ci / test (push) Failing after 19m29s
2026-11-04 14:51:20 +08:00
Zhou Weidong a7b4277536 style(home): 观看全集按钮改用暖深炭灰
flutter-ci / test (push) Failing after 19m24s
2026-11-04 14:24:21 +08:00
Zhou Weidong dabff48f7c merge: 首页观看全集配色匹配设计稿
flutter-ci / test (push) Failing after 20m24s
2026-11-04 13:44:42 +08:00
Zhou Weidong 6acc00394b style(home): 匹配观看全集设计稿配色 2026-11-04 13:42:47 +08:00
Zhou Weidong f3cfd0d0ec merge: 客户端播放器交互优化
flutter-ci / test (push) Failing after 19m31s
2026-11-03 18:28:31 +08:00
Zhou Weidong 35cbdd2bd3 perf(player): 优化点击响应与互动反馈 2026-11-03 18:28:14 +08:00
10 changed files with 81 additions and 70 deletions

No files matched your search

+5 -5
View File
@@ -1,7 +1,5 @@
// Author: fruit.com.cn // Author: fruit.com.cn
import 'package:flutter/foundation.dart';
import 'dev_config.dart'; import 'dev_config.dart';
import 'prod_config.dart'; import 'prod_config.dart';
@@ -11,10 +9,12 @@ import 'prod_config.dart';
/// - `dev_config.dart`:[DevConfig](本机联调) /// - `dev_config.dart`:[DevConfig](本机联调)
/// - `prod_config.dart`:[ProdConfig](生产) /// - `prod_config.dart`:[ProdConfig](生产)
/// ///
/// Debug 使用开发环境,Release 自动使用生产环境,不做网络探测。 /// 切换环境只改下面这一个布尔值,不用传任何构建参数、也不做网络探测。
abstract final class AppConfig { abstract final class AppConfig {
/// **环境开关**:Debug 使用开发环境,Release 使用生产环境。 /// **环境开关**:true 用开发环境,false 用生产环境。
static const bool useDevConfig = !kReleaseMode; ///
/// 打生产包前必须改成 false;release 构建若仍为 true 会在启动时直接报错。
static const bool useDevConfig = true;
/// 当前选中的环境地址。 /// 当前选中的环境地址。
static String get baseUrl => static String get baseUrl =>
+6
View File
@@ -30,6 +30,12 @@ abstract final class AppColors {
/// 首页及剧集播放页视频信息文案:参考设计图的 65% 半透明白。 /// 首页及剧集播放页视频信息文案:参考设计图的 65% 半透明白。
static const homeFeedVideoCopy = Color(0xA6FFFFFF); static const homeFeedVideoCopy = Color(0xA6FFFFFF);
/// 首页观看全集按钮:与浮层玻璃语言一致的半透明白表面。
static const watchAllSurface = Color(0x26FFFFFF);
/// 观看全集按钮的极淡白色轮廓。
static const watchAllBorder = Color(0x0FFFFFFF);
/// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。 /// 主操作、选中态、播放进度。旧品牌橙已全量映射为品牌红(2026-10 改版)。
static const blue = Color(0xFFF04E4E); static const blue = Color(0xFFF04E4E);
static const primary = blue; static const primary = blue;
@@ -35,13 +35,8 @@ const _metadataStrutStyle = StrutStyle(
const double _watchAllInset = 12; const double _watchAllInset = 12;
const double _watchAllRadius = 6; const double _watchAllRadius = 6;
const double _watchAllFont = 16; const double _watchAllFont = 16;
const _watchAllGradient = LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [AppColors.mineStatShade, AppColors.brandRed, AppColors.vipButtonEnd],
);
/// 「观看完整剧集」渐变按钮高度,overlay 用它安排底部避让。 /// 「观看完整剧集」按钮高度,overlay 用它安排底部避让。
const double homeFeedWatchAllHeight = _watchAllHeight; const double homeFeedWatchAllHeight = _watchAllHeight;
/// 播放量紧凑展示(跟随语言):zh 20万 / en 200K / id 200 rb。 /// 播放量紧凑展示(跟随语言):zh 20万 / en 200K / id 200 rb。
@@ -212,8 +207,13 @@ class HomeFeedWatchAllBar extends StatelessWidget {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(_watchAllRadius), borderRadius: BorderRadius.circular(_watchAllRadius),
child: DecoratedBox( child: DecoratedBox(
key: const Key('home-feed-full-series-gradient'), key: const Key('home-feed-full-series-surface'),
decoration: const BoxDecoration(gradient: _watchAllGradient), decoration: const BoxDecoration(
color: AppColors.watchAllSurface,
border: Border.fromBorderSide(
BorderSide(color: AppColors.watchAllBorder),
),
),
child: Material( child: Material(
key: const Key('home-feed-full-series-bg'), key: const Key('home-feed-full-series-bg'),
color: AppColors.transparent, color: AppColors.transparent,
@@ -12,9 +12,9 @@ const double _defaultIconSize = 32;
const double _defaultCountFont = 12; const double _defaultCountFont = 12;
const double _defaultIconCountGap = 2; const double _defaultIconCountGap = 2;
const double _defaultItemGap = 24; const double _defaultItemGap = 24;
const _tapDuration = Duration(milliseconds: 360); const _tapDuration = Duration(milliseconds: 220);
const double _popScale = 1.34; const double _popScale = 1.14;
const double _cancelScale = 0.68; const double _cancelScale = 0.86;
const double _popWeight = 40; const double _popWeight = 40;
const double _settleWeight = 60; const double _settleWeight = 60;
final _growTween = _tapTween(_popScale); final _growTween = _tapTween(_popScale);
@@ -39,7 +39,7 @@ class PlayerTapGestureRegion extends StatefulWidget {
} }
class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> { class _PlayerTapGestureRegionState extends State<PlayerTapGestureRegion> {
static const Duration _singleTapDelay = Duration(milliseconds: 320); static const Duration _singleTapDelay = Duration(milliseconds: 240);
Timer? _singleTapTimer; Timer? _singleTapTimer;
DateTime? _lastDownAt; DateTime? _lastDownAt;
@@ -1,18 +0,0 @@
// Author: fruit.com.cn
// 应用环境选择测试。
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/config/app_config.dart';
import 'package:fruitshorts/core/config/dev_config.dart';
import 'package:fruitshorts/core/config/prod_config.dart';
void main() {
test('should_use_environment_matching_build_mode', () {
expect(AppConfig.useDevConfig, !kReleaseMode);
expect(
AppConfig.baseUrl,
kReleaseMode ? ProdConfig.baseUrl : DevConfig.baseUrl,
);
});
}
+5 -12
View File
@@ -260,23 +260,16 @@ void main() {
_expectNoShadows(tester); _expectNoShadows(tester);
}); });
testWidgets('should_use_small_radius_and_brand_gradient_when_watching_all', ( testWidgets('should_use_reference_surface_when_watching_all', (tester) async {
tester,
) async {
await _pump(tester); await _pump(tester);
final background = find.byKey(const Key('home-feed-full-series-bg')); final background = find.byKey(const Key('home-feed-full-series-bg'));
final decorated = tester.widget<DecoratedBox>( final decorated = tester.widget<DecoratedBox>(
find.byKey(const Key('home-feed-full-series-gradient')), find.byKey(const Key('home-feed-full-series-surface')),
); );
final decoration = decorated.decoration as BoxDecoration; final decoration = decorated.decoration as BoxDecoration;
expect((decoration.gradient! as LinearGradient).colors, [ expect(decoration.color, AppColors.watchAllSurface);
AppColors.mineStatShade, expect(decoration.border?.top.color, AppColors.watchAllBorder);
AppColors.brandRed, expect(decoration.gradient, isNull);
AppColors.vipButtonEnd,
]);
final gradient = decoration.gradient! as LinearGradient;
expect(gradient.begin, Alignment.centerLeft);
expect(gradient.end, Alignment.centerRight);
expect(tester.widget<Text>(find.text('Watch all')).style?.fontSize, 16); expect(tester.widget<Text>(find.text('Watch all')).style?.fontSize, 16);
expect( expect(
tester.widget<Text>(find.text('Watch all')).style?.fontWeight, tester.widget<Text>(find.text('Watch all')).style?.fontWeight,
+27 -12
View File
@@ -1,5 +1,5 @@
// Author: fruit.com.cn // Author: fruit.com.cn
// 职责:检查观看全集按钮实际绘制像素,防止渐变只配置而不可见。 // 职责:检查观看全集按钮的玻璃表面确实被绘制出来。
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -7,18 +7,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart';
const _boundaryKey = Key('watch-gradient-boundary'); const _boundaryKey = Key('watch-surface-boundary');
const _width = 280.0; const _width = 280.0;
const _sampleInset = 16; const _sampleInset = 16;
const _sampleY = 4; const _sampleY = 4;
const _rgbaChannels = 4; const _rgbaChannels = 4;
const _minimumRedDelta = 50; const _surfaceTolerance = 0.01;
const _drama = Drama( const _drama = Drama(
id: 'gradient', id: 'surface',
title: 'Gradient', title: 'Surface',
synopsis: '', synopsis: '',
posterUrl: '', posterUrl: '',
); );
@@ -28,12 +29,16 @@ Future<void> _pumpButton(WidgetTester tester) => tester.pumpWidget(
localizationsDelegates: AppLocalizations.localizationsDelegates, localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold( home: Scaffold(
backgroundColor: Colors.black,
body: Center( body: Center(
child: RepaintBoundary( child: RepaintBoundary(
key: _boundaryKey, key: _boundaryKey,
child: SizedBox( child: SizedBox(
width: _width, width: _width,
child: HomeFeedWatchAllBar(drama: _drama, onTap: () {}), child: ColoredBox(
color: Colors.black,
child: HomeFeedWatchAllBar(drama: _drama, onTap: () {}),
),
), ),
), ),
), ),
@@ -52,7 +57,7 @@ Color _sample(ByteData bytes, int width, int x) {
} }
void main() { void main() {
testWidgets('should_paint_visible_red_gradient_instead_of_flat_fill', ( testWidgets('should_paint_reference_dark_surface_when_watching_all', (
tester, tester,
) async { ) async {
await _pumpButton(tester); await _pumpButton(tester);
@@ -68,11 +73,21 @@ void main() {
final middle = _sample(bytes, image.width, image.width ~/ 2); final middle = _sample(bytes, image.width, image.width ~/ 2);
final right = _sample(bytes, image.width, image.width - _sampleInset); final right = _sample(bytes, image.width, image.width - _sampleInset);
image.dispose(); image.dispose();
expect(left.a, 1); for (final sample in [left, middle, right]) {
expect(right.a, 1); expect(sample.a, 1);
expect((right.r - left.r) * 255, greaterThan(_minimumRedDelta)); expect(
expect(left.g, lessThan(middle.g)); sample.r,
expect(middle.g, lessThan(right.g)); closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
);
expect(
sample.g,
closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
);
expect(
sample.b,
closeTo(AppColors.watchAllSurface.a, _surfaceTolerance),
);
}
}); });
}); });
} }
+9 -7
View File
@@ -16,8 +16,10 @@ import '../support/fake_haptics.dart';
import '../support/fake_interaction_repositories.dart'; import '../support/fake_interaction_repositories.dart';
void main() { void main() {
testWidgets('double tap within 320ms never pauses', (tester) async { testWidgets('should_like_without_pausing_when_double_tap_within_240ms', (
for (final delay in [120, 250, 300]) { tester,
) async {
for (final delay in [80, 160, 220]) {
final repository = FakeInteractionRepository(); final repository = FakeInteractionRepository();
final haptics = FakeHaptics(); final haptics = FakeHaptics();
final key = GlobalKey<_HarnessState>(); final key = GlobalKey<_HarnessState>();
@@ -53,7 +55,7 @@ void main() {
} }
}); });
testWidgets('taps 500ms apart toggle playback twice', (tester) async { testWidgets('should_toggle_twice_when_taps_are_260ms_apart', (tester) async {
final key = GlobalKey<_HarnessState>(); final key = GlobalKey<_HarnessState>();
final clock = _TestClock(); final clock = _TestClock();
await _pumpHarness( await _pumpHarness(
@@ -67,15 +69,15 @@ void main() {
await tester.tapAt( await tester.tapAt(
tester.getCenter(find.byKey(const Key('home-feed-gesture'))), tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
); );
clock.advance(const Duration(milliseconds: 500)); clock.advance(const Duration(milliseconds: 260));
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 260));
expect(key.currentState!.pauseToggles, 1); expect(key.currentState!.pauseToggles, 1);
await tester.tapAt( await tester.tapAt(
tester.getCenter(find.byKey(const Key('home-feed-gesture'))), tester.getCenter(find.byKey(const Key('home-feed-gesture'))),
); );
clock.advance(const Duration(milliseconds: 500)); clock.advance(const Duration(milliseconds: 260));
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 260));
expect(key.currentState!.pauseToggles, 2); expect(key.currentState!.pauseToggles, 2);
}); });
+17 -4
View File
@@ -14,11 +14,13 @@ const _popFrame = Duration(milliseconds: 100);
const _buttonSize = 48.0; const _buttonSize = 48.0;
const _compactGap = 6.0; const _compactGap = 6.0;
const _epsilon = 0.01; const _epsilon = 0.01;
const _cancelledMaximum = 0.8; const _cancelledMinimum = 0.8;
const _popMinimum = 1.22; const _popMinimum = 1.03;
const _popMaximum = 1.18;
const _burstKey = Key('rail-tap-burst'); const _burstKey = Key('rail-tap-burst');
const _directionFrame = Duration(milliseconds: 60); const _directionFrame = Duration(milliseconds: 60);
const _directionFrames = 4; const _directionFrames = 2;
const _motionCompletion = Duration(milliseconds: 230);
Future<void> _pumpRail( Future<void> _pumpRail(
WidgetTester tester, { WidgetTester tester, {
@@ -115,7 +117,9 @@ void main() {
await tester.pump(_popFrame); await tester.pump(_popFrame);
expect( expect(
_scale(tester, key), _scale(tester, key),
selected ? lessThan(_cancelledMaximum) : greaterThan(_popMinimum), selected
? allOf(greaterThan(_cancelledMinimum), lessThan(1))
: allOf(greaterThan(_popMinimum), lessThan(_popMaximum)),
); );
_expectBurst(tester, key, selected); _expectBurst(tester, key, selected);
expect(tester.getRect(_button(key)), rect); expect(tester.getRect(_button(key)), rect);
@@ -155,6 +159,15 @@ void main() {
}, },
); );
testWidgets('should_finish_feedback_within_230ms', (tester) async {
await _pumpRail(tester, onTap: () {});
await tester.tap(_button(_likeKey));
await tester.pump();
await tester.pump(_motionCompletion);
expect(find.byKey(_burstKey), findsNothing);
expect(_scale(tester, _likeKey), closeTo(1, _epsilon));
});
testWidgets('should_keep_callbacks_without_motion_when_reduced', ( testWidgets('should_keep_callbacks_without_motion_when_reduced', (
tester, tester,
) async { ) async {