Unverified Commit 3a4fb9df authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Fix search view position (#124169)

parent 90f8ac5b
......@@ -148,6 +148,7 @@ class SearchAnchor extends StatefulWidget {
TextStyle? viewHeaderHintStyle,
Color? dividerColor,
BoxConstraints? constraints,
BoxConstraints? viewConstraints,
bool? isFullScreen,
SearchController searchController,
required SuggestionsBuilder suggestionsBuilder
......@@ -254,6 +255,11 @@ class SearchAnchor extends StatefulWidget {
/// Optional size constraints for the search view.
///
/// By default, the search view has the same width as the anchor and is 2/3
/// the height of the screen. If the width and height of the view are within
/// the [viewConstraints], the view will show its default size. Otherwise,
/// the size of the view will be constrained by this property.
///
/// If null, the value of [SearchViewThemeData.constraints] will be used. If
/// this is also null, then the constraints defaults to:
/// ```dart
......@@ -477,24 +483,43 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
final Size screenSize = MediaQuery.of(context).size;
final Rect anchorRect = getRect() ?? Rect.zero;
// Check if the search view goes off the screen.
final BoxConstraints effectiveConstraints = viewConstraints ?? viewTheme.constraints ?? viewDefaults.constraints!;
final double verticalDistanceToEdge = screenSize.height - anchorRect.top;
final double endHeight = math.max(effectiveConstraints.minHeight, math.min(screenSize.height * 2 / 3, verticalDistanceToEdge));
_rectTween.begin = anchorRect;
final double viewWidth = clampDouble(anchorRect.width, effectiveConstraints.minWidth, effectiveConstraints.maxWidth);
final double viewHeight = clampDouble(screenSize.height * 2 / 3, effectiveConstraints.minHeight, effectiveConstraints.maxHeight);
switch (textDirection ?? TextDirection.ltr) {
case TextDirection.ltr:
final double viewEdgeToScreenEdge = screenSize.width - anchorRect.left;
final double endWidth = math.max(effectiveConstraints.minWidth, math.min(anchorRect.width, viewEdgeToScreenEdge));
final Size endSize = Size(endWidth, endHeight);
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (anchorRect.topLeft & endSize);
final double viewLeftToScreenRight = screenSize.width - anchorRect.left;
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
// Make sure the search view doesn't go off the screen. If the search view
// doesn't fit, move the top-left corner of the view to fit the window.
// If the window is smaller than the view, then we resize the view to fit the window.
Offset topLeft = anchorRect.topLeft;
if (viewLeftToScreenRight < viewWidth) {
topLeft = Offset(screenSize.width - math.min(viewWidth, screenSize.width), topLeft.dy);
}
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
}
final Size endSize = Size(viewWidth, viewHeight);
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
return;
case TextDirection.rtl:
final double viewEdgeToScreenEdge = anchorRect.right;
final double endWidth = math.max(effectiveConstraints.minWidth, math.min(anchorRect.width, viewEdgeToScreenEdge));
final Offset topLeft = Offset(math.max(anchorRect.right - endWidth, 0.0), anchorRect.top);
final Size endSize = Size(endWidth, endHeight);
final double viewRightToScreenLeft = anchorRect.right;
final double viewTopToScreenBottom = screenSize.height - anchorRect.top;
// Make sure the search view doesn't go off the screen.
Offset topLeft = Offset(math.max(anchorRect.right - viewWidth, 0.0), anchorRect.top);
if (viewRightToScreenLeft < viewWidth) {
topLeft = Offset(0.0, topLeft.dy);
}
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, screenSize.height - math.min(viewHeight, screenSize.height));
}
final Size endSize = Size(viewWidth, viewHeight);
_rectTween.end = showFullScreenView ? Offset.zero & screenSize : (topLeft & endSize);
}
}
......@@ -626,7 +651,6 @@ class _ViewContentState extends State<_ViewContent> {
super.initState();
_viewRect = widget.viewRect;
_controller = widget.searchController;
result = widget.suggestionsBuilder(context, _controller);
if (!_focusNode.hasFocus) {
_focusNode.requestFocus();
}
......@@ -645,20 +669,45 @@ class _ViewContentState extends State<_ViewContent> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
result = widget.suggestionsBuilder(context, _controller);
final Size updatedScreenSize = MediaQuery.of(context).size;
if (_screenSize != updatedScreenSize) {
_screenSize = updatedScreenSize;
setState(() {
final Rect anchorRect = widget.getRect() ?? _viewRect;
final BoxConstraints constraints = widget.viewConstraints ?? widget.viewTheme.constraints ?? widget.viewDefaults.constraints!;
final Size updatedViewSize = Size(math.max(constraints.minWidth, anchorRect.width), _viewRect.height);
final double viewWidth = clampDouble(anchorRect.width, constraints.minWidth, constraints.maxWidth);
final double viewHeight = clampDouble(_screenSize!.height * 2 / 3, constraints.minHeight, constraints.maxHeight);
final Size updatedViewSize = Size(viewWidth, viewHeight);
switch (Directionality.of(context)) {
case TextDirection.ltr:
final Offset updatedPosition = anchorRect.topLeft;
_viewRect = updatedPosition & updatedViewSize;
final double viewLeftToScreenRight = _screenSize!.width - anchorRect.left;
final double viewTopToScreenBottom = _screenSize!.height - anchorRect.top;
// Make sure the search view doesn't go off the screen when the screen
// size is changed. If the search view doesn't fit, move the top-left
// corner of the view to fit the window. If the window is smaller than
// the view, then we resize the view to fit the window.
Offset topLeft = anchorRect.topLeft;
if (viewLeftToScreenRight < viewWidth) {
topLeft = Offset(_screenSize!.width - math.min(viewWidth, _screenSize!.width), anchorRect.top);
}
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, _screenSize!.height - math.min(viewHeight, _screenSize!.height));
}
_viewRect = topLeft & updatedViewSize;
return;
case TextDirection.rtl:
final Offset topLeft = Offset(math.max(anchorRect.right - updatedViewSize.width, 0.0), anchorRect.top);
final double viewTopToScreenBottom = _screenSize!.height - anchorRect.top;
Offset topLeft = Offset(
math.max(anchorRect.right - viewWidth, 0.0),
anchorRect.top,
);
if (viewTopToScreenBottom < viewHeight) {
topLeft = Offset(topLeft.dx, _screenSize!.height - math.min(viewHeight, _screenSize!.height));
}
_viewRect = topLeft & updatedViewSize;
}
});
......@@ -834,6 +883,7 @@ class _SearchAnchorWithSearchBar extends SearchAnchor {
TextStyle? viewHeaderHintStyle,
super.dividerColor,
BoxConstraints? constraints,
super.viewConstraints,
super.isFullScreen,
super.searchController,
required super.suggestionsBuilder
......
......@@ -1423,6 +1423,143 @@ void main() {
controller.openView();
expect(controller.isOpen, true);
});
testWidgets('Search view does not go off the screen - LTR', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Material(
child: Align(
// Put the search anchor on the bottom-right corner of the screen to test
// if the search view goes off the window.
alignment: Alignment.bottomRight,
child: SearchAnchor(
isFullScreen: false,
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),),
);
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
final Rect iconButton = tester.getRect(findIconButton);
// Icon button has a size of (48.0, 48.0) and the screen size is (800.0, 600.0).
expect(iconButton, equals(const Rect.fromLTRB(752.0, 552.0, 800.0, 600.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(440.0, 200.0, 800.0, 600.0)));
});
testWidgets('Search view does not go off the screen - RTL', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Align(
// Put the search anchor on the bottom-left corner of the screen to test
// if the search view goes off the window when the text direction is right-to-left.
alignment: Alignment.bottomLeft,
child: SearchAnchor(
isFullScreen: false,
builder: (BuildContext context, SearchController controller) {
return IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),
),),
);
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
final Rect iconButton = tester.getRect(findIconButton);
expect(iconButton, equals(const Rect.fromLTRB(0.0, 552.0, 48.0, 600.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 200.0, 360.0, 600.0)));
});
testWidgets('Search view becomes smaller if the window size is smaller than the view size', (WidgetTester tester) async {
addTearDown(tester.view.reset);
tester.view.physicalSize = const Size(200.0, 200.0);
tester.view.devicePixelRatio = 1.0;
Widget buildSearchAnchor({TextDirection textDirection = TextDirection.ltr}) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: Material(
child: SearchAnchor(
isFullScreen: false,
builder: (BuildContext context, SearchController controller) {
return Align(
alignment: Alignment.bottomRight,
child: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
controller.openView();
},
),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
),);
}
// Test LTR text direction.
await tester.pumpWidget(buildSearchAnchor());
final Finder findIconButton = find.widgetWithIcon(IconButton, Icons.search);
final Rect iconButton = tester.getRect(findIconButton);
// The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0)
expect(iconButton, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRect = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRect, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0)));
// Test RTL text direction.
await tester.pumpWidget(Container());
await tester.pumpWidget(buildSearchAnchor(textDirection: TextDirection.rtl));
final Finder findIconButtonRTL = find.widgetWithIcon(IconButton, Icons.search);
final Rect iconButtonRTL = tester.getRect(findIconButtonRTL);
// The icon button size is (48.0, 48.0), and the screen size is (200.0, 200.0)
expect(iconButtonRTL, equals(const Rect.fromLTRB(152.0, 152.0, 200.0, 200.0)));
await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
final Rect searchViewRectRTL = tester.getRect(find.descendant(of: findViewContent(), matching: find.byType(SizedBox)).first);
expect(searchViewRectRTL, equals(const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0)));
});
}
TextStyle? _iconStyle(WidgetTester tester, IconData icon) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment