-
Notifications
You must be signed in to change notification settings - Fork 28.6k
New Reorderable list widgets #74299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Reorderable list widgets #74299
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still working through, just submitting current review state since I saw there were updates. 🎉
ErrorDescription( | ||
'${context.widget.runtimeType} widgets require an Overlay ' | ||
'widget ancestor.\n' | ||
'An overlay lets widgets float on top of other eidget children.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'An overlay lets widgets float on top of other eidget children.' | |
'An overlay lets widgets float on top of other widget children.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
/// A callback that allows the app to add an animated decoration around | ||
/// an item when it is being dragged. | ||
/// | ||
/// The this is null, a default decoration of a Material widget with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The this => If this property
); | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing comma
@override | ||
void didUpdateWidget(ReorderableListView oldWidget) { | ||
super.didUpdateWidget(oldWidget); | ||
_listOverlayEntry.markNeedsBuild(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overlay entry depends on on a ton of widget properties, so OK to do this unconditionally. Maybe a comment explaining as much..
final Map<CustomSemanticsAction, VoidCallback> semanticsActions = <CustomSemanticsAction, VoidCallback>{}; | ||
|
||
// Create the appropriate semantics actions. | ||
void moveToStart() => reorder(index, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These actions are OK if the list is reversed? I think they're OK, not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I think so, as the indices all work the same in reverse, just the display is inverted. So moving to 0 is still moving to the start of the list, but the start of the list is displayed at the bottom instead of the top.
/// These will take care of recognizing the start of a drag gesture and call | ||
/// the list state's start item drag method. | ||
/// | ||
/// This widget's [ReorderableListState] can be used manually start a item |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be used => can be used to
/// Most applications will not use this directly, but will wrap the item | ||
/// (or part of the item, like a drag handle) in either a | ||
/// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener] | ||
/// which call this for the application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call this for the application => call this method when they detect the gesture that triggers a drag start/
setState(() { | ||
if (_insertIndex! < widget.itemCount - 1) { | ||
// Find the location of the item we want to insert before | ||
final RenderBox itemRenderBox = _items[_insertIndex!]!.context.findRenderObject()! as RenderBox; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe factor out an Offset _itemOffsetAt(int index)
method?
Offset _itemOffsetAt(int index) {
final RenderBox itemRenderBox = _items[index]!.context.findRenderObject()! as RenderBox;
return itemRenderBox.localToGlobal(Offset.zero);
}
@override | ||
void dispose() { | ||
_offsetAnimation?.dispose(); | ||
super.dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need to _listState._unregisterItem(this)
here? If the item was (somehow) explicitly disposed, then it wouldn't have been deactivated first...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my bad. I thought it had to go through deactivate before dispose. Good catch.
|
||
@override | ||
void deactivate() { | ||
_listState._unregisterItem(index, this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the item is activated later? Isn't that possible when the item is moved within the list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I should probably catch that in the build method.
return itemPosition & itemRenderBox.size; | ||
} | ||
|
||
void _update() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be the only method with a _foo private name. Maybe it should get a regular name, like rebuild()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't looked at tests
@@ -62,25 +31,30 @@ typedef ReorderCallback = void Function(int oldIndex, int newIndex); | |||
/// {@tool dartpad --template=stateful_widget_scaffold} | |||
/// | |||
/// ```dart | |||
/// List<String> _list = List.generate(5, (i) => "${i}"); | |||
/// final List<int> items = List<int>.generate(50, (int index) => index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: items => _items (this would probably be private in a real app)
/// To change the appearance or the layout of the drag handles, make | ||
/// this parameter false and wrap each list item, or a widget within | ||
/// each list item, with [ReorderableDragStartListener] or | ||
/// [ReorderableDelayedDragStartListener]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: PR description said that you could also have a custom ReaorderableListener, maybe mention that here as well?
/// {@tool dartpad --template=stateful_widget_scaffold} | ||
/// | ||
/// ```dart | ||
/// final List<int> items = List<int>.generate(50, (int index) => index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: _items ?
@@ -102,6 +74,8 @@ class ReorderableListView extends StatefulWidget { | |||
this.scrollDirection = Axis.vertical, | |||
this.padding, | |||
this.reverse = false, | |||
this.buildDefaultDragHandles = true, | |||
this.proxyDecorator, | |||
}) : assert(scrollDirection != null), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: assert(buildDefaultDragHandles != null)
/// A callback that allows the app to add an animated decoration around | ||
/// an item when it is being dragged. | ||
/// | ||
/// The this is null, a default decoration of a Material widget with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "The this"
|
||
@override | ||
void deactivate() { | ||
_listState._unregisterItem(index, this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After deactivate has been called, the object may be added to a different part of the tree. In that case, you'd have to re-register the item, but I don't think the state has a hook for that... Maybe just do the unregistering in dispose?
} | ||
} | ||
|
||
/// A wrapper widget that will recognize the start of a drag on a by a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "on a by a"
/// * [ReorderableListView], a material design list that allows the user to | ||
/// reorder its items. | ||
class ReorderableDragStartListener extends StatelessWidget { | ||
/// Creates a listener for an drag immediately following a pointer down |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: an -> a
/// subclasses can use this to customize the drag start gesture. | ||
@protected | ||
MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer({ | ||
Object? debugOwner, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make sense to have the debug owner passed in? Isn't that always gonna be "this"?
@protected | ||
MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer({ | ||
Object? debugOwner, | ||
PointerDeviceKind? kind, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand the signature of this method. Why is kind passed in here?
/// A callback that allows the app to add an animated decoration around | ||
/// an item when it is being dragged. | ||
/// | ||
/// The this is null, a default decoration of a Material widget with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// The this is null, a default decoration of a Material widget with | |
/// When this is null, a default decoration of a Material widget with |
class ReorderableListView extends StatefulWidget { | ||
|
||
/// Creates a reorderable list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add more docs here? - usually about the required params and defaults.
final EdgeInsets padding = widget.padding ?? const EdgeInsets.all(0); | ||
late EdgeInsets outerPadding; | ||
late EdgeInsets listPadding; | ||
if (widget.scrollDirection == Axis.vertical) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a switch statement
/// Defaults to false. | ||
final bool reverse; | ||
|
||
/// An object that can be used to control the position to which this scroll |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there already some macros on these types of parameters we can just sub in?
/// scroll view needs to be recomputed whenever the scroll position changes. | ||
/// | ||
/// Defaults to false. | ||
final bool shrinkWrap; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we assert this and others like it are not null above? I think we are still null checking in the framework for folks that have not migrated to null safety yet. I may be wrong though.
} | ||
|
||
SliverChildDelegate _createDelegate() { | ||
return SliverChildBuilderDelegate(_itemBuilder, childCount: widget.itemCount + (_reorderingDrag ? 1 : 0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it to account for a placeholder space?
…hen they are moved to the overlay during drag. Thanks Hans!
…causing the dragged item to appear in the wrong place.
…onal 100px drop target
Description
To address some of the shortcomings described in #66080, this PR adds two new Reorderable lists widgets and overhauls the current
ReorderableListView
to fix a lot of problems.There are now the following:
ReorderableList
, also in the widgets package.The
ReorderableList
andSliverReorderableList
allow you to customize how the drag operation starts. This is be done by wrapping each item in a drag listener that will tell the list to start dragging. We have provided two such listeners for the common cases:Or you can subclass
ReorderableDragStartListener
and build your own listener.One of these is required by the two widget lists, but the material
ReorderableListView
will automatically wrap the child in a delayed listener for mobile and adds a default 'dragHandle' on top of each item by default on desktops. These can be turned off if you want to build your own.While this doesn't address all the issues, hopefully it will be a good improvement on what is currently there and make it easier for you to build what you need. We can follow this up with more enhancements in the future.
Updated Material
ReorderableListView
ReorderableList
with toggle for drag handles and refresh indicatorA horizontal list with custom proxy decorator
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.