refactor(ui): unify sliver page headers

This commit is contained in:
zarzet
2026-07-29 17:10:24 +07:00
parent c6f7b22e43
commit fa0c7b20b5
13 changed files with 130 additions and 95 deletions
+108
View File
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
/// The collapsing header used by every top-level tab and every settings-style
/// sub-page.
///
/// This replaces five hand-rolled copies of the same `SliverAppBar` +
/// `LayoutBuilder` + `FlexibleSpaceBar` block. Those copies had drifted into two
/// type ramps (tab roots expanded the title to 34pt, sub-pages to 28pt); both
/// now expand to [AppTokens.headerExpandedTitleSize], which matches the
/// Material 3 large top app bar headline.
class AppSliverHeader extends StatelessWidget {
/// Root of a navigation tab: no back button, and the title stays aligned with
/// the content margin at every collapse ratio.
const AppSliverHeader.tabRoot({super.key, required this.title, this.actions})
: leading = null,
_showLeading = false;
/// A pushed page: shows [leading] (a back button by default) and slides the
/// title clear of it as the header collapses.
const AppSliverHeader.page({
super.key,
required this.title,
this.actions,
this.leading,
}) : _showLeading = true;
final String title;
final List<Widget>? actions;
/// Overrides the default back button. Only used by the page variant.
final Widget? leading;
final bool _showLeading;
/// Left inset of the collapsed title when a leading button is present: enough
/// to clear a 48dp icon button plus the 8dp toolbar margin.
static const double _leadingClearance = 56;
/// Content margin the expanded title aligns to.
static const double _contentMargin = 24;
@override
Widget build(BuildContext context) {
final tokens = context.tokens;
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
final maxHeight = tokens.headerExpandedHeight + topPadding;
final minHeight = kToolbarHeight + topPadding;
return SliverAppBar(
expandedHeight: maxHeight,
collapsedHeight: kToolbarHeight,
floating: false,
pinned: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
automaticallyImplyLeading: false,
leading: _showLeading
? leading ??
IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
)
: null,
actions: actions,
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final expandRatio =
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
.clamp(0.0, 1.0);
final leftPadding = _showLeading
? _leadingClearance -
((_leadingClearance - _contentMargin) * expandRatio)
: _contentMargin;
final fontSize =
tokens.headerCollapsedTitleSize +
(tokens.headerExpandedTitleSize -
tokens.headerCollapsedTitleSize) *
expandRatio;
return FlexibleSpaceBar(
expandedTitleScale: 1.0,
titlePadding: EdgeInsets.only(
left: leftPadding,
right: tokens.gapLg,
bottom: tokens.gapLg,
),
title: Text(
title,
// The title grows as the header expands; without a cap a long
// localized title overflowed instead of ellipsizing.
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
);
},
),
);
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/widgets/settings_sliver_app_bar.dart';
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
class PrioritySettingsScaffold extends StatelessWidget {
final bool hasChanges;
@@ -55,7 +55,7 @@ class PrioritySettingsScaffold extends StatelessWidget {
child: Scaffold(
body: CustomScrollView(
slivers: [
SettingsSliverAppBar(
AppSliverHeader.page(
title: title,
leading: IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
-64
View File
@@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/utils/app_bar_layout.dart';
/// The collapsing header shared by settings-style pages: a pinned
/// [SliverAppBar] whose title slides toward the leading edge and shrinks
/// from 28 to 20 logical pixels as the header collapses.
class SettingsSliverAppBar extends StatelessWidget {
const SettingsSliverAppBar({
super.key,
required this.title,
this.actions,
this.leading,
});
final String title;
final List<Widget>? actions;
/// Defaults to a back button popping the current route.
final Widget? leading;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final topPadding = normalizedHeaderTopPadding(context);
return SliverAppBar(
expandedHeight: 120 + topPadding,
collapsedHeight: kToolbarHeight,
floating: false,
pinned: true,
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
leading:
leading ??
IconButton(
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: actions,
flexibleSpace: LayoutBuilder(
builder: (context, constraints) {
final maxHeight = 120 + topPadding;
final minHeight = kToolbarHeight + topPadding;
final expandRatio =
((constraints.maxHeight - minHeight) / (maxHeight - minHeight))
.clamp(0.0, 1.0);
final leftPadding = 56 - (32 * expandRatio);
return FlexibleSpaceBar(
expandedTitleScale: 1.0,
titlePadding: EdgeInsets.only(left: leftPadding, bottom: 16),
title: Text(
title,
style: TextStyle(
fontSize: 20 + (8 * expandRatio),
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
);
},
),
);
}
}