Skip to content

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

Merged
merged 31 commits into from
Jan 23, 2021
Merged

New Reorderable list widgets #74299

merged 31 commits into from
Jan 23, 2021

Conversation

darrenaustin
Copy link
Contributor

@darrenaustin darrenaustin commented Jan 20, 2021

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 - a lower level flexible widget that allows the user to interactively move items around the list. It is design-language agnostic and lives in the widgets package.
  • SliverReorderableList - a SliverList version of the ReorderableList, also in the widgets package.
  • ReorderableListView - A Material Design implementation of the reorderable list updated to use the SliverRerorderableList internally.

The ReorderableList and SliverReorderableList 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:

  • ReorderableDragStartListener which triggers the drag from a single pointer down event (useful for wrapping just a drag handle instead of the whole item).
  • ReorderableDelayedDragStartListener which triggers after a long press.

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

ReorderableListView

ReorderableList with toggle for drag handles and refresh indicator

ReorderableList

A horizontal list with custom proxy decorator

Horizontal

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Sorry, something went wrong.

@flutter-dashboard flutter-dashboard bot added f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Jan 20, 2021
@google-cla google-cla bot added the cla: yes label Jan 20, 2021
@google-cla

This comment has been minimized.

@google-cla google-cla bot added cla: no and removed cla: yes labels Jan 21, 2021
@darrenaustin darrenaustin marked this pull request as ready for review January 22, 2021 20:06
@darrenaustin darrenaustin changed the title WIP: New Reorderable list widgets New Reorderable list widgets Jan 22, 2021
@Piinks Piinks added f: scrolling Viewports, list views, slivers, etc. c: new feature Nothing broken; request for a new capability labels Jan 22, 2021
Copy link
Contributor

@Piinks Piinks left a 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.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'An overlay lets widgets float on top of other eidget children.'
'An overlay lets widgets float on top of other widget children.'

Copy link
Contributor

@HansMuller HansMuller left a 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
Copy link
Contributor

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

);
},
}
Copy link
Contributor

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();
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.
Copy link
Contributor

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;
Copy link
Contributor

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();
Copy link
Contributor

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...

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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()

Copy link
Member

@goderbauer goderbauer left a 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);
Copy link
Member

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].
Copy link
Member

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);
Copy link
Member

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),
Copy link
Member

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
Copy link
Member

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);
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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,
Copy link
Member

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,
Copy link
Member

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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.
Copy link
Contributor

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) {
Copy link
Contributor

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
Copy link
Contributor

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;
Copy link
Contributor

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));
Copy link
Contributor

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: new feature Nothing broken; request for a new capability f: material design flutter/packages/flutter/material repository. f: scrolling Viewports, list views, slivers, etc. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants