Skip to content

Commit

Permalink
Add CupertinoSliverNavigationBar large title magnification on over …
Browse files Browse the repository at this point in the history
…scroll (flutter#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
  • Loading branch information
ivirtex committed Dec 8, 2022
1 parent 2ffc5bc commit ef40e3e
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 34 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -100,3 +100,4 @@ Jingyi Chen <jingyichen@link.cuhk.edu.cn>
Junhua Lin <1075209054@qq.com>
Tomasz Gucio <tgucio@gmail.com>
Jason C.H <ctrysbita@outlook.com>
Hubert Jóźwiak <hjozwiakdx@gmail.com>
Expand Up @@ -20,7 +20,7 @@ void main() {
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 {
Expand All @@ -43,7 +43,7 @@ void main() {

// 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 {
Expand Down
165 changes: 140 additions & 25 deletions packages/flutter/lib/src/cupertino/nav_bar.dart
Expand Up @@ -33,6 +33,8 @@ const double _kNavBarShowLargeTitleThreshold = 10.0;

const double _kNavBarEdgePadding = 16.0;

const double _kNavBarBottomPadding = 8.0;

const double _kNavBarBackButtonTapWidth = 50.0;

/// Title text transfer fade.
Expand Down Expand Up @@ -833,31 +835,27 @@ class _LargeTitleNavigationBarSliverDelegate
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,
),
),
),
Expand Down Expand Up @@ -921,6 +919,123 @@ class _LargeTitleNavigationBarSliverDelegate
}
}

/// 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
Expand Down

0 comments on commit ef40e3e

Please sign in to comment.