Skip to content

Commit 3a4fb9d

Browse files
authored
Fix search view position (#124169)
1 parent 90f8ac5 commit 3a4fb9d

2 files changed

Lines changed: 203 additions & 16 deletions

File tree

packages/flutter/lib/src/material/search_anchor.dart

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class SearchAnchor extends StatefulWidget {
148148
TextStyle? viewHeaderHintStyle,
149149
Color? dividerColor,
150150
BoxConstraints? constraints,
151+
BoxConstraints? viewConstraints,
151152
bool? isFullScreen,
152153
SearchController searchController,
153154
required SuggestionsBuilder suggestionsBuilder
@@ -254,6 +255,11 @@ class SearchAnchor extends StatefulWidget {
254255

255256
/// Optional size constraints for the search view.
256257
///
258+
/// By default, the search view has the same width as the anchor and is 2/3
259+
/// the height of the screen. If the width and height of the view are within
260+
/// the [viewConstraints], the view will show its default size. Otherwise,
261+
/// the size of the view will be constrained by this property.
262+
///
257263
/// If null, the value of [SearchViewThemeData.constraints] will be used. If
258264
/// this is also null, then the constraints defaults to:
259265
/// ```dart
@@ -477,24 +483,43 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
477483
final Size screenSize = MediaQuery.of(context).size;
478484
final Rect anchorRect = getRect() ?? Rect.zero;
479485

480-
// Check if the search view goes off the screen.
481486
final BoxConstraints effectiveConstraints = viewConstraints ?? viewTheme.constraints ?? viewDefaults.constraints!;
482-
final double verticalDistanceToEdge = screenSize.height - anchorRect.top;
483-
final double endHeight = math.max(effectiveConstraints.minHeight, math.min(screenSize.height * 2 / 3, verticalDistanceToEdge));
484487
_rectTween.begin = anchorRect;
485488

489+
final double viewWidth = clampDouble(anchorRect.width, effectiveConstraints.minWidth, effectiveConstraints.maxWidth);
490+
final double viewHeight = clampDouble(screenSize.height * 2 / 3, effectiveConstraints.minHeight, effectiveConstraints.maxHeight);
491+
486492
switch (textDirection ?? TextDirection.ltr) {
487493
case TextDirection.ltr:
488-
final double viewEdgeToScreenEdge = screenSize.width - anchorRect.left;
489-
final double endWidth = math.max(effectiveConstraints.minWidth, math.min(anchorRect.width, viewEdgeToScreenEdge));
490-
final Size endSize = Size(endWidth, endHeight);
491-
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (anchorRect.topLeft & endSize);
494+
final double viewLeftToScreenRight = screenSize.width - anchorRect.left;
495+
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
496+
497+
// Make sure the search view doesn't go off the screen. If the search view
498+
// doesn't fit, move the top-left corner of the view to fit the window.
499+
// If the window is smaller than the view, then we resize the view to fit the window.
500+
Offset topLeft = anchorRect.topLeft;
501+
if (viewLeftToScreenRight < viewWidth) {
502+
topLeft = Offset(screenSize.width - math.min(viewWidth, screenSize.width), topLeft.dy);
503+
}
504+
if (viewTopToScreenBottom < viewHeight) {
505+
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
506+
}
507+
final Size endSize = Size(viewWidth, viewHeight);
508+
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
492509
return;
493510
case TextDirection.rtl:
494-
final double viewEdgeToScreenEdge = anchorRect.right;
495-
final double endWidth = math.max(effectiveConstraints.minWidth, math.min(anchorRect.width, viewEdgeToScreenEdge));
496-
final Offset topLeft = Offset(math.max(anchorRect.right - endWidth, 0.0), anchorRect.top);
497-
final Size endSize = Size(endWidth, endHeight);
511+
final double viewRightToScreenLeft = anchorRect.right;
512+
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
513+
514+
// Make sure the search view doesn't go off the screen.
515+
Offset topLeft = Offset(math.max(anchorRect.right - viewWidth, 0.0), anchorRect.top);
516+
if (viewRightToScreenLeft < viewWidth) {
517+
topLeft = Offset(0.0, topLeft.dy);
518+
}
519+
if (viewTopToScreenBottom < viewHeight) {
520+
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
521+
}
522+
final Size endSize = Size(viewWidth, viewHeight);
498523
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
499524
}
500525
}
@@ -626,7 +651,6 @@ class _ViewContentState extends State<_ViewContent> {
626651
super.initState();
627652
_viewRect = widget.viewRect;
628653
_controller = widget.searchController;
629-
result = widget.suggestionsBuilder(context, _controller);
630654
if (!_focusNode.hasFocus) {
631655
_focusNode.requestFocus();
632656
}
@@ -645,20 +669,45 @@ class _ViewContentState extends State<_ViewContent> {
645669
@override
646670
void didChangeDependencies() {
647671
super.didChangeDependencies();
672+
result = widget.suggestionsBuilder(context, _controller);
648673
final Size updatedScreenSize = MediaQuery.of(context).size;
674+
649675
if (_screenSize != updatedScreenSize) {
650676
_screenSize = updatedScreenSize;
651677
setState(() {
652678
final Rect anchorRect = widget.getRect() ?? _viewRect;
653679
final BoxConstraints constraints = widget.viewConstraints ?? widget.viewTheme.constraints ?? widget.viewDefaults.constraints!;
654-
final Size updatedViewSize = Size(math.max(constraints.minWidth, anchorRect.width), _viewRect.height);
680+
final double viewWidth = clampDouble(anchorRect.width, constraints.minWidth, constraints.maxWidth);
681+
final double viewHeight = clampDouble(_screenSize!.height * 2 / 3, constraints.minHeight, constraints.maxHeight);
682+
final Size updatedViewSize = Size(viewWidth, viewHeight);
683+
655684
switch (Directionality.of(context)) {
656685
case TextDirection.ltr:
657-
final Offset updatedPosition = anchorRect.topLeft;
658-
_viewRect = updatedPosition & updatedViewSize;
686+
final double viewLeftToScreenRight = _screenSize!.width - anchorRect.left;
687+
final double viewTopToScreenBottom = _screenSize!.height - anchorRect.top;
688+
689+
// Make sure the search view doesn't go off the screen when the screen
690+
// size is changed. If the search view doesn't fit, move the top-left
691+
// corner of the view to fit the window. If the window is smaller than
692+
// the view, then we resize the view to fit the window.
693+
Offset topLeft = anchorRect.topLeft;
694+
if (viewLeftToScreenRight < viewWidth) {
695+
topLeft = Offset(_screenSize!.width - math.min(viewWidth, _screenSize!.width), anchorRect.top);
696+
}
697+
if (viewTopToScreenBottom < viewHeight) {
698+
topLeft = Offset(topLeft.dx, _screenSize!.height - math.min(viewHeight, _screenSize!.height));
699+
}
700+
_viewRect = topLeft & updatedViewSize;
659701
return;
660702
case TextDirection.rtl:
661-
final Offset topLeft = Offset(math.max(anchorRect.right - updatedViewSize.width, 0.0), anchorRect.top);
703+
final double viewTopToScreenBottom = _screenSize!.height - anchorRect.top;
704+
Offset topLeft = Offset(
705+
math.max(anchorRect.right - viewWidth, 0.0),
706+
anchorRect.top,
707+
);
708+
if (viewTopToScreenBottom < viewHeight) {
709+
topLeft = Offset(topLeft.dx, _screenSize!.height - math.min(viewHeight, _screenSize!.height));
710+
}
662711
_viewRect = topLeft & updatedViewSize;
663712
}
664713
});
@@ -834,6 +883,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
834883
TextStyle? viewHeaderHintStyle,
835884
super.dividerColor,
836885
BoxConstraints? constraints,
886+
super.viewConstraints,
837887
super.isFullScreen,
838888
super.searchController,
839889
required super.suggestionsBuilder

packages/flutter/test/material/search_anchor_test.dart

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,143 @@ void main() {
14231423
controller.openView();
14241424
expect(controller.isOpen, true);
14251425
});
1426+
1427+
testWidgets('Search view does not go off the screen - LTR', (WidgetTester tester) async {
1428+
await tester.pumpWidget(MaterialApp(
1429+
home: Material(
1430+
child: Align(
1431+
// Put the search anchor on the bottom-right corner of the screen to test
1432+
// if the search view goes off the window.
1433+
alignment: Alignment.bottomRight,
1434+
child: SearchAnchor(
1435+
isFullScreen: false,
1436+
builder: (BuildContext context, SearchController controller) {
1437+
return IconButton(
1438+
icon: const Icon(Icons.search),
1439+
onPressed: () {
1440+
controller.openView();
1441+
},
1442+
);
1443+
},
1444+
suggestionsBuilder: (BuildContext context, SearchController controller) {
1445+
return <Widget>[];
1446+
},
1447+
),
1448+
),
1449+
),),
1450+
);
1451+
1452+
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
1453+
final Rect iconButton = tester.getRect(findIconButton);
1454+
// Icon button has a size of (48.0, 48.0) and the screen size is (800.0, 600.0).
1455+
expect(iconButton, equals(const Rect.fromLTRB(752.0, 552.0, 800.0, 600.0)));
1456+
1457+
await tester.tap(find.byIcon(Icons.search));
1458+
await tester.pumpAndSettle();
1459+
1460+
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
1461+
expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0)));
1462+
});
1463+
1464+
testWidgets('Search view does not go off the screen - RTL', (WidgetTester tester) async {
1465+
await tester.pumpWidget(MaterialApp(
1466+
home: Directionality(
1467+
textDirection: TextDirection.rtl,
1468+
child: Material(
1469+
child: Align(
1470+
// Put the search anchor on the bottom-left corner of the screen to test
1471+
// if the search view goes off the window when the text direction is right-to-left.
1472+
alignment: Alignment.bottomLeft,
1473+
child: SearchAnchor(
1474+
isFullScreen: false,
1475+
builder: (BuildContext context, SearchController controller) {
1476+
return IconButton(
1477+
icon: const Icon(Icons.search),
1478+
onPressed: () {
1479+
controller.openView();
1480+
},
1481+
);
1482+
},
1483+
suggestionsBuilder: (BuildContext context, SearchController controller) {
1484+
return <Widget>[];
1485+
},
1486+
),
1487+
),
1488+
),
1489+
),),
1490+
);
1491+
1492+
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
1493+
final Rect iconButton = tester.getRect(findIconButton);
1494+
expect(iconButton, equals(const Rect.fromLTRB(0.0, 552.0, 48.0, 600.0)));
1495+
1496+
await tester.tap(find.byIcon(Icons.search));
1497+
await tester.pumpAndSettle();
1498+
1499+
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
1500+
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 200.0, 360.0, 600.0)));
1501+
});
1502+
1503+
testWidgets('Search view becomes smaller if the window size is smaller than the view size', (WidgetTester tester) async {
1504+
addTearDown(tester.view.reset);
1505+
tester.view.physicalSize = const Size(200.0, 200.0);
1506+
tester.view.devicePixelRatio = 1.0;
1507+
1508+
Widget buildSearchAnchor({TextDirection textDirection = TextDirection.ltr}) {
1509+
return MaterialApp(
1510+
home: Directionality(
1511+
textDirection: textDirection,
1512+
child: Material(
1513+
child: SearchAnchor(
1514+
isFullScreen: false,
1515+
builder: (BuildContext context, SearchController controller) {
1516+
return Align(
1517+
alignment: Alignment.bottomRight,
1518+
child: IconButton(
1519+
icon: const Icon(Icons.search),
1520+
onPressed: () {
1521+
controller.openView();
1522+
},
1523+
),
1524+
);
1525+
},
1526+
suggestionsBuilder: (BuildContext context, SearchController controller) {
1527+
return <Widget>[];
1528+
},
1529+
),
1530+
),
1531+
),);
1532+
}
1533+
1534+
// Test LTR text direction.
1535+
await tester.pumpWidget(buildSearchAnchor());
1536+
1537+
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
1538+
final Rect iconButton = tester.getRect(findIconButton);
1539+
// The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0)
1540+
expect(iconButton, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0)));
1541+
1542+
await tester.tap(find.byIcon(Icons.search));
1543+
await tester.pumpAndSettle();
1544+
1545+
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
1546+
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0)));
1547+
1548+
// Test RTL text direction.
1549+
await tester.pumpWidget(Container());
1550+
await tester.pumpWidget(buildSearchAnchor(textDirection: TextDirection.rtl));
1551+
1552+
final Finder findIconButtonRTL = find.widgetWithIcon(IconButton, Icons.search);
1553+
final Rect iconButtonRTL = tester.getRect(findIconButtonRTL);
1554+
// The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0)
1555+
expect(iconButtonRTL, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0)));
1556+
1557+
await tester.tap(find.byIcon(Icons.search));
1558+
await tester.pumpAndSettle();
1559+
1560+
final Rect searchViewRectRTL = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
1561+
expect(searchViewRectRTL, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0)));
1562+
});
14261563
}
14271564

14281565
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {

0 commit comments

Comments
 (0)