fix(flutter): 统一详情页顶部圆形按钮

This commit is contained in:
Zhou Weidong committed 2026-10-20 16:15:53 +08:00
1 parent 230f16a44a
commit c1dd033581
4 files changed
+97 -62

No files matched your search

+40 -11
View File
@@ -21,27 +21,31 @@ const Color _darkBackground = Color(0x59000000);
const Color _darkShadowColor = Color(0x33000000); const Color _darkShadowColor = Color(0x33000000);
const Offset _lightShadowOffset = Offset(0, 2); const Offset _lightShadowOffset = Offset(0, 2);
/// 二级页面统一返回按钮,浅色页使用白底描边,深色页使用半透明黑底。 /// 二级页面统一圆形操作按钮,返回、分享和更多共用同一尺寸与表面样式。
class AppSubpageBackButton extends StatelessWidget { class AppSubpageCircleButton extends StatelessWidget {
const AppSubpageBackButton({ const AppSubpageCircleButton({
super.key, super.key,
required this.backKey, required this.buttonKey,
required this.tooltip,
required this.icon,
required this.onPressed,
this.dark = false, this.dark = false,
this.onBack,
}); });
final Key backKey; final Key buttonKey;
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
final bool dark; final bool dark;
final VoidCallback? onBack;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
key: backKey, key: buttonKey,
width: _backSize, width: _backSize,
height: _backSize, height: _backSize,
child: Tooltip( child: Tooltip(
message: MaterialLocalizations.of(context).backButtonTooltip, message: tooltip,
child: DecoratedBox( child: DecoratedBox(
key: const Key('subpage-back-shadow'), key: const Key('subpage-back-shadow'),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -67,10 +71,10 @@ class AppSubpageBackButton extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
customBorder: const CircleBorder(), customBorder: const CircleBorder(),
onTap: onBack ?? () => Navigator.of(context).maybePop(), onTap: onPressed,
child: Center( child: Center(
child: Icon( child: Icon(
Icons.chevron_left_rounded, icon,
color: dark ? Colors.white : AppColors.onSurface, color: dark ? Colors.white : AppColors.onSurface,
size: _iconSize, size: _iconSize,
weight: _iconWeight, weight: _iconWeight,
@@ -84,6 +88,31 @@ class AppSubpageBackButton extends StatelessWidget {
} }
} }
/// 二级页面统一返回按钮,浅色页使用白底描边,深色页使用半透明黑底。
class AppSubpageBackButton extends StatelessWidget {
const AppSubpageBackButton({
super.key,
required this.backKey,
this.dark = false,
this.onBack,
});
final Key backKey;
final bool dark;
final VoidCallback? onBack;
@override
Widget build(BuildContext context) {
return AppSubpageCircleButton(
buttonKey: backKey,
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: Icons.chevron_left_rounded,
dark: dark,
onPressed: onBack ?? () => Navigator.of(context).maybePop(),
);
}
}
/// 二级页面统一页头:返回按钮、左对齐标题、可选副标题与右侧操作。 /// 二级页面统一页头:返回按钮、左对齐标题、可选副标题与右侧操作。
class AppSubpageHeader extends StatelessWidget { class AppSubpageHeader extends StatelessWidget {
const AppSubpageHeader({ const AppSubpageHeader({
+40 -14
View File
@@ -34,8 +34,10 @@ const String _menuDownload = 'download';
const String _menuShare = 'share'; const String _menuShare = 'share';
const String _menuReport = 'report'; const String _menuReport = 'report';
const int _maxTags = 4; const int _maxTags = 4;
const double _moreSize = 36; const double _stateHeaderHeight = 52;
const double _moreIconSize = 18; const double _stateHeaderHorizontal = 14;
const double _stateTitleGap = 12;
const double _stateTitleSize = 18;
final RegExp _tagSplitter = RegExp(r'[,/、·]'); final RegExp _tagSplitter = RegExp(r'[,/、·]');
@@ -189,7 +191,35 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
AppSubpageHeader(title: title, backKey: const Key('detail-back')), SizedBox(
height: _stateHeaderHeight,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: _stateHeaderHorizontal,
),
child: Row(
children: [
const AppSubpageBackButton(
backKey: Key('detail-back'),
dark: true,
),
const SizedBox(width: _stateTitleGap),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: _stateTitleSize,
fontWeight: FontWeight.w800,
),
),
),
],
),
),
),
Expanded(child: child), Expanded(child: child),
], ],
), ),
@@ -285,17 +315,13 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
), ),
), ),
], ],
child: Container( child: IgnorePointer(
width: _moreSize, child: AppSubpageCircleButton(
height: _moreSize, buttonKey: const Key('detail-more-visual'),
decoration: const BoxDecoration( tooltip: l10n.more,
color: AppColors.card, icon: Icons.more_horiz_rounded,
shape: BoxShape.circle, dark: true,
), onPressed: () {},
child: const Icon(
Icons.more_horiz_rounded,
size: _moreIconSize,
color: AppColors.onSurface,
), ),
), ),
); );
@@ -18,8 +18,6 @@ const double _infoGap = 14;
const double _favoriteHeight = 34; const double _favoriteHeight = 34;
const double _favoriteRadius = 17; const double _favoriteRadius = 17;
const double _favoriteHorizontal = 12; const double _favoriteHorizontal = 12;
const double _circleSize = 36;
const double _iconSize = 18;
/// 详情页顶部操作与短剧主信息所需的交互回调。 /// 详情页顶部操作与短剧主信息所需的交互回调。
class DramaDetailHeaderActions { class DramaDetailHeaderActions {
@@ -87,10 +85,11 @@ class DramaDetailHeader extends StatelessWidget {
onBack: actions.onBack, onBack: actions.onBack,
), ),
const Spacer(), const Spacer(),
_CircleAction( AppSubpageCircleButton(
actionKey: const Key('detail-share'), buttonKey: const Key('detail-share'),
tooltip: l10n.share, tooltip: l10n.share,
icon: Icons.share_outlined, icon: Icons.share_outlined,
dark: true,
onPressed: actions.onShare, onPressed: actions.onShare,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -201,36 +200,3 @@ class DramaDetailHeader extends StatelessWidget {
); );
} }
} }
class _CircleAction extends StatelessWidget {
const _CircleAction({
required this.actionKey,
required this.tooltip,
required this.icon,
required this.onPressed,
});
final Key actionKey;
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Container(
key: actionKey,
width: _circleSize,
height: _circleSize,
decoration: const BoxDecoration(
color: AppColors.card,
shape: BoxShape.circle,
),
child: IconButton(
tooltip: tooltip,
onPressed: onPressed,
padding: EdgeInsets.zero,
icon: Icon(icon, size: _iconSize, color: AppColors.onSurface),
),
);
}
}
@@ -94,6 +94,20 @@ void main() {
expect(tester.getSize(find.byKey(const Key('detail-cta'))).height, 46); expect(tester.getSize(find.byKey(const Key('detail-cta'))).height, 46);
expect(find.byKey(const Key('detail-share')), findsOneWidget); expect(find.byKey(const Key('detail-share')), findsOneWidget);
expect(find.byKey(const Key('detail-more')), findsOneWidget); expect(find.byKey(const Key('detail-more')), findsOneWidget);
for (final key in const [Key('detail-share'), Key('detail-more')]) {
expect(tester.getSize(find.byKey(key)), const Size(40, 40));
expect(
tester
.widget<Material>(
find.descendant(
of: find.byKey(key),
matching: find.byType(Material),
),
)
.color,
const Color(0x59000000),
);
}
await tester.tap(find.byKey(const Key('detail-more'))); await tester.tap(find.byKey(const Key('detail-more')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();