Skip to content

Conversation

@rkishan516
Copy link
Contributor

Fix: Flicker when no update in index of dragged item

Resolves #150843

This PR ensures that even if we move dragged item anywhere in list and comeback to initial position, we smoothly adjust to that position.

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 signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact "@test-exemption-reviewer" in the #hackers channel in Chat (don't just cc them here, they won't see it! Use Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@github-actions github-actions bot added the framework flutter/packages/flutter repository. See also f: labels. label Jun 29, 2024
@goderbauer
Copy link
Member

@rkishan516 Thank you for the contribution. It looks like it is failing some checks. Can you please take a look? Also, as per the bot message above: Can you add a test for this so we never regress this in the future? Thank you.

@rkishan516
Copy link
Contributor Author

@goderbauer Sure, I will check failing checks and add tests.

@rkishan516
Copy link
Contributor Author

rkishan516 commented Jul 6, 2024

@goderbauer I have added regression test for this PR, but need some help with other failing test.

@goderbauer
Copy link
Member

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch 3 times, most recently from 09099f1 to 3e2507b Compare July 19, 2024 13:48
@rkishan516
Copy link
Contributor Author

@goderbauer I have fixed the tests, But I might have messed up in code quality. Can you please review ?

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch 5 times, most recently from ad0ce2d to 20b96cb Compare July 19, 2024 14:04
@chunhtai chunhtai requested a review from Piinks July 23, 2024 22:23
@chunhtai
Copy link
Contributor

Hi @Piinks Can you take a look at this pr? feel free to reassign if not.

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from 20b96cb to 6b6a21e Compare July 25, 2024 03:11
@Piinks Piinks added the f: scrolling Viewports, list views, slivers, etc. label Jul 29, 2024
@github-actions github-actions bot removed the f: scrolling Viewports, list views, slivers, etc. label Aug 2, 2024
@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from 0a3ed86 to df87d63 Compare August 5, 2024 23:22
@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from 0535985 to 42dc0ea Compare August 22, 2024 01:28
expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 500));
});

testWidgets('SliverReorderableList - properly animates the drop at starting position', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a similar test where reverse: true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Piinks I have pushed the required test.

int newIndex = _insertIndex!;
for (final _ReorderableItemState item in _items.values) {
if (item.index == _dragIndex! || !item.mounted) {
if ((_reverse && item.index == _dragIndex!) || !item.mounted) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a test in reverse as well to cover this case.

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch 2 times, most recently from d7e2805 to 5c4042b Compare September 3, 2024 06:20
@github-actions github-actions bot added the f: material design flutter/packages/flutter/material repository. label Sep 3, 2024
@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from 5c4042b to 585b3c8 Compare September 6, 2024 01:18
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.

@rkishan516 this looks just about ready to land, can you check the changes in the dropdown menu file? Maybe they were pulled in accidentally from a merge.

int? search(List<DropdownMenuEntry<T>> entries, TextEditingController textEditingController) {
final String searchText = textEditingController.value.text.toLowerCase();
int? search(List<DropdownMenuEntry<T>> entries) {
final String searchText = _localTextEditingController!.value.text.toLowerCase();
Copy link
Contributor

Choose a reason for hiding this comment

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

@rkishan516 should these changes be here? They appear unrelated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I don't know, How I messed it up today. I have fixed and pushed.

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from c8e2b63 to 5a0a5f1 Compare September 11, 2024 18:05
@github-actions github-actions bot removed the f: material design flutter/packages/flutter/material repository. label Sep 11, 2024
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.

LGTM, thank you!

@Piinks Piinks requested a review from QuncCccccc September 11, 2024 18:21
Copy link
Contributor

@QuncCccccc QuncCccccc left a comment

Choose a reason for hiding this comment

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

LGTM with nits:) Thanks for the contribution!

} else {
if (itemStart <= proxyItemStart && proxyItemStart <= itemMiddle) {
if (item.index == _dragIndex!) {
// If end of the proxy is in not in ending half of item
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo?

Suggested change
// If end of the proxy is in not in ending half of item
// If end of the proxy is not in ending half of item,

if (itemStart <= proxyItemStart && proxyItemStart <= itemMiddle) {
if (item.index == _dragIndex!) {
// If end of the proxy is in not in ending half of item
// We don't process, because its drag item index
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
// We don't process, because its drag item index
// we don't process, because its drag item index.

This comment looks a little bit confusing, could you help double check? "because it's the original dragged item"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@QuncCccccc I have pushed the required changes.

@rkishan516 rkishan516 force-pushed the reorderable-list-flicker branch from 5a0a5f1 to 93a2ca0 Compare September 12, 2024 01:54
@Piinks Piinks force-pushed the reorderable-list-flicker branch from 283d5f3 to f3a82d7 Compare September 13, 2024 16:16
@Piinks Piinks added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 13, 2024
@auto-submit auto-submit bot merged commit bbc17fa into flutter:master Sep 13, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 13, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 13, 2024
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Sep 13, 2024
flutter/flutter@303f222...2d30fe4

2024-09-13 [email protected] Roll Packages from 91caa7a to 330581f (4 revisions) (flutter/flutter#155171)
2024-09-13 [email protected] Fix: Flicker when reorderable list doesn't change its position (flutter/flutter#151026)
2024-09-13 [email protected] Stop reading .packages from flutter_tools. (flutter/flutter#154912)
2024-09-13 [email protected] Roll Flutter Engine from 70109e3b40c0 to bef48e87f438 (1 revision) (flutter/flutter#155156)
2024-09-13 [email protected] Roll Flutter Engine from d917a15823f3 to 70109e3b40c0 (1 revision) (flutter/flutter#155151)
2024-09-13 [email protected] Roll Flutter Engine from 94696ed75dea to d917a15823f3 (1 revision) (flutter/flutter#155147)
2024-09-13 [email protected] Fix TextField content should be selected on desktop when gaining focus (flutter/flutter#154916)
2024-09-13 [email protected] Roll Flutter Engine from 04802b779045 to 94696ed75dea (1 revision) (flutter/flutter#155144)
2024-09-13 [email protected] Roll Flutter Engine from 3d8163f47919 to 04802b779045 (2 revisions) (flutter/flutter#155138)
2024-09-13 [email protected] Roll Flutter Engine from 4d5fea97e933 to 3d8163f47919 (1 revision) (flutter/flutter#155136)
2024-09-13 [email protected] Mark `_LayoutBuilderElement` as always clean (flutter/flutter#154694)
2024-09-13 [email protected] Roll Flutter Engine from 8609af642725 to 4d5fea97e933 (7 revisions) (flutter/flutter#155134)
2024-09-12 [email protected] Disable fuchsia in flutter_tools (flutter/flutter#155111)
2024-09-12 [email protected] Address frame policy benchmark flakes (flutter/flutter#155130)
2024-09-12 [email protected] Roll Flutter Engine from 48ddaf578fb0 to 8609af642725 (11 revisions) (flutter/flutter#155128)
2024-09-12 49699333+dependabot[bot]@users.noreply.github.com Bump peter-evans/create-pull-request from 7.0.1 to 7.0.2 (flutter/flutter#155126)
2024-09-12 [email protected] Prevent the keyboard from reshowing on iOS (flutter/flutter#154584)
2024-09-12 [email protected] fix(Linux): specify application id (flutter/flutter#154522)
2024-09-12 [email protected] update changelog on master (flutter/flutter#155109)
2024-09-12 [email protected] iOS: update provisioning profile for 2024-2025 cert (flutter/flutter#155101)
2024-09-12 [email protected] Roll Packages from 4c18648 to 91caa7a (2 revisions) (flutter/flutter#155103)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Dec 11, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Dec 12, 2024
@rkishan516 rkishan516 deleted the reorderable-list-flicker branch July 26, 2025 05:46
github-merge-queue bot pushed a commit that referenced this pull request Aug 7, 2025
_**Note:** Alongside this PR, I've also prepared [another
PR](#172882) with an alternative
solution involving a more substantial refactor that addresses the root
cause, rather than adding more conditional logic._

## Description

This PR fixes the proxy animation bug where dragging a `ReorderableList`
item downward and then back to its original position causes it to
animate to the wrong location (one position too low).

## The Problem

When dragging a `ReorderableList` item downward and then back to its
original position, the proxy widget briefly animates to the wrong
location (one position too low) before snapping to the correct spot.

**Reproduction**: Drag any item down past at least one other item, then
drag it back to where it started.

<p align="center">
<img
src="https://github.com/user-attachments/assets/d0931dff-5600-441c-8536-2c61789767d0"
alt="demo2" width="250">
</p>

## Root Cause

This bug is specific to dragging an item down and then bringing it back
up to nearly (but not 100% of the way ) to its original position:

1. When the item approaches its original position **from below**,
`_insertIndex` becomes `item.index + 1`
- This happens because Flutter's `ReorderableList` calculates
`_insertIndex` with the dragged item still present in the list (see
#24786)
2. The proxy _should_ animate to the item's original position at
`item.index`
    - _But the proxy actually animates one position too low._
    - This happens because `_dragEnd` incorrectly calculates 
    `_finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
  _extentOffset(...)`
- The `_extentOffset(...)` addition, designed for items dropping
_between other items_, shifts the position down by one item's height
- The correct calculation for "returning home from below" should be just
`_itemOffsetAt(_insertIndex! - 1)`

Note that this only occurs when returning from below (`_insertIndex >
item.index`). Dragging upward (in a vertical list for example) or
doesn't trigger this bug.

## Existing Implementation

The existing `_dragEnd` method in `reorderable_list.dart`:

```dart
void _dragEnd(_DragInfo item) {
  setState(() {
    if (_insertIndex == item.index) {
      _finalDropPosition = _itemOffsetAt(_insertIndex!);
    } else if (_reverse) {
      if (_insertIndex! >= _items.length) {
        _finalDropPosition =
            _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition =
            _itemOffsetAt(_insertIndex!) +
            _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
      }
    } else {
      if (_insertIndex! == 0) {
        _finalDropPosition = _itemOffsetAt(0) - 
          _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
          _extentOffset(_itemExtentAt(_insertIndex! - 1), _scrollDirection);
      }
    }
  });
}
```

When returning from below, the code falls through to the final else
block, which incorrectly adds `_extentOffset`.

## Fix

Detect when `_insertIndex - item.index == 1` (indicating a return to
original position from below) and animate to the correct position.

```dart
if (_insertIndex! - item.index == 1) {
  // Drop at the original position when item returns from below
  _finalDropPosition = _itemOffsetAt(_insertIndex! - 1);
}
```

This fix was proposed by @frankpape in
#90856 (comment);
I've merely validated and researched the background of why the fix
works, and supported it with tests.

**_Demo of the fixed implementation:_**
<p align="center">
<img
src="https://github.com/user-attachments/assets/a53e8920-ebca-4326-abe9-3b43b34419e5"
alt="fixed" width="250">
</p>

Fixes #88331
Fixes #90856
Fixes #150843

## A note about a previous PR: 

While investigating this issue, I found a PR addressing what seemed to
be [the same exact
issue](#150843): PR #151026; it
turns out that that PR solved a _portion_ of the edge case: the case
where an item is dragged down and back and slightly **overshoots** its
original position when being dragged back & dropped—but that PR did not
account for the presence of this bug when the dragged item slightly
**undershoots** its original position on the return drag. This new PR
effectively addresses the 'undershooting' case.

With this, I've added a new pair of regression tests that are identical
to the [previous PR's
tests](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/reorderable_list_test.dart#L734),
except for the fact that they simulate an undershoot on the return trip
(90% of the way back instead of 110% like the original tests). This
definitively captures the issue, failing in the master branch and
passing in this PR's branch.

Here is the specific case resolved by the [**old**
PR](#151026):
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/b0ddc745-6e9e-4f12-97da-454e2e76b06d"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/03e181fa-f43b-4405-b0c0-16d3465ad990"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Here is the additional case resolved by **this** PR:
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/9b4bb591-aa2f-4cf0-88b8-a3ec32b0f0ac"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/31646e9c-78f4-4252-921f-53583193868f"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Two final observations worth noting:
- The fix proposed in this PR seems to **supersede** the previous PR's
solution; it addresses both cases (overshooting and undershooting) even
in my tests with the [original PR's changes
](https://github.com/flutter/flutter/pull/151026/files#diff-23a4bb073009d89f09084bdf5f85232de135b8f11be625e6312bb85900a90e67)
reverted. Probably best to keep the old PR's code anyway to be
conservative, but noteworthy.
- I also found it notable that neither this PR nor the older PR fix any
issue with "reversed lists", which, in my tests, are simply not subject
to this edge case as we've defined it. The regression tests added for
the reverse case are thus purely precautionary.

## Pre-launch Checklist

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
ksokolovskyi pushed a commit to ksokolovskyi/flutter that referenced this pull request Aug 19, 2025
…172380)

_**Note:** Alongside this PR, I've also prepared [another
PR](flutter#172882) with an alternative
solution involving a more substantial refactor that addresses the root
cause, rather than adding more conditional logic._

## Description

This PR fixes the proxy animation bug where dragging a `ReorderableList`
item downward and then back to its original position causes it to
animate to the wrong location (one position too low).

## The Problem

When dragging a `ReorderableList` item downward and then back to its
original position, the proxy widget briefly animates to the wrong
location (one position too low) before snapping to the correct spot.

**Reproduction**: Drag any item down past at least one other item, then
drag it back to where it started.

<p align="center">
<img
src="https://github.com/user-attachments/assets/d0931dff-5600-441c-8536-2c61789767d0"
alt="demo2" width="250">
</p>

## Root Cause

This bug is specific to dragging an item down and then bringing it back
up to nearly (but not 100% of the way ) to its original position:

1. When the item approaches its original position **from below**,
`_insertIndex` becomes `item.index + 1`
- This happens because Flutter's `ReorderableList` calculates
`_insertIndex` with the dragged item still present in the list (see
flutter#24786)
2. The proxy _should_ animate to the item's original position at
`item.index`
    - _But the proxy actually animates one position too low._
    - This happens because `_dragEnd` incorrectly calculates 
    `_finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
  _extentOffset(...)`
- The `_extentOffset(...)` addition, designed for items dropping
_between other items_, shifts the position down by one item's height
- The correct calculation for "returning home from below" should be just
`_itemOffsetAt(_insertIndex! - 1)`

Note that this only occurs when returning from below (`_insertIndex >
item.index`). Dragging upward (in a vertical list for example) or
doesn't trigger this bug.

## Existing Implementation

The existing `_dragEnd` method in `reorderable_list.dart`:

```dart
void _dragEnd(_DragInfo item) {
  setState(() {
    if (_insertIndex == item.index) {
      _finalDropPosition = _itemOffsetAt(_insertIndex!);
    } else if (_reverse) {
      if (_insertIndex! >= _items.length) {
        _finalDropPosition =
            _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition =
            _itemOffsetAt(_insertIndex!) +
            _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
      }
    } else {
      if (_insertIndex! == 0) {
        _finalDropPosition = _itemOffsetAt(0) - 
          _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
          _extentOffset(_itemExtentAt(_insertIndex! - 1), _scrollDirection);
      }
    }
  });
}
```

When returning from below, the code falls through to the final else
block, which incorrectly adds `_extentOffset`.

## Fix

Detect when `_insertIndex - item.index == 1` (indicating a return to
original position from below) and animate to the correct position.

```dart
if (_insertIndex! - item.index == 1) {
  // Drop at the original position when item returns from below
  _finalDropPosition = _itemOffsetAt(_insertIndex! - 1);
}
```

This fix was proposed by @frankpape in
flutter#90856 (comment);
I've merely validated and researched the background of why the fix
works, and supported it with tests.

**_Demo of the fixed implementation:_**
<p align="center">
<img
src="https://github.com/user-attachments/assets/a53e8920-ebca-4326-abe9-3b43b34419e5"
alt="fixed" width="250">
</p>

Fixes flutter#88331
Fixes flutter#90856
Fixes flutter#150843

## A note about a previous PR: 

While investigating this issue, I found a PR addressing what seemed to
be [the same exact
issue](flutter#150843): PR flutter#151026; it
turns out that that PR solved a _portion_ of the edge case: the case
where an item is dragged down and back and slightly **overshoots** its
original position when being dragged back & dropped—but that PR did not
account for the presence of this bug when the dragged item slightly
**undershoots** its original position on the return drag. This new PR
effectively addresses the 'undershooting' case.

With this, I've added a new pair of regression tests that are identical
to the [previous PR's
tests](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/reorderable_list_test.dart#L734),
except for the fact that they simulate an undershoot on the return trip
(90% of the way back instead of 110% like the original tests). This
definitively captures the issue, failing in the master branch and
passing in this PR's branch.

Here is the specific case resolved by the [**old**
PR](flutter#151026):
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/b0ddc745-6e9e-4f12-97da-454e2e76b06d"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/03e181fa-f43b-4405-b0c0-16d3465ad990"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Here is the additional case resolved by **this** PR:
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/9b4bb591-aa2f-4cf0-88b8-a3ec32b0f0ac"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/31646e9c-78f4-4252-921f-53583193868f"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Two final observations worth noting:
- The fix proposed in this PR seems to **supersede** the previous PR's
solution; it addresses both cases (overshooting and undershooting) even
in my tests with the [original PR's changes
](https://github.com/flutter/flutter/pull/151026/files#diff-23a4bb073009d89f09084bdf5f85232de135b8f11be625e6312bb85900a90e67)
reverted. Probably best to keep the old PR's code anyway to be
conservative, but noteworthy.
- I also found it notable that neither this PR nor the older PR fix any
issue with "reversed lists", which, in my tests, are simply not subject
to this edge case as we've defined it. The regression tests added for
the reverse case are thus purely precautionary.

## Pre-launch Checklist

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
mboetger pushed a commit to mboetger/flutter that referenced this pull request Sep 18, 2025
…172380)

_**Note:** Alongside this PR, I've also prepared [another
PR](flutter#172882) with an alternative
solution involving a more substantial refactor that addresses the root
cause, rather than adding more conditional logic._

## Description

This PR fixes the proxy animation bug where dragging a `ReorderableList`
item downward and then back to its original position causes it to
animate to the wrong location (one position too low).

## The Problem

When dragging a `ReorderableList` item downward and then back to its
original position, the proxy widget briefly animates to the wrong
location (one position too low) before snapping to the correct spot.

**Reproduction**: Drag any item down past at least one other item, then
drag it back to where it started.

<p align="center">
<img
src="https://github.com/user-attachments/assets/d0931dff-5600-441c-8536-2c61789767d0"
alt="demo2" width="250">
</p>

## Root Cause

This bug is specific to dragging an item down and then bringing it back
up to nearly (but not 100% of the way ) to its original position:

1. When the item approaches its original position **from below**,
`_insertIndex` becomes `item.index + 1`
- This happens because Flutter's `ReorderableList` calculates
`_insertIndex` with the dragged item still present in the list (see
flutter#24786)
2. The proxy _should_ animate to the item's original position at
`item.index`
    - _But the proxy actually animates one position too low._
    - This happens because `_dragEnd` incorrectly calculates 
    `_finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
  _extentOffset(...)`
- The `_extentOffset(...)` addition, designed for items dropping
_between other items_, shifts the position down by one item's height
- The correct calculation for "returning home from below" should be just
`_itemOffsetAt(_insertIndex! - 1)`

Note that this only occurs when returning from below (`_insertIndex >
item.index`). Dragging upward (in a vertical list for example) or
doesn't trigger this bug.

## Existing Implementation

The existing `_dragEnd` method in `reorderable_list.dart`:

```dart
void _dragEnd(_DragInfo item) {
  setState(() {
    if (_insertIndex == item.index) {
      _finalDropPosition = _itemOffsetAt(_insertIndex!);
    } else if (_reverse) {
      if (_insertIndex! >= _items.length) {
        _finalDropPosition =
            _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition =
            _itemOffsetAt(_insertIndex!) +
            _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
      }
    } else {
      if (_insertIndex! == 0) {
        _finalDropPosition = _itemOffsetAt(0) - 
          _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
          _extentOffset(_itemExtentAt(_insertIndex! - 1), _scrollDirection);
      }
    }
  });
}
```

When returning from below, the code falls through to the final else
block, which incorrectly adds `_extentOffset`.

## Fix

Detect when `_insertIndex - item.index == 1` (indicating a return to
original position from below) and animate to the correct position.

```dart
if (_insertIndex! - item.index == 1) {
  // Drop at the original position when item returns from below
  _finalDropPosition = _itemOffsetAt(_insertIndex! - 1);
}
```

This fix was proposed by @frankpape in
flutter#90856 (comment);
I've merely validated and researched the background of why the fix
works, and supported it with tests.

**_Demo of the fixed implementation:_**
<p align="center">
<img
src="https://github.com/user-attachments/assets/a53e8920-ebca-4326-abe9-3b43b34419e5"
alt="fixed" width="250">
</p>

Fixes flutter#88331
Fixes flutter#90856
Fixes flutter#150843

## A note about a previous PR: 

While investigating this issue, I found a PR addressing what seemed to
be [the same exact
issue](flutter#150843): PR flutter#151026; it
turns out that that PR solved a _portion_ of the edge case: the case
where an item is dragged down and back and slightly **overshoots** its
original position when being dragged back & dropped—but that PR did not
account for the presence of this bug when the dragged item slightly
**undershoots** its original position on the return drag. This new PR
effectively addresses the 'undershooting' case.

With this, I've added a new pair of regression tests that are identical
to the [previous PR's
tests](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/reorderable_list_test.dart#L734),
except for the fact that they simulate an undershoot on the return trip
(90% of the way back instead of 110% like the original tests). This
definitively captures the issue, failing in the master branch and
passing in this PR's branch.

Here is the specific case resolved by the [**old**
PR](flutter#151026):
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/b0ddc745-6e9e-4f12-97da-454e2e76b06d"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/03e181fa-f43b-4405-b0c0-16d3465ad990"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Here is the additional case resolved by **this** PR:
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/9b4bb591-aa2f-4cf0-88b8-a3ec32b0f0ac"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/31646e9c-78f4-4252-921f-53583193868f"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Two final observations worth noting:
- The fix proposed in this PR seems to **supersede** the previous PR's
solution; it addresses both cases (overshooting and undershooting) even
in my tests with the [original PR's changes
](https://github.com/flutter/flutter/pull/151026/files#diff-23a4bb073009d89f09084bdf5f85232de135b8f11be625e6312bb85900a90e67)
reverted. Probably best to keep the old PR's code anyway to be
conservative, but noteworthy.
- I also found it notable that neither this PR nor the older PR fix any
issue with "reversed lists", which, in my tests, are simply not subject
to this edge case as we've defined it. The regression tests added for
the reverse case are thus purely precautionary.

## Pre-launch Checklist

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
korca0220 pushed a commit to korca0220/flutter that referenced this pull request Sep 22, 2025
…172380)

_**Note:** Alongside this PR, I've also prepared [another
PR](flutter#172882) with an alternative
solution involving a more substantial refactor that addresses the root
cause, rather than adding more conditional logic._

## Description

This PR fixes the proxy animation bug where dragging a `ReorderableList`
item downward and then back to its original position causes it to
animate to the wrong location (one position too low).

## The Problem

When dragging a `ReorderableList` item downward and then back to its
original position, the proxy widget briefly animates to the wrong
location (one position too low) before snapping to the correct spot.

**Reproduction**: Drag any item down past at least one other item, then
drag it back to where it started.

<p align="center">
<img
src="https://github.com/user-attachments/assets/d0931dff-5600-441c-8536-2c61789767d0"
alt="demo2" width="250">
</p>

## Root Cause

This bug is specific to dragging an item down and then bringing it back
up to nearly (but not 100% of the way ) to its original position:

1. When the item approaches its original position **from below**,
`_insertIndex` becomes `item.index + 1`
- This happens because Flutter's `ReorderableList` calculates
`_insertIndex` with the dragged item still present in the list (see
flutter#24786)
2. The proxy _should_ animate to the item's original position at
`item.index`
    - _But the proxy actually animates one position too low._
    - This happens because `_dragEnd` incorrectly calculates 
    `_finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
  _extentOffset(...)`
- The `_extentOffset(...)` addition, designed for items dropping
_between other items_, shifts the position down by one item's height
- The correct calculation for "returning home from below" should be just
`_itemOffsetAt(_insertIndex! - 1)`

Note that this only occurs when returning from below (`_insertIndex >
item.index`). Dragging upward (in a vertical list for example) or
doesn't trigger this bug.

## Existing Implementation

The existing `_dragEnd` method in `reorderable_list.dart`:

```dart
void _dragEnd(_DragInfo item) {
  setState(() {
    if (_insertIndex == item.index) {
      _finalDropPosition = _itemOffsetAt(_insertIndex!);
    } else if (_reverse) {
      if (_insertIndex! >= _items.length) {
        _finalDropPosition =
            _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition =
            _itemOffsetAt(_insertIndex!) +
            _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
      }
    } else {
      if (_insertIndex! == 0) {
        _finalDropPosition = _itemOffsetAt(0) - 
          _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
          _extentOffset(_itemExtentAt(_insertIndex! - 1), _scrollDirection);
      }
    }
  });
}
```

When returning from below, the code falls through to the final else
block, which incorrectly adds `_extentOffset`.

## Fix

Detect when `_insertIndex - item.index == 1` (indicating a return to
original position from below) and animate to the correct position.

```dart
if (_insertIndex! - item.index == 1) {
  // Drop at the original position when item returns from below
  _finalDropPosition = _itemOffsetAt(_insertIndex! - 1);
}
```

This fix was proposed by @frankpape in
flutter#90856 (comment);
I've merely validated and researched the background of why the fix
works, and supported it with tests.

**_Demo of the fixed implementation:_**
<p align="center">
<img
src="https://github.com/user-attachments/assets/a53e8920-ebca-4326-abe9-3b43b34419e5"
alt="fixed" width="250">
</p>

Fixes flutter#88331
Fixes flutter#90856
Fixes flutter#150843

## A note about a previous PR: 

While investigating this issue, I found a PR addressing what seemed to
be [the same exact
issue](flutter#150843): PR flutter#151026; it
turns out that that PR solved a _portion_ of the edge case: the case
where an item is dragged down and back and slightly **overshoots** its
original position when being dragged back & dropped—but that PR did not
account for the presence of this bug when the dragged item slightly
**undershoots** its original position on the return drag. This new PR
effectively addresses the 'undershooting' case.

With this, I've added a new pair of regression tests that are identical
to the [previous PR's
tests](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/reorderable_list_test.dart#L734),
except for the fact that they simulate an undershoot on the return trip
(90% of the way back instead of 110% like the original tests). This
definitively captures the issue, failing in the master branch and
passing in this PR's branch.

Here is the specific case resolved by the [**old**
PR](flutter#151026):
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/b0ddc745-6e9e-4f12-97da-454e2e76b06d"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/03e181fa-f43b-4405-b0c0-16d3465ad990"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Here is the additional case resolved by **this** PR:
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/9b4bb591-aa2f-4cf0-88b8-a3ec32b0f0ac"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/31646e9c-78f4-4252-921f-53583193868f"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Two final observations worth noting:
- The fix proposed in this PR seems to **supersede** the previous PR's
solution; it addresses both cases (overshooting and undershooting) even
in my tests with the [original PR's changes
](https://github.com/flutter/flutter/pull/151026/files#diff-23a4bb073009d89f09084bdf5f85232de135b8f11be625e6312bb85900a90e67)
reverted. Probably best to keep the old PR's code anyway to be
conservative, but noteworthy.
- I also found it notable that neither this PR nor the older PR fix any
issue with "reversed lists", which, in my tests, are simply not subject
to this edge case as we've defined it. The regression tests added for
the reverse case are thus purely precautionary.

## Pre-launch Checklist

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
lucaantonelli pushed a commit to lucaantonelli/flutter that referenced this pull request Nov 21, 2025
…172380)

_**Note:** Alongside this PR, I've also prepared [another
PR](flutter#172882) with an alternative
solution involving a more substantial refactor that addresses the root
cause, rather than adding more conditional logic._

## Description

This PR fixes the proxy animation bug where dragging a `ReorderableList`
item downward and then back to its original position causes it to
animate to the wrong location (one position too low).

## The Problem

When dragging a `ReorderableList` item downward and then back to its
original position, the proxy widget briefly animates to the wrong
location (one position too low) before snapping to the correct spot.

**Reproduction**: Drag any item down past at least one other item, then
drag it back to where it started.

<p align="center">
<img
src="https://github.com/user-attachments/assets/d0931dff-5600-441c-8536-2c61789767d0"
alt="demo2" width="250">
</p>

## Root Cause

This bug is specific to dragging an item down and then bringing it back
up to nearly (but not 100% of the way ) to its original position:

1. When the item approaches its original position **from below**,
`_insertIndex` becomes `item.index + 1`
- This happens because Flutter's `ReorderableList` calculates
`_insertIndex` with the dragged item still present in the list (see
flutter#24786)
2. The proxy _should_ animate to the item's original position at
`item.index`
    - _But the proxy actually animates one position too low._
    - This happens because `_dragEnd` incorrectly calculates 
    `_finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
  _extentOffset(...)`
- The `_extentOffset(...)` addition, designed for items dropping
_between other items_, shifts the position down by one item's height
- The correct calculation for "returning home from below" should be just
`_itemOffsetAt(_insertIndex! - 1)`

Note that this only occurs when returning from below (`_insertIndex >
item.index`). Dragging upward (in a vertical list for example) or
doesn't trigger this bug.

## Existing Implementation

The existing `_dragEnd` method in `reorderable_list.dart`:

```dart
void _dragEnd(_DragInfo item) {
  setState(() {
    if (_insertIndex == item.index) {
      _finalDropPosition = _itemOffsetAt(_insertIndex!);
    } else if (_reverse) {
      if (_insertIndex! >= _items.length) {
        _finalDropPosition =
            _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition =
            _itemOffsetAt(_insertIndex!) +
            _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
      }
    } else {
      if (_insertIndex! == 0) {
        _finalDropPosition = _itemOffsetAt(0) - 
          _extentOffset(item.itemExtent, _scrollDirection);
      } else {
        _finalDropPosition = _itemOffsetAt(_insertIndex! - 1) +
          _extentOffset(_itemExtentAt(_insertIndex! - 1), _scrollDirection);
      }
    }
  });
}
```

When returning from below, the code falls through to the final else
block, which incorrectly adds `_extentOffset`.

## Fix

Detect when `_insertIndex - item.index == 1` (indicating a return to
original position from below) and animate to the correct position.

```dart
if (_insertIndex! - item.index == 1) {
  // Drop at the original position when item returns from below
  _finalDropPosition = _itemOffsetAt(_insertIndex! - 1);
}
```

This fix was proposed by @frankpape in
flutter#90856 (comment);
I've merely validated and researched the background of why the fix
works, and supported it with tests.

**_Demo of the fixed implementation:_**
<p align="center">
<img
src="https://github.com/user-attachments/assets/a53e8920-ebca-4326-abe9-3b43b34419e5"
alt="fixed" width="250">
</p>

Fixes flutter#88331
Fixes flutter#90856
Fixes flutter#150843

## A note about a previous PR: 

While investigating this issue, I found a PR addressing what seemed to
be [the same exact
issue](flutter#150843): PR flutter#151026; it
turns out that that PR solved a _portion_ of the edge case: the case
where an item is dragged down and back and slightly **overshoots** its
original position when being dragged back & dropped—but that PR did not
account for the presence of this bug when the dragged item slightly
**undershoots** its original position on the return drag. This new PR
effectively addresses the 'undershooting' case.

With this, I've added a new pair of regression tests that are identical
to the [previous PR's
tests](https://github.com/flutter/flutter/blob/master/packages/flutter/test/widgets/reorderable_list_test.dart#L734),
except for the fact that they simulate an undershoot on the return trip
(90% of the way back instead of 110% like the original tests). This
definitively captures the issue, failing in the master branch and
passing in this PR's branch.

Here is the specific case resolved by the [**old**
PR](flutter#151026):
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/b0ddc745-6e9e-4f12-97da-454e2e76b06d"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/03e181fa-f43b-4405-b0c0-16d3465ad990"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Here is the additional case resolved by **this** PR:
<table>
  <tr>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/9b4bb591-aa2f-4cf0-88b8-a3ec32b0f0ac"
alt="Before" width="200"><br>
      <sub>Before</sub>
    </td>
    <td align="center">
<img
src="https://github.com/user-attachments/assets/31646e9c-78f4-4252-921f-53583193868f"
alt="After" width="200"><br>
      <sub>After</sub>
    </td>
  </tr>
</table>

Two final observations worth noting:
- The fix proposed in this PR seems to **supersede** the previous PR's
solution; it addresses both cases (overshooting and undershooting) even
in my tests with the [original PR's changes
](https://github.com/flutter/flutter/pull/151026/files#diff-23a4bb073009d89f09084bdf5f85232de135b8f11be625e6312bb85900a90e67)
reverted. Probably best to keep the old PR's code anyway to be
conservative, but noteworthy.
- I also found it notable that neither this PR nor the older PR fix any
issue with "reversed lists", which, in my tests, are simply not subject
to this edge case as we've defined it. The regression tests added for
the reverse case are thus purely precautionary.

## Pre-launch Checklist

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReorderableList - incorrect newIndex when dragging an item down and returning it to its original position

5 participants