Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
2b974aed
Unverified
Commit
2b974aed
authored
Mar 28, 2022
by
Andrei Diaconu
Committed by
GitHub
Mar 28, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Make popup menus avoid display features (#98981)
parent
231c1a4b
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
151 additions
and
20 deletions
+151
-20
popup_menu.dart
packages/flutter/lib/src/material/popup_menu.dart
+38
-9
display_feature_sub_screen.dart
...s/flutter/lib/src/widgets/display_feature_sub_screen.dart
+15
-10
popup_menu_test.dart
packages/flutter/test/material/popup_menu_test.dart
+61
-1
display_feature_sub_screen_test.dart
...flutter/test/widgets/display_feature_sub_screen_test.dart
+37
-0
No files found.
packages/flutter/lib/src/material/popup_menu.dart
View file @
2b974aed
...
...
@@ -636,6 +636,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
this
.
selectedItemIndex
,
this
.
textDirection
,
this
.
padding
,
this
.
avoidBounds
,
);
// Rectangle of underlying button, relative to the overlay's dimensions.
...
...
@@ -655,6 +656,9 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// The padding of unsafe area.
EdgeInsets
padding
;
// List of rectangles that we should avoid overlapping. Unusable screen area.
final
Set
<
Rect
>
avoidBounds
;
// We put the child wherever position specifies, so long as it will fit within
// the specified parent size padded (inset) by 8. If necessary, we adjust the
// child's position so that it fits.
...
...
@@ -705,19 +709,38 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
break
;
}
}
final
Offset
wantedPosition
=
Offset
(
x
,
y
);
final
Offset
originCenter
=
position
.
toRect
(
Offset
.
zero
&
size
).
center
;
final
Iterable
<
Rect
>
subScreens
=
DisplayFeatureSubScreen
.
subScreensInBounds
(
Offset
.
zero
&
size
,
avoidBounds
);
final
Rect
subScreen
=
_closestScreen
(
subScreens
,
originCenter
);
return
_fitInsideScreen
(
subScreen
,
childSize
,
wantedPosition
);
}
Rect
_closestScreen
(
Iterable
<
Rect
>
screens
,
Offset
point
)
{
Rect
closest
=
screens
.
first
;
for
(
final
Rect
screen
in
screens
)
{
if
((
screen
.
center
-
point
).
distance
<
(
closest
.
center
-
point
).
distance
)
{
closest
=
screen
;
}
}
return
closest
;
}
Offset
_fitInsideScreen
(
Rect
screen
,
Size
childSize
,
Offset
wantedPosition
){
double
x
=
wantedPosition
.
dx
;
double
y
=
wantedPosition
.
dy
;
// Avoid going outside an area defined as the rectangle 8.0 pixels from the
// edge of the screen in every direction.
if
(
x
<
_kMenuScreenPadding
+
padding
.
left
)
x
=
_kMenuScreenPadding
+
padding
.
left
;
else
if
(
x
+
childSize
.
width
>
s
ize
.
width
-
_kMenuScreenPadding
-
padding
.
right
)
x
=
s
ize
.
width
-
childSize
.
width
-
_kMenuScreenPadding
-
padding
.
right
;
if
(
y
<
_kMenuScreenPadding
+
padding
.
top
)
if
(
x
<
screen
.
left
+
_kMenuScreenPadding
+
padding
.
left
)
x
=
screen
.
left
+
_kMenuScreenPadding
+
padding
.
left
;
else
if
(
x
+
childSize
.
width
>
s
creen
.
right
-
_kMenuScreenPadding
-
padding
.
right
)
x
=
s
creen
.
right
-
childSize
.
width
-
_kMenuScreenPadding
-
padding
.
right
;
if
(
y
<
screen
.
top
+
_kMenuScreenPadding
+
padding
.
top
)
y
=
_kMenuScreenPadding
+
padding
.
top
;
else
if
(
y
+
childSize
.
height
>
s
ize
.
height
-
_kMenuScreenPadding
-
padding
.
bottom
)
y
=
s
ize
.
height
-
padding
.
bottom
-
_kMenuScreenPadding
-
childSize
.
height
;
else
if
(
y
+
childSize
.
height
>
s
creen
.
bottom
-
_kMenuScreenPadding
-
padding
.
bottom
)
y
=
s
creen
.
bottom
-
childSize
.
height
-
_kMenuScreenPadding
-
padding
.
bottom
;
return
Offset
(
x
,
y
);
return
Offset
(
x
,
y
);
}
@override
...
...
@@ -731,7 +754,8 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
||
selectedItemIndex
!=
oldDelegate
.
selectedItemIndex
||
textDirection
!=
oldDelegate
.
textDirection
||
!
listEquals
(
itemSizes
,
oldDelegate
.
itemSizes
)
||
padding
!=
oldDelegate
.
padding
;
||
padding
!=
oldDelegate
.
padding
||
!
setEquals
(
avoidBounds
,
oldDelegate
.
avoidBounds
);
}
}
...
...
@@ -813,6 +837,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
selectedItemIndex
,
Directionality
.
of
(
context
),
mediaQuery
.
padding
,
_avoidBounds
(
mediaQuery
),
),
child:
capturedThemes
.
wrap
(
menu
),
);
...
...
@@ -820,6 +845,10 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
),
);
}
Set
<
Rect
>
_avoidBounds
(
MediaQueryData
mediaQuery
)
{
return
DisplayFeatureSubScreen
.
avoidBounds
(
mediaQuery
).
toSet
();
}
}
/// Show a popup menu that contains the `items` at `position`.
...
...
packages/flutter/lib/src/widgets/display_feature_sub_screen.dart
View file @
2b974aed
...
...
@@ -3,7 +3,7 @@
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
show
DisplayFeature
;
import
'dart:ui'
show
DisplayFeature
,
DisplayFeatureState
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/rendering.dart'
;
...
...
@@ -19,9 +19,8 @@ import 'media_query.dart';
/// A [DisplayFeature] splits the screen into sub-screens when both these
/// conditions are met:
///
/// * it obstructs the screen, meaning the area it occupies is not 0. Display
/// features of type [DisplayFeatureType.fold] can have height 0 or width 0
/// and not be obstructing the screen.
/// * it obstructs the screen, meaning the area it occupies is not 0 or the
/// `state` is [DisplayFeatureState.postureHalfOpened].
/// * it is at least as tall as the screen, producing a left and right
/// sub-screen or it is at least as wide as the screen, producing a top and
/// bottom sub-screen
...
...
@@ -100,7 +99,7 @@ class DisplayFeatureSubScreen extends StatelessWidget {
final
Size
parentSize
=
mediaQuery
.
size
;
final
Rect
wantedBounds
=
Offset
.
zero
&
parentSize
;
final
Offset
resolvedAnchorPoint
=
_capOffset
(
anchorPoint
??
_fallbackAnchorPoint
(
context
),
parentSize
);
final
Iterable
<
Rect
>
subScreens
=
_subScreensInBounds
(
wantedBounds
,
_
avoidBounds
(
mediaQuery
));
final
Iterable
<
Rect
>
subScreens
=
subScreensInBounds
(
wantedBounds
,
avoidBounds
(
mediaQuery
));
final
Rect
closestSubScreen
=
_closestToAnchorPoint
(
subScreens
,
resolvedAnchorPoint
);
return
Padding
(
...
...
@@ -127,9 +126,15 @@ class DisplayFeatureSubScreen extends StatelessWidget {
}
}
static
Iterable
<
Rect
>
_avoidBounds
(
MediaQueryData
mediaQuery
)
{
return
mediaQuery
.
displayFeatures
.
map
((
DisplayFeature
d
)
=>
d
.
bounds
)
.
where
((
Rect
r
)
=>
r
.
shortestSide
>
0
);
/// Returns the areas of the screen that are obstructed by display features.
///
/// A [DisplayFeature] obstructs the screen when the the area it occupies is
/// not 0 or the `state` is [DisplayFeatureState.postureHalfOpened].
static
Iterable
<
Rect
>
avoidBounds
(
MediaQueryData
mediaQuery
)
{
return
mediaQuery
.
displayFeatures
.
where
((
DisplayFeature
d
)
=>
d
.
bounds
.
shortestSide
>
0
||
d
.
state
==
DisplayFeatureState
.
postureHalfOpened
)
.
map
((
DisplayFeature
d
)
=>
d
.
bounds
);
}
/// Returns the closest sub-screen to the [anchorPoint].
...
...
@@ -188,8 +193,8 @@ class DisplayFeatureSubScreen extends StatelessWidget {
}
/// Returns sub-screens resulted by dividing [wantedBounds] along items of
/// [avoidBounds] that are at least as
high
or as wide.
static
Iterable
<
Rect
>
_
subScreensInBounds
(
Rect
wantedBounds
,
Iterable
<
Rect
>
avoidBounds
)
{
/// [avoidBounds] that are at least as
tall
or as wide.
static
Iterable
<
Rect
>
subScreensInBounds
(
Rect
wantedBounds
,
Iterable
<
Rect
>
avoidBounds
)
{
Iterable
<
Rect
>
subScreens
=
<
Rect
>[
wantedBounds
];
for
(
final
Rect
bounds
in
avoidBounds
)
{
final
List
<
Rect
>
newSubScreens
=
<
Rect
>[];
...
...
packages/flutter/test/material/popup_menu_test.dart
View file @
2b974aed
...
...
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
show
SemanticsFlag
;
import
'dart:ui'
show
SemanticsFlag
,
DisplayFeature
,
DisplayFeatureType
,
DisplayFeatureState
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/gestures.dart'
;
...
...
@@ -853,6 +853,66 @@ void main() {
expect
(
tester
.
getTopLeft
(
popupFinder
),
buttonTopLeft
);
});
testWidgets
(
'PopupMenu positioning around display features'
,
(
WidgetTester
tester
)
async
{
final
Key
buttonKey
=
UniqueKey
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
MediaQuery
(
data:
const
MediaQueryData
(
size:
Size
(
800
,
600
),
displayFeatures:
<
DisplayFeature
>[
// A 20-pixel wide vertical display feature, similar to a foldable
// with a visible hinge. Splits the display into two "virtual screens"
// and the popup menu should never overlap the display feature.
DisplayFeature
(
bounds:
Rect
.
fromLTRB
(
390
,
0
,
410
,
600
),
type:
DisplayFeatureType
.
cutout
,
state:
DisplayFeatureState
.
unknown
,
)
]
),
child:
Scaffold
(
body:
Navigator
(
onGenerateRoute:
(
RouteSettings
settings
)
{
return
MaterialPageRoute
<
dynamic
>(
builder:
(
BuildContext
context
)
{
return
Padding
(
// Position the button in the top-right of the first "virtual screen"
padding:
const
EdgeInsets
.
only
(
right:
390.0
),
child:
Align
(
alignment:
Alignment
.
topRight
,
child:
PopupMenuButton
<
int
>(
key:
buttonKey
,
itemBuilder:
(
_
)
=>
<
PopupMenuItem
<
int
>>[
const
PopupMenuItem
<
int
>(
value:
1
,
child:
Text
(
'Item 1'
)),
const
PopupMenuItem
<
int
>(
value:
2
,
child:
Text
(
'Item 2'
)),
],
child:
const
Text
(
'Show Menu'
),
),
),
);
},
);
},
),
),
),
),
);
final
Finder
buttonFinder
=
find
.
byKey
(
buttonKey
);
final
Finder
popupFinder
=
find
.
bySemanticsLabel
(
'Popup menu'
);
await
tester
.
tap
(
buttonFinder
);
await
tester
.
pumpAndSettle
();
// Since the display feature splits the display into 2 sub-screens, popup
// menu should be positioned to fit in the first virtual screen, where the
// originating button is.
// The 8 pixels is [_kMenuScreenPadding].
expect
(
tester
.
getTopRight
(
popupFinder
),
const
Offset
(
390
-
8
,
8
));
});
testWidgets
(
'PopupMenu removes MediaQuery padding'
,
(
WidgetTester
tester
)
async
{
late
BuildContext
popupContext
;
...
...
packages/flutter/test/widgets/display_feature_sub_screen_test.dart
View file @
2b974aed
...
...
@@ -192,6 +192,11 @@ void main() {
type:
DisplayFeatureType
.
cutout
,
state:
DisplayFeatureState
.
unknown
,
),
const
DisplayFeature
(
bounds:
Rect
.
fromLTRB
(
0
,
300
,
800
,
300
),
type:
DisplayFeatureType
.
fold
,
state:
DisplayFeatureState
.
postureFlat
,
),
]
);
...
...
@@ -217,5 +222,37 @@ void main() {
expect
(
renderBox
.
size
.
height
,
equals
(
600.0
));
expect
(
renderBox
.
localToGlobal
(
Offset
.
zero
),
equals
(
Offset
.
zero
));
});
testWidgets
(
'with size 0 display feature in half-opened posture and anchorPoint'
,
(
WidgetTester
tester
)
async
{
const
Key
childKey
=
Key
(
'childKey'
);
final
MediaQueryData
mediaQuery
=
MediaQueryData
.
fromWindow
(
WidgetsBinding
.
instance
.
window
).
copyWith
(
displayFeatures:
<
DisplayFeature
>[
const
DisplayFeature
(
bounds:
Rect
.
fromLTRB
(
0
,
300
,
800
,
300
),
type:
DisplayFeatureType
.
fold
,
state:
DisplayFeatureState
.
postureHalfOpened
,
),
]
);
await
tester
.
pumpWidget
(
MediaQuery
(
data:
mediaQuery
,
child:
const
DisplayFeatureSubScreen
(
anchorPoint:
Offset
(
1000
,
1000
),
child:
SizedBox
(
key:
childKey
,
width:
double
.
infinity
,
height:
double
.
infinity
,
),
),
),
);
final
RenderBox
renderBox
=
tester
.
renderObject
(
find
.
byKey
(
childKey
));
expect
(
renderBox
.
size
.
width
,
equals
(
800.0
));
expect
(
renderBox
.
size
.
height
,
equals
(
300.0
));
expect
(
renderBox
.
localToGlobal
(
Offset
.
zero
),
equals
(
const
Offset
(
0
,
300
)));
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment