Skip to content

Commit ef40e3e

Browse files
authored
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
1 parent 2ffc5bc commit ef40e3e

File tree

4 files changed

+294
-34
lines changed

4 files changed

+294
-34
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,4 @@ Jingyi Chen <[email protected]>
100100
Junhua Lin <[email protected]>
101101
Tomasz Gucio <[email protected]>
102102
Jason C.H <[email protected]>
103+
Hubert Jóźwiak <[email protected]>

examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.0_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ void main() {
2020
await tester.pumpAndSettle();
2121

2222
// Large title is hidden and at higher position.
23-
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0);
23+
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
2424
});
2525

2626
testWidgets('Middle widget is visible in both collapsed and expanded states', (WidgetTester tester) async {
@@ -43,7 +43,7 @@ void main() {
4343

4444
// Large title is hidden and middle title is visible.
4545
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
46-
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0);
46+
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
4747
});
4848

4949
testWidgets('CupertinoSliverNavigationBar with previous route has back button', (WidgetTester tester) async {

packages/flutter/lib/src/cupertino/nav_bar.dart

Lines changed: 140 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const double _kNavBarShowLargeTitleThreshold = 10.0;
3333

3434
const double _kNavBarEdgePadding = 16.0;
3535

36+
const double _kNavBarBottomPadding = 8.0;
37+
3638
const double _kNavBarBackButtonTapWidth = 50.0;
3739

3840
/// Title text transfer fade.
@@ -833,31 +835,27 @@ class _LargeTitleNavigationBarSliverDelegate
833835
right: 0.0,
834836
bottom: 0.0,
835837
child: ClipRect(
836-
// The large title starts at the persistent bar.
837-
// It's aligned with the bottom of the sliver and expands clipped
838-
// and behind the persistent bar.
839-
child: OverflowBox(
840-
minHeight: 0.0,
841-
maxHeight: double.infinity,
842-
alignment: AlignmentDirectional.bottomStart,
843-
child: Padding(
844-
padding: const EdgeInsetsDirectional.only(
845-
start: _kNavBarEdgePadding,
846-
bottom: 8.0, // Bottom has a different padding.
847-
),
848-
child: SafeArea(
849-
top: false,
850-
bottom: false,
851-
child: AnimatedOpacity(
852-
opacity: showLargeTitle ? 1.0 : 0.0,
853-
duration: _kNavBarTitleFadeDuration,
854-
child: Semantics(
855-
header: true,
856-
child: DefaultTextStyle(
857-
style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
858-
maxLines: 1,
859-
overflow: TextOverflow.ellipsis,
860-
child: components.largeTitle!,
838+
child: Padding(
839+
padding: const EdgeInsetsDirectional.only(
840+
start: _kNavBarEdgePadding,
841+
bottom: _kNavBarBottomPadding
842+
),
843+
child: SafeArea(
844+
top: false,
845+
bottom: false,
846+
child: AnimatedOpacity(
847+
opacity: showLargeTitle ? 1.0 : 0.0,
848+
duration: _kNavBarTitleFadeDuration,
849+
child: Semantics(
850+
header: true,
851+
child: DefaultTextStyle(
852+
style: CupertinoTheme.of(context)
853+
.textTheme
854+
.navLargeTitleTextStyle,
855+
maxLines: 1,
856+
overflow: TextOverflow.ellipsis,
857+
child: _LargeTitle(
858+
child: components.largeTitle,
861859
),
862860
),
863861
),
@@ -921,6 +919,123 @@ class _LargeTitleNavigationBarSliverDelegate
921919
}
922920
}
923921

922+
/// The large title of the navigation bar.
923+
///
924+
/// Magnifies on over-scroll when [CupertinoSliverNavigationBar.stretch]
925+
/// parameter is true.
926+
class _LargeTitle extends SingleChildRenderObjectWidget {
927+
const _LargeTitle({ super.child });
928+
929+
@override
930+
_RenderLargeTitle createRenderObject(BuildContext context) {
931+
return _RenderLargeTitle(alignment: AlignmentDirectional.bottomStart.resolve(Directionality.of(context)));
932+
}
933+
934+
@override
935+
void updateRenderObject(BuildContext context, _RenderLargeTitle renderObject) {
936+
renderObject.alignment = AlignmentDirectional.bottomStart.resolve(Directionality.of(context));
937+
}
938+
}
939+
940+
class _RenderLargeTitle extends RenderShiftedBox {
941+
_RenderLargeTitle({
942+
required Alignment alignment,
943+
}) : _alignment = alignment,
944+
super(null);
945+
946+
Alignment get alignment => _alignment;
947+
Alignment _alignment;
948+
set alignment(Alignment value) {
949+
if (_alignment == value) {
950+
return;
951+
}
952+
_alignment = value;
953+
954+
markNeedsLayout();
955+
}
956+
957+
double _scale = 1.0;
958+
959+
@override
960+
void performLayout() {
961+
final RenderBox? child = this.child;
962+
Size childSize = Size.zero;
963+
964+
size = constraints.biggest;
965+
966+
if (child == null) {
967+
return;
968+
}
969+
970+
final BoxConstraints childConstriants = constraints.widthConstraints().loosen();
971+
child.layout(childConstriants, parentUsesSize: true);
972+
973+
final double maxScale = child.size.width != 0.0
974+
? clampDouble(constraints.maxWidth / child.size.width, 1.0, 1.1)
975+
: 1.1;
976+
_scale = clampDouble(
977+
1.0 + (constraints.maxHeight - (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding)) / (_kNavBarLargeTitleHeightExtension - _kNavBarBottomPadding) * 0.03,
978+
1.0,
979+
maxScale,
980+
);
981+
982+
childSize = child.size * _scale;
983+
final BoxParentData childParentData = child.parentData! as BoxParentData;
984+
childParentData.offset = alignment.alongOffset(size - childSize as Offset);
985+
}
986+
987+
@override
988+
void applyPaintTransform(RenderBox child, Matrix4 transform) {
989+
assert(child == this.child);
990+
991+
super.applyPaintTransform(child, transform);
992+
993+
transform.scale(_scale, _scale);
994+
}
995+
996+
@override
997+
void paint(PaintingContext context, Offset offset) {
998+
final RenderBox? child = this.child;
999+
1000+
if (child == null) {
1001+
layer = null;
1002+
} else {
1003+
final BoxParentData childParentData = child.parentData! as BoxParentData;
1004+
1005+
layer = context.pushTransform(
1006+
needsCompositing,
1007+
offset + childParentData.offset,
1008+
Matrix4.diagonal3Values(_scale, _scale, 1.0),
1009+
(PaintingContext context, Offset offset) => context.paintChild(child, offset),
1010+
oldLayer: layer as TransformLayer?,
1011+
);
1012+
}
1013+
}
1014+
1015+
@override
1016+
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
1017+
final RenderBox? child = this.child;
1018+
1019+
if (child == null) {
1020+
return false;
1021+
}
1022+
1023+
final Offset childOffset = (child.parentData! as BoxParentData).offset;
1024+
1025+
final Matrix4 transform = Matrix4.identity()
1026+
..scale(1.0/_scale, 1.0/_scale, 1.0)
1027+
..translate(-childOffset.dx, -childOffset.dy);
1028+
1029+
return result.addWithRawTransform(
1030+
transform: transform,
1031+
position: position,
1032+
hitTest: (BoxHitTestResult result, Offset transformed) {
1033+
return child.hitTest(result, position: transformed);
1034+
}
1035+
);
1036+
}
1037+
}
1038+
9241039
/// The top part of the navigation bar that's never scrolled away.
9251040
///
9261041
/// Consists of the entire navigation bar without background and border when used

0 commit comments

Comments
 (0)