From 2ef2beb621fa1d6038d9e8fcf857e60e2c585f0b Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:58:14 +0700 Subject: [PATCH] fix(ui): resolve collapsed header title overlap --- lib/widgets/app_sliver_header.dart | 19 +++++++++--- test/design_system_test.dart | 46 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/lib/widgets/app_sliver_header.dart b/lib/widgets/app_sliver_header.dart index 1e4ace05..cc411f10 100644 --- a/lib/widgets/app_sliver_header.dart +++ b/lib/widgets/app_sliver_header.dart @@ -44,7 +44,6 @@ class AppSliverHeader extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = normalizedHeaderTopPadding(context); final maxHeight = tokens.headerExpandedHeight + topPadding; - final minHeight = kToolbarHeight + topPadding; final edgeInset = detailHeaderEdgeInset(context); return SliverAppBar( @@ -74,9 +73,21 @@ class AppSliverHeader extends StatelessWidget { actions: actions, flexibleSpace: LayoutBuilder( builder: (context, constraints) { - final expandRatio = - ((constraints.maxHeight - minHeight) / (maxHeight - minHeight)) - .clamp(0.0, 1.0); + // SliverAppBar adds the platform's actual top safe-area inset to its + // extents. Deriving the collapse ratio from our requested heights + // left notched iPhones looking almost fully expanded even after the + // toolbar had collapsed, so the large title occupied the back + // button's space. Read the resolved sliver geometry instead. + final settings = context + .dependOnInheritedWidgetOfExactType(); + final minExtent = settings?.minExtent ?? kToolbarHeight; + final maxExtent = settings?.maxExtent ?? maxHeight; + final currentExtent = + settings?.currentExtent ?? constraints.maxHeight; + final extentDelta = maxExtent - minExtent; + final expandRatio = extentDelta > 0 + ? ((currentExtent - minExtent) / extentDelta).clamp(0.0, 1.0) + : 0.0; final leftPadding = _showLeading ? (_leadingClearance + edgeInset) - (((_leadingClearance + edgeInset) - _contentMargin) * diff --git a/test/design_system_test.dart b/test/design_system_test.dart index 56f51bf6..7adb75a4 100644 --- a/test/design_system_test.dart +++ b/test/design_system_test.dart @@ -341,6 +341,52 @@ void main() { expect(style.fontSize, AppTokens.standard.headerExpandedTitleSize); }); + for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) { + testWidgets( + 'collapsed page title clears leading control on ${platform.name}', + (tester) async { + final controller = ScrollController(); + addTearDown(controller.dispose); + final topInset = platform == TargetPlatform.iOS ? 59.0 : 24.0; + + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.light().copyWith(platform: platform), + home: MediaQuery( + data: MediaQueryData( + size: const Size(430, 932), + padding: EdgeInsets.only(top: topInset), + ), + child: Scaffold( + body: CustomScrollView( + controller: controller, + slivers: const [ + AppSliverHeader.page(title: 'Metadata'), + SliverToBoxAdapter(child: SizedBox(height: 1200)), + ], + ), + ), + ), + ), + ); + + controller.jumpTo(AppTokens.standard.headerExpandedHeight); + await tester.pump(); + + final title = find.text('Metadata'); + final backIcon = find.byIcon(Icons.arrow_back); + expect( + tester.getRect(title).overlaps(tester.getRect(backIcon)), + isFalse, + ); + expect( + tester.widget(title).style?.fontSize, + AppTokens.standard.headerCollapsedTitleSize, + ); + }, + ); + } + test('is the only collapsing header implementation left', () { final offenders = _libSources() .where(