fix(flutter): 统一详情页顶部圆形按钮
This commit is contained in:
1 parent
230f16a44a
commit
c1dd033581
4 files changed
+97
-62
No files matched your search
@@ -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({
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user