Unverified Commit a3cbe253 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Part 1: Improve Overlay API (#28747)

parent d4f8a7f2
......@@ -272,26 +272,71 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
insertAll(widget.initialEntries);
}
int _insertionIndex(OverlayEntry below, OverlayEntry above) {
assert(above == null || below == null);
if (below != null)
return _entries.indexOf(below);
if (above != null)
return _entries.indexOf(above) + 1;
return _entries.length;
}
/// Insert the given entry into the overlay.
///
/// If `below` is non-null, the entry is inserted just below `below`.
/// If `above` is non-null, the entry is inserted just above `above`.
/// Otherwise, the entry is inserted on top.
void insert(OverlayEntry entry, { OverlayEntry above }) {
assert(entry._overlay == null);
assert(above == null || (above._overlay == this && _entries.contains(above)));
///
/// It is an error to specify both `above` and `below`.
void insert(OverlayEntry entry, { OverlayEntry below, OverlayEntry above }) {
assert(
above == null || below == null,
'Only one of `above` and `below` may be specified.',
);
assert(
above == null || (above._overlay == this && _entries.contains(above)),
'The provided entry for `above` is not present in the Overlay.',
);
assert(
below == null || (below._overlay == this && _entries.contains(below)),
'The provided entry for `below` is not present in the Overlay.',
);
assert(!_entries.contains(entry), 'The specified entry is already present in the Overlay.');
assert(entry._overlay == null, 'The specified entry is already present in another Overlay.');
entry._overlay = this;
setState(() {
final int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
_entries.insert(index, entry);
_entries.insert(_insertionIndex(below, above), entry);
});
}
/// Insert all the entries in the given iterable.
///
/// If `below` is non-null, the entries are inserted just below `below`.
/// If `above` is non-null, the entries are inserted just above `above`.
/// Otherwise, the entries are inserted on top.
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above }) {
assert(above == null || (above._overlay == this && _entries.contains(above)));
///
/// It is an error to specify both `above` and `below`.
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
assert(
above == null || below == null,
'Only one of `above` and `below` may be specified.',
);
assert(
above == null || (above._overlay == this && _entries.contains(above)),
'The provided entry for `above` is not present in the Overlay.',
);
assert(
below == null || (below._overlay == this && _entries.contains(below)),
'The provided entry for `below` is not present in the Overlay.',
);
assert(
entries.every((OverlayEntry entry) => !_entries.contains(entry)),
'One or more of the specified entries are already present in the Overlay.'
);
assert(
entries.every((OverlayEntry entry) => entry._overlay == null),
'One or more of the specified entries are already present in another Overlay.'
);
if (entries.isEmpty)
return;
for (OverlayEntry entry in entries) {
......@@ -299,8 +344,62 @@ class OverlayState extends State<Overlay> with TickerProviderStateMixin {
entry._overlay = this;
}
setState(() {
final int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
_entries.insertAll(index, entries);
_entries.insertAll(_insertionIndex(below, above), entries);
});
}
/// Remove all the entries listed in the given iterable, then reinsert them
/// into the overlay in the given order.
///
/// Entries mention in `newEntries` but absent from the overlay are inserted
/// as if with [insertAll].
///
/// Entries not mentioned in `newEntries` but present in the overlay are
/// positioned as a group in the resulting list relative to the entries that
/// were moved, as specified by one of `below` or `above`, which, if
/// specified, must be one of the entries in `newEntries`:
///
/// If `below` is non-null, the group is positioned just below `below`.
/// If `above` is non-null, the group is positioned just above `above`.
/// Otherwise, the group is left on top, with all the rearranged entries
/// below.
///
/// It is an error to specify both `above` and `below`.
void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry below, OverlayEntry above }) {
final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false);
assert(
above == null || below == null,
'Only one of `above` and `below` may be specified.',
);
assert(
above == null || (above._overlay == this && _entries.contains(above) && newEntriesList.contains(above)),
'The entry used for `above` must be in the Overlay and in the `newEntriesList`.'
);
assert(
below == null || (below._overlay == this && _entries.contains(below) && newEntriesList.contains(below)),
'The entry used for `below` must be in the Overlay and in the `newEntriesList`.'
);
assert(
newEntriesList.every((OverlayEntry entry) => entry._overlay == null || entry._overlay == this),
'One or more of the specified entries are already present in another Overlay.'
);
assert(
newEntriesList.every((OverlayEntry entry) => _entries.indexOf(entry) == _entries.lastIndexOf(entry)),
'One or more of the specified entries are specified multiple times.'
);
if (newEntriesList.isEmpty)
return;
if (listEquals(_entries, newEntriesList))
return;
final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.from(_entries);
for (OverlayEntry entry in newEntriesList) {
entry._overlay ??= this;
}
setState(() {
_entries.clear();
_entries.addAll(newEntriesList);
old.removeAll(newEntriesList);
_entries.insertAll(_insertionIndex(below, above), old);
});
}
......
......@@ -159,4 +159,457 @@ void main() {
),
);
});
testWidgets('insert top', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<String> buildOrder = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base']);
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insert(
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New');
return Container();
}
),
);
await tester.pump();
expect(buildOrder, <String>['Base', 'New']);
});
testWidgets('insert below', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
OverlayEntry base;
final List<String> buildOrder = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
base = OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base']);
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insert(
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New');
return Container();
}
),
below: base,
);
await tester.pump();
expect(buildOrder, <String>['New', 'Base']);
});
testWidgets('insert above', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
OverlayEntry base;
final List<String> buildOrder = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
base = OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Top');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base', 'Top']);
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insert(
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New');
return Container();
}
),
above: base,
);
await tester.pump();
expect(buildOrder, <String>['Base', 'New', 'Top']);
});
testWidgets('insertAll top', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<String> buildOrder = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base']);
final List<OverlayEntry> entries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New1');
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New2');
return Container();
},
),
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insertAll(entries);
await tester.pump();
expect(buildOrder, <String>['Base', 'New1', 'New2']);
});
testWidgets('insertAll below', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
OverlayEntry base;
final List<String> buildOrder = <String>[];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
base = OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base']);
final List<OverlayEntry> entries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New1');
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New2');
return Container();
},
),
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insertAll(entries, below: base);
await tester.pump();
expect(buildOrder, <String>['New1', 'New2','Base']);
});
testWidgets('insertAll above', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<String> buildOrder = <String>[];
OverlayEntry base;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
base = OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Base');
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('Top');
return Container();
},
),
],
),
),
);
expect(buildOrder, <String>['Base', 'Top']);
final List<OverlayEntry> entries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New1');
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add('New2');
return Container();
},
),
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.insertAll(entries, above: base);
await tester.pump();
expect(buildOrder, <String>['Base', 'New1', 'New2', 'Top']);
});
testWidgets('rearrange', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<int> buildOrder = <int>[];
final List<OverlayEntry> initialEntries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(0);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(1);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(2);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(3);
return Container();
},
),
];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: initialEntries,
),
),
);
expect(buildOrder, <int>[0, 1, 2, 3]);
final List<OverlayEntry> rearranged = <OverlayEntry>[
initialEntries[3],
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(4);
return Container();
},
),
initialEntries[2],
// 1 intentionally missing, will end up on top
initialEntries[0],
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.rearrange(rearranged);
await tester.pump();
expect(buildOrder, <int>[3, 4, 2, 0, 1]);
});
testWidgets('rearrange above', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<int> buildOrder = <int>[];
final List<OverlayEntry> initialEntries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(0);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(1);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(2);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(3);
return Container();
},
),
];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: initialEntries,
),
),
);
expect(buildOrder, <int>[0, 1, 2, 3]);
final List<OverlayEntry> rearranged = <OverlayEntry>[
initialEntries[3],
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(4);
return Container();
},
),
initialEntries[2],
// 1 intentionally missing
initialEntries[0],
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.rearrange(rearranged, above: initialEntries[2]);
await tester.pump();
expect(buildOrder, <int>[3, 4, 2, 1, 0]);
});
testWidgets('rearrange below', (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
final List<int> buildOrder = <int>[];
final List<OverlayEntry> initialEntries = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(0);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(1);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(2);
return Container();
},
),
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(3);
return Container();
},
),
];
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: initialEntries,
),
),
);
expect(buildOrder, <int>[0, 1, 2, 3]);
final List<OverlayEntry> rearranged = <OverlayEntry>[
initialEntries[3],
OverlayEntry(
builder: (BuildContext context) {
buildOrder.add(4);
return Container();
},
),
initialEntries[2],
// 1 intentionally missing
initialEntries[0],
];
buildOrder.clear();
final OverlayState overlay = overlayKey.currentState;
overlay.rearrange(rearranged, below: initialEntries[2]);
await tester.pump();
expect(buildOrder, <int>[3, 4, 1, 2, 0]);
});
}
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