Reland Refactor reorderable list semantics (#124395)
Reland Refactor reorderable list semantics
This commit is contained in:
parent
4c3d7333b1
commit
e5e765e683
@ -5,13 +5,11 @@
|
|||||||
import 'dart:ui' show lerpDouble;
|
import 'dart:ui' show lerpDouble;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'icons.dart';
|
import 'icons.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
/// A list whose items the user can interactively reorder by dragging.
|
/// A list whose items the user can interactively reorder by dragging.
|
||||||
@ -266,64 +264,6 @@ class ReorderableListView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ReorderableListViewState extends State<ReorderableListView> {
|
class _ReorderableListViewState extends State<ReorderableListView> {
|
||||||
Widget _wrapWithSemantics(Widget child, int index) {
|
|
||||||
void reorder(int startIndex, int endIndex) {
|
|
||||||
if (startIndex != endIndex) {
|
|
||||||
widget.onReorder(startIndex, endIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, determine which semantics actions apply.
|
|
||||||
final Map<CustomSemanticsAction, VoidCallback> semanticsActions = <CustomSemanticsAction, VoidCallback>{};
|
|
||||||
|
|
||||||
// Create the appropriate semantics actions.
|
|
||||||
void moveToStart() => reorder(index, 0);
|
|
||||||
void moveToEnd() => reorder(index, widget.itemCount);
|
|
||||||
void moveBefore() => reorder(index, index - 1);
|
|
||||||
// To move after, we go to index+2 because we are moving it to the space
|
|
||||||
// before index+2, which is after the space at index+1.
|
|
||||||
void moveAfter() => reorder(index, index + 2);
|
|
||||||
|
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
|
||||||
|
|
||||||
// If the item can move to before its current position in the list.
|
|
||||||
if (index > 0) {
|
|
||||||
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart;
|
|
||||||
String reorderItemBefore = localizations.reorderItemUp;
|
|
||||||
if (widget.scrollDirection == Axis.horizontal) {
|
|
||||||
reorderItemBefore = Directionality.of(context) == TextDirection.ltr
|
|
||||||
? localizations.reorderItemLeft
|
|
||||||
: localizations.reorderItemRight;
|
|
||||||
}
|
|
||||||
semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the item can move to after its current position in the list.
|
|
||||||
if (index < widget.itemCount - 1) {
|
|
||||||
String reorderItemAfter = localizations.reorderItemDown;
|
|
||||||
if (widget.scrollDirection == Axis.horizontal) {
|
|
||||||
reorderItemAfter = Directionality.of(context) == TextDirection.ltr
|
|
||||||
? localizations.reorderItemRight
|
|
||||||
: localizations.reorderItemLeft;
|
|
||||||
}
|
|
||||||
semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter;
|
|
||||||
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We pass toWrap with a GlobalKey into the item so that when it
|
|
||||||
// gets dragged, the accessibility framework can preserve the selected
|
|
||||||
// state of the dragging item.
|
|
||||||
//
|
|
||||||
// We also apply the relevant custom accessibility actions for moving the item
|
|
||||||
// up, down, to the start, and to the end of the list.
|
|
||||||
return MergeSemantics(
|
|
||||||
child: Semantics(
|
|
||||||
customSemanticsActions: semanticsActions,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(BuildContext context, int index) {
|
Widget _itemBuilder(BuildContext context, int index) {
|
||||||
final Widget item = widget.itemBuilder(context, index);
|
final Widget item = widget.itemBuilder(context, index);
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -335,9 +275,6 @@ class _ReorderableListViewState extends State<ReorderableListView> {
|
|||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
|
|
||||||
// TODO(goderbauer): The semantics stuff should probably happen inside
|
|
||||||
// _ReorderableItem so the widget versions can have them as well.
|
|
||||||
final Widget itemWithSemantics = _wrapWithSemantics(item, index);
|
|
||||||
final Key itemGlobalKey = _ReorderableListViewChildGlobalKey(item.key!, this);
|
final Key itemGlobalKey = _ReorderableListViewChildGlobalKey(item.key!, this);
|
||||||
|
|
||||||
if (widget.buildDefaultDragHandles) {
|
if (widget.buildDefaultDragHandles) {
|
||||||
@ -350,7 +287,7 @@ class _ReorderableListViewState extends State<ReorderableListView> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
key: itemGlobalKey,
|
key: itemGlobalKey,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
itemWithSemantics,
|
item,
|
||||||
Positioned.directional(
|
Positioned.directional(
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -370,7 +307,7 @@ class _ReorderableListViewState extends State<ReorderableListView> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
key: itemGlobalKey,
|
key: itemGlobalKey,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
itemWithSemantics,
|
item,
|
||||||
Positioned.directional(
|
Positioned.directional(
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -394,14 +331,14 @@ class _ReorderableListViewState extends State<ReorderableListView> {
|
|||||||
return ReorderableDelayedDragStartListener(
|
return ReorderableDelayedDragStartListener(
|
||||||
key: itemGlobalKey,
|
key: itemGlobalKey,
|
||||||
index: index,
|
index: index,
|
||||||
child: itemWithSemantics,
|
child: item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyedSubtree(
|
return KeyedSubtree(
|
||||||
key: itemGlobalKey,
|
key: itemGlobalKey,
|
||||||
child: itemWithSemantics,
|
child: item,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import 'basic.dart';
|
|||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'inherited_theme.dart';
|
import 'inherited_theme.dart';
|
||||||
|
import 'localizations.dart';
|
||||||
import 'media_query.dart';
|
import 'media_query.dart';
|
||||||
import 'overlay.dart';
|
import 'overlay.dart';
|
||||||
import 'scroll_controller.dart';
|
import 'scroll_controller.dart';
|
||||||
@ -928,6 +929,63 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
|||||||
key: _ReorderableItemGlobalKey(child.key!, index, this),
|
key: _ReorderableItemGlobalKey(child.key!, index, this),
|
||||||
index: index,
|
index: index,
|
||||||
capturedThemes: InheritedTheme.capture(from: context, to: overlay.context),
|
capturedThemes: InheritedTheme.capture(from: context, to: overlay.context),
|
||||||
|
child: _wrapWithSemantics(child, index),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapWithSemantics(Widget child, int index) {
|
||||||
|
void reorder(int startIndex, int endIndex) {
|
||||||
|
if (startIndex != endIndex) {
|
||||||
|
widget.onReorder(startIndex, endIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, determine which semantics actions apply.
|
||||||
|
final Map<CustomSemanticsAction, VoidCallback> semanticsActions = <CustomSemanticsAction, VoidCallback>{};
|
||||||
|
|
||||||
|
// Create the appropriate semantics actions.
|
||||||
|
void moveToStart() => reorder(index, 0);
|
||||||
|
void moveToEnd() => reorder(index, widget.itemCount);
|
||||||
|
void moveBefore() => reorder(index, index - 1);
|
||||||
|
// To move after, go to index+2 because it is moved to the space
|
||||||
|
// before index+2, which is after the space at index+1.
|
||||||
|
void moveAfter() => reorder(index, index + 2);
|
||||||
|
|
||||||
|
final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
|
||||||
|
final bool isHorizontal = _scrollDirection == Axis.horizontal;
|
||||||
|
// If the item can move to before its current position in the list.
|
||||||
|
if (index > 0) {
|
||||||
|
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart;
|
||||||
|
String reorderItemBefore = localizations.reorderItemUp;
|
||||||
|
if (isHorizontal) {
|
||||||
|
reorderItemBefore = Directionality.of(context) == TextDirection.ltr
|
||||||
|
? localizations.reorderItemLeft
|
||||||
|
: localizations.reorderItemRight;
|
||||||
|
}
|
||||||
|
semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the item can move to after its current position in the list.
|
||||||
|
if (index < widget.itemCount - 1) {
|
||||||
|
String reorderItemAfter = localizations.reorderItemDown;
|
||||||
|
if (isHorizontal) {
|
||||||
|
reorderItemAfter = Directionality.of(context) == TextDirection.ltr
|
||||||
|
? localizations.reorderItemRight
|
||||||
|
: localizations.reorderItemLeft;
|
||||||
|
}
|
||||||
|
semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter;
|
||||||
|
semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass toWrap with a GlobalKey into the item so that when it
|
||||||
|
// gets dragged, the accessibility framework can preserve the selected
|
||||||
|
// state of the dragging item.
|
||||||
|
//
|
||||||
|
// Also apply the relevant custom accessibility actions for moving the item
|
||||||
|
// up, down, to the start, and to the end of the list.
|
||||||
|
return Semantics(
|
||||||
|
container: true,
|
||||||
|
customSemanticsActions: semanticsActions,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -673,8 +673,23 @@ void main() {
|
|||||||
// Get the switch tile's semantics:
|
// Get the switch tile's semantics:
|
||||||
final SemanticsNode semanticsNode = tester.getSemantics(find.byKey(const Key('Switch tile')));
|
final SemanticsNode semanticsNode = tester.getSemantics(find.byKey(const Key('Switch tile')));
|
||||||
|
|
||||||
// Check for properties of both SwitchTile semantics and the ReorderableListView custom semantics actions.
|
// Check for ReorderableListView custom semantics actions.
|
||||||
expect(semanticsNode, matchesSemantics(
|
expect(semanticsNode, matchesSemantics(
|
||||||
|
customActions: const <CustomSemanticsAction>[
|
||||||
|
CustomSemanticsAction(label: 'Move up'),
|
||||||
|
CustomSemanticsAction(label: 'Move down'),
|
||||||
|
CustomSemanticsAction(label: 'Move to the end'),
|
||||||
|
CustomSemanticsAction(label: 'Move to the start'),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
// Check for properties of SwitchTile semantics.
|
||||||
|
late SemanticsNode child;
|
||||||
|
semanticsNode.visitChildren((SemanticsNode node) {
|
||||||
|
child = node;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
expect(child, matchesSemantics(
|
||||||
hasToggledState: true,
|
hasToggledState: true,
|
||||||
isToggled: true,
|
isToggled: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
@ -682,12 +697,6 @@ void main() {
|
|||||||
hasEnabledState: true,
|
hasEnabledState: true,
|
||||||
label: 'Switch tile',
|
label: 'Switch tile',
|
||||||
hasTapAction: true,
|
hasTapAction: true,
|
||||||
customActions: const <CustomSemanticsAction>[
|
|
||||||
CustomSemanticsAction(label: 'Move up'),
|
|
||||||
CustomSemanticsAction(label: 'Move down'),
|
|
||||||
CustomSemanticsAction(label: 'Move to the end'),
|
|
||||||
CustomSemanticsAction(label: 'Move to the start'),
|
|
||||||
],
|
|
||||||
));
|
));
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
});
|
});
|
||||||
@ -1644,7 +1653,7 @@ void main() {
|
|||||||
DefaultMaterialLocalizations.delegate,
|
DefaultMaterialLocalizations.delegate,
|
||||||
DefaultWidgetsLocalizations.delegate,
|
DefaultWidgetsLocalizations.delegate,
|
||||||
],
|
],
|
||||||
child:SizedBox(
|
child: SizedBox(
|
||||||
width: 100.0,
|
width: 100.0,
|
||||||
height: 100.0,
|
height: 100.0,
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
|
@ -4,8 +4,11 @@
|
|||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'semantics_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('SliverReorderableList works well when having gestureSettings', (WidgetTester tester) async {
|
testWidgets('SliverReorderableList works well when having gestureSettings', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/103404
|
// Regression test for https://github.com/flutter/flutter/issues/103404
|
||||||
@ -64,6 +67,103 @@ void main() {
|
|||||||
expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
|
expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('SliverReorderableList item has correct semantics', (WidgetTester tester) async {
|
||||||
|
final SemanticsTester semantics = SemanticsTester(tester);
|
||||||
|
const int itemCount = 5;
|
||||||
|
int onReorderCallCount = 0;
|
||||||
|
final List<int> items = List<int>.generate(itemCount, (int index) => index);
|
||||||
|
|
||||||
|
void handleReorder(int fromIndex, int toIndex) {
|
||||||
|
onReorderCallCount += 1;
|
||||||
|
if (toIndex > fromIndex) {
|
||||||
|
toIndex -= 1;
|
||||||
|
}
|
||||||
|
items.insert(toIndex, items.removeAt(fromIndex));
|
||||||
|
}
|
||||||
|
// The list has five elements of height 100
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(gestureSettings: DeviceGestureSettings(touchSlop: 8.0)),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverReorderableList(
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return SizedBox(
|
||||||
|
key: ValueKey<int>(items[index]),
|
||||||
|
height: 100,
|
||||||
|
child: ReorderableDragStartListener(
|
||||||
|
index: index,
|
||||||
|
child: Text('item ${items[index]}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onReorder: handleReorder,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
semantics,
|
||||||
|
includesNodeWith(
|
||||||
|
label: 'item 0',
|
||||||
|
actions: <SemanticsAction>[SemanticsAction.customAction],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final SemanticsNode node = tester.getSemantics(find.text('item 0'));
|
||||||
|
|
||||||
|
// perform custom action 'move down'.
|
||||||
|
tester.binding.pipelineOwner.semanticsOwner!.performAction(node.id, SemanticsAction.customAction, 0);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(onReorderCallCount, 1);
|
||||||
|
expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
|
||||||
|
|
||||||
|
semantics.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('SliverReorderableList custom semantics action has correct label', (WidgetTester tester) async {
|
||||||
|
const int itemCount = 5;
|
||||||
|
final List<int> items = List<int>.generate(itemCount, (int index) => index);
|
||||||
|
// The list has five elements of height 100
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(gestureSettings: DeviceGestureSettings(touchSlop: 8.0)),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: <Widget>[
|
||||||
|
SliverReorderableList(
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return SizedBox(
|
||||||
|
key: ValueKey<int>(items[index]),
|
||||||
|
height: 100,
|
||||||
|
child: ReorderableDragStartListener(
|
||||||
|
index: index,
|
||||||
|
child: Text('item ${items[index]}'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onReorder: (int _, int __) { },
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final SemanticsNode node = tester.getSemantics(find.text('item 0'));
|
||||||
|
final SemanticsData data = node.getSemanticsData();
|
||||||
|
expect(data.customSemanticsActionIds!.length, 2);
|
||||||
|
final CustomSemanticsAction action1 = CustomSemanticsAction.getAction(data.customSemanticsActionIds![0])!;
|
||||||
|
expect(action1.label, 'Move down');
|
||||||
|
final CustomSemanticsAction action2 = CustomSemanticsAction.getAction(data.customSemanticsActionIds![1])!;
|
||||||
|
expect(action2.label, 'Move to the end');
|
||||||
|
});
|
||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||||
testWidgets('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
testWidgets('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||||
bool finderCalled = false;
|
bool finderCalled = false;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user