Add `CupertinoSliverNavigationBar` large title magnification on over scroll (#110127)

* Add magnification of CupertinoSliverNavigationBar large title

* Fix padding in maximum scale computation

* Apply magnification by using RenderBox

* Do not pass key to the superclass constructor

* Use `clampDouble` instead of `clamp` extension method

* Remove trailing whitespaces to make linter happy

* Name test variables more precisely

* Move transform computation to `performLayout` and implement `hitTestChildren`

* Address comments

* Address comments

* Address comments

* Update comment about scale

* Fix hit-testing

* Fix hit-testing again

* Make linter happy

* Implement magnifying without using LayoutBuilder

* Remove trailing spaces

* Add hit-testing of the large title

* Remove whitespaces

* Fix scale computation and some tests

* Fix remaining tests

* Refactor and fix comments

* Update comments
diff --git a/AUTHORS b/AUTHORS
index 33f5781..1911504 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -100,3 +100,4 @@
 Junhua Lin <1075209054@qq.com>
 Tomasz Gucio <tgucio@gmail.com>
 Jason C.H <ctrysbita@outlook.com>
+Hubert Jóźwiak <hjozwiakdx@gmail.com>
\ No newline at end of file
diff --git a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart b/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart
index b839f34..208e763 100644
--- a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart
+++ b/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart
@@ -20,7 +20,7 @@
     await tester.pumpAndSettle();
 
     // Large title is hidden and at higher position.
-    expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0);
+    expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
   });
 
   testWidgets('Middle widget is visible in both collapsed and expanded states', (WidgetTester tester) async {
@@ -43,7 +43,7 @@
 
     // Large title is hidden and middle title is visible.
     expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
-    expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0);
+    expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
   });
 
   testWidgets('CupertinoSliverNavigationBar with previous route has back button', (WidgetTester tester) async {
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index 8261bd9..d7cfcff 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -33,6 +33,8 @@
 
 const double _kNavBarEdgePadding = 16.0;
 
+const double _kNavBarBottomPadding = 8.0;
+
 const double _kNavBarBackButtonTapWidth = 50.0;
 
 /// Title text transfer fade.
@@ -833,31 +835,27 @@
               right: 0.0,
               bottom: 0.0,
               child: ClipRect(
-                // The large title starts at the persistent bar.
-                // It's aligned with the bottom of the sliver and expands clipped
-                // and behind the persistent bar.
-                child: OverflowBox(
-                  minHeight: 0.0,
-                  maxHeight: double.infinity,
-                  alignment: AlignmentDirectional.bottomStart,
-                  child: Padding(
-                    padding: const EdgeInsetsDirectional.only(
-                      start: _kNavBarEdgePadding,
-                      bottom: 8.0, // Bottom has a different padding.
-                    ),
-                    child: SafeArea(
-                      top: false,
-                      bottom: false,
-                      child: AnimatedOpacity(
-                        opacity: showLargeTitle ? 1.0 : 0.0,
-                        duration: _kNavBarTitleFadeDuration,
-                        child: Semantics(
-                          header: true,
-                          child: DefaultTextStyle(
-                            style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
-                            maxLines: 1,
-                            overflow: TextOverflow.ellipsis,
-                            child: components.largeTitle!,
+                child: Padding(
+                  padding: const EdgeInsetsDirectional.only(
+                    start: _kNavBarEdgePadding,
+                    bottom: _kNavBarBottomPadding
+                  ),
+                  child: SafeArea(
+                    top: false,
+                    bottom: false,
+                    child: AnimatedOpacity(
+                      opacity: showLargeTitle ? 1.0 : 0.0,
+                      duration: _kNavBarTitleFadeDuration,
+                      child: Semantics(
+                        header: true,
+                        child: DefaultTextStyle(
+                          style: CupertinoTheme.of(context)
+                              .textTheme
+                              .navLargeTitleTextStyle,
+                          maxLines: 1,
+                          overflow: TextOverflow.ellipsis,
+                          child: _LargeTitle(
+                            child: components.largeTitle,
                           ),
                         ),
                       ),
@@ -921,6 +919,123 @@
   }
 }
 
+/// The large title of the navigation bar.
+///
+/// Magnifies on over-scroll when [CupertinoSliverNavigationBar.stretch]
+/// parameter is true.
+class _LargeTitle extends SingleChildRenderObjectWidget {
+  const _LargeTitle({ super.child });
+
+  @override
+  _RenderLargeTitle createRenderObject(BuildContext context) {
+    return _RenderLargeTitle(alignment: AlignmentDirectional.bottomStart.resolve(Directionality.of(context)));
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderLargeTitle renderObject) {
+    renderObject.alignment = AlignmentDirectional.bottomStart.resolve(Directionality.of(context));
+  }
+}
+
+class _RenderLargeTitle extends RenderShiftedBox {
+  _RenderLargeTitle({
+    required Alignment alignment,
+  })  : _alignment = alignment,
+        super(null);
+
+  Alignment get alignment => _alignment;
+  Alignment _alignment;
+  set alignment(Alignment value) {
+    if (_alignment == value) {
+      return;
+    }
+    _alignment = value;
+
+    markNeedsLayout();
+  }
+
+  double _scale = 1.0;
+
+  @override
+  void performLayout() {
+    final RenderBox? child = this.child;
+    Size childSize = Size.zero;
+
+    size = constraints.biggest;
+
+    if (child == null) {
+      return;
+    }
+
+    final BoxConstraints childConstriants = constraints.widthConstraints().loosen();
+    child.layout(childConstriants, parentUsesSize: true);
+
+    final double maxScale = child.size.width != 0.0
+      ? clampDouble(constraints.maxWidth / child.size.width, 1.0, 1.1)
+      : 1.1;
+    _scale = clampDouble(
+      1.0 + (constraints.maxHeight - (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding)) / (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding) * 0.03,
+      1.0,
+      maxScale,
+    );
+
+    childSize = child.size * _scale;
+    final BoxParentData childParentData = child.parentData! as BoxParentData;
+    childParentData.offset = alignment.alongOffset(size - childSize as Offset);
+  }
+
+  @override
+  void applyPaintTransform(RenderBox child, Matrix4 transform) {
+    assert(child == this.child);
+
+    super.applyPaintTransform(child, transform);
+
+    transform.scale(_scale, _scale);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    final RenderBox? child = this.child;
+
+    if (child == null) {
+      layer = null;
+    } else {
+      final BoxParentData childParentData = child.parentData! as BoxParentData;
+
+      layer = context.pushTransform(
+        needsCompositing,
+        offset + childParentData.offset,
+        Matrix4.diagonal3Values(_scale, _scale, 1.0),
+        (PaintingContext context, Offset offset) => context.paintChild(child, offset),
+        oldLayer: layer as TransformLayer?,
+      );
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+    final RenderBox? child = this.child;
+
+    if (child == null) {
+      return false;
+    }
+
+    final Offset childOffset = (child.parentData! as BoxParentData).offset;
+
+    final Matrix4 transform = Matrix4.identity()
+      ..scale(1.0/_scale, 1.0/_scale, 1.0)
+      ..translate(-childOffset.dx, -childOffset.dy);
+
+    return result.addWithRawTransform(
+      transform: transform,
+      position: position,
+      hitTest: (BoxHitTestResult result, Offset transformed) {
+        return child.hitTest(result, position: transformed);
+      }
+    );
+  }
+}
+
 /// The top part of the navigation bar that's never scrolled away.
 ///
 /// Consists of the entire navigation bar without background and border when used
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart
index 1725a20..269e45b 100644
--- a/packages/flutter/test/cupertino/nav_bar_test.dart
+++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -441,8 +441,8 @@
       1.0, // The larger font title is visible.
     ]);
 
-    expect(tester.getTopLeft(find.widgetWithText(OverflowBox, 'Title')).dy, 44.0);
-    expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 52.0);
+    expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
+    expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 52.0);
 
     scrollController.jumpTo(600.0);
     await tester.pump(); // Once to trigger the opacity animation.
@@ -470,9 +470,9 @@
     expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
     expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
 
-    expect(tester.getTopLeft(find.widgetWithText(OverflowBox, 'Title')).dy, 44.0);
+    expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
     // The OverflowBox is squished with the text in it.
-    expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 0.0);
+    expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 0.0);
   });
 
   testWidgets('User specified middle is always visible in sliver', (WidgetTester tester) async {
@@ -517,8 +517,8 @@
     expect(find.text('Title'), findsOneWidget);
     expect(tester.getCenter(find.byKey(segmentedControlsKey)).dx, 400.0);
 
-    expect(tester.getTopLeft(find.widgetWithText(OverflowBox, 'Title')).dy, 44.0);
-    expect(tester.getSize(find.widgetWithText(OverflowBox, 'Title')).height, 52.0);
+    expect(tester.getTopLeft(find.widgetWithText(ClipRect, 'Title').first).dy, 44.0);
+    expect(tester.getSize(find.widgetWithText(ClipRect, 'Title').first).height, 52.0);
 
     scrollController.jumpTo(600.0);
     await tester.pump(); // Once to trigger the opacity animation.
@@ -639,7 +639,7 @@
     expect(tester.getTopLeft(find.byType(NavigationToolbar)).dy, 0.0);
     expect(tester.getSize(find.byType(NavigationToolbar)).height, 44.0);
 
-    expect(tester.getBottomLeft(find.text('Title')).dy, 44.0 - 8.0); // Extension gone, (static part - padding) left.
+    expect(tester.getBottomLeft(find.text('Title')).dy, 44.0); // Extension gone.
   });
 
   testWidgets('Auto back/close button', (WidgetTester tester) async {
@@ -1405,6 +1405,150 @@
     expect(find.text('Page 1'), findsOneWidget);
     expect(find.text('Page 2'), findsNothing);
   });
+
+  testWidgets(
+    'CupertinoSliverNavigationBar magnifies upon over-scroll and shrinks back once over-scroll ends',
+    (WidgetTester tester) async {
+      const Text titleText = Text('Large Title');
+
+      await tester.pumpWidget(
+        CupertinoApp(
+          home: CupertinoPageScaffold(
+            child: CustomScrollView(
+              slivers: <Widget>[
+                const CupertinoSliverNavigationBar(
+                  largeTitle: titleText,
+                  stretch: true,
+                ),
+                SliverToBoxAdapter(
+                  child: Container(
+                    height: 1200.0,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      final Finder titleTextFinder = find.byWidget(titleText).first;
+
+      // Gets the height of the large title
+      final Offset initialLargeTitleTextOffset =
+          tester.getBottomLeft(titleTextFinder) -
+              tester.getTopLeft(titleTextFinder);
+
+      // Drag for overscroll
+      await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
+      await tester.pump();
+
+      final Offset magnifiedTitleTextOffset =
+          tester.getBottomLeft(titleTextFinder) -
+              tester.getTopLeft(titleTextFinder);
+
+      expect(
+        magnifiedTitleTextOffset.dy.abs(),
+        greaterThan(initialLargeTitleTextOffset.dy.abs()),
+      );
+
+      // Ensure title text retracts to original size after releasing gesture
+      await tester.pumpAndSettle();
+
+      final Offset finalTitleTextOffset = tester.getBottomLeft(titleTextFinder) -
+          tester.getTopLeft(titleTextFinder);
+
+      expect(
+        finalTitleTextOffset.dy.abs(),
+        initialLargeTitleTextOffset.dy.abs(),
+      );
+    },
+  );
+
+  testWidgets(
+    'CupertinoSliverNavigationBar large title text does not get clipped when magnified',
+    (WidgetTester tester) async {
+      const Text titleText = Text('Very very very long large title');
+
+      await tester.pumpWidget(
+        CupertinoApp(
+          home: CupertinoPageScaffold(
+            child: CustomScrollView(
+              slivers: <Widget>[
+                const CupertinoSliverNavigationBar(
+                  largeTitle: titleText,
+                  stretch: true,
+                ),
+                SliverToBoxAdapter(
+                  child: Container(
+                    height: 1200.0,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      final Finder titleTextFinder = find.byWidget(titleText).first;
+
+      // Gets the width of the large title
+      final Offset initialLargeTitleTextOffset =
+          tester.getBottomLeft(titleTextFinder) -
+              tester.getBottomRight(titleTextFinder);
+
+      // Drag for overscroll
+      await tester.drag(find.byType(Scrollable), const Offset(0.0, 150.0));
+      await tester.pump();
+
+      final Offset magnifiedTitleTextOffset =
+          tester.getBottomLeft(titleTextFinder) -
+              tester.getBottomRight(titleTextFinder);
+
+      expect(
+        magnifiedTitleTextOffset.dx.abs(),
+        equals(initialLargeTitleTextOffset.dx.abs()),
+      );
+    },
+  );
+
+  testWidgets(
+    'CupertinoSliverNavigationBar large title can be hit tested when magnified',
+    (WidgetTester tester) async {
+      final ScrollController scrollController = ScrollController();
+
+      await tester.pumpWidget(
+        CupertinoApp(
+          home: CupertinoPageScaffold(
+            child: CustomScrollView(
+              controller: scrollController,
+              slivers: <Widget>[
+                const CupertinoSliverNavigationBar(
+                  largeTitle: Text('Large title'),
+                  stretch: true,
+                ),
+                SliverToBoxAdapter(
+                  child: Container(
+                    height: 1200.0,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      final Finder largeTitleFinder = find.text('Large title').first;
+
+      // Drag for overscroll
+      await tester.drag(find.byType(Scrollable), const Offset(0.0, 250.0));
+
+      // Hold position of the scroll view, so the Scrollable unblocks the hit-testing
+      scrollController.position.hold(() {});
+      await tester.pumpAndSettle();
+
+      expect(largeTitleFinder.hitTestable(), findsOneWidget);
+    },
+  );
 }
 
 class _ExpectStyles extends StatelessWidget {