Repository navigation
Description
Steps to reproduce
- Build a
ReorderableListView(orSliverReorderableList) whose items have different heights, for example[68, 85, 126, 97, 54, 106, 110, 119]. - Long-press the item of height 106 (index 5) and drag it up, slowly, over the item of height 54 (index 4) above it.
- Keep the finger moving slowly while the top of the dragged item is in the upper half of item 4 (a band of about 26 px here).
The widget test below reproduces it deterministically.
Expected results
Item 4 slides down once and stays down while the finger rests or creeps inside that band. Each new position of the finger gives the same gap position as the previous check.
Actual results
Item 4 flips between its resting position and its pushed-down position on every pointer move, however small (0.5 px in the test). On a device this looks like the rows below the dragged item flickering, and any app reacting to the gap moving (for example with a haptic) fires repeatedly.
The gap calculation in SliverReorderableListState._dragUpdateItems is not idempotent in this situation:
- In state "gap above item 4" (item 4 shifted down by the drag extent), nothing in the loop matches except the dragged item's own slot, whose branch
if (item.index == _dragIndex!) { if (itemMiddle <= proxyItemEnd && proxyItemEnd <= itemEnd) newIndex = _dragIndex!; }sets the gap back to the dragged index. Item 4 moves back up. - In state "gap in own slot", item 4 is at rest and its branch
itemStart <= proxyItemStart && proxyItemStart <= itemMiddlemoves the gap above item 4 again.
Both branches match whenever the dragged item is taller than the item above it: the proxy's top is in that item's upper half while the proxy's bottom is still in the lower half of its own slot. The band is [max(top_k, top_k + h - g/2), top_k + h/2] for a dragged extent g and an upper neighbour of height h, so it never occurs with equal-height items, which is probably why it went unnoticed.
The own-slot branch was added in #151026 (for #150843). Since #172380, _dragEnd already treats _insertIndex == item.index + 1 as "returned to the start", and _handleReorderItem already normalises newIndex > oldIndex, so that branch seems redundant now. Skipping the dragged item in the loop for the forward direction, as the reverse branch already does (if (item.index == _dragIndex! || !item.mounted) continue;), fixes the flip, and the existing regression tests for #150843, #88331 and #110949 still pass with that change (checked on 3.44.1).
Still present on master as of today.
Code sample
Widget test (uses only `package:flutter`)
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('ReorderableListView gap flips on every move over a shorter item',
(tester) async {
const heights = <double>[68, 85, 126, 97, 54, 106, 110, 119];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ReorderableListView.builder(
itemCount: heights.length,
itemBuilder: (context, i) => SizedBox(
key: ValueKey(i),
height: heights[i],
child: Text('Row $i'),
),
onReorder: (a, b) {},
),
),
),
);
final row4 = find.byKey(const ValueKey(4));
// Pick up row 5 (106 px) and move it up into the top half of row 4 (54 px).
final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const ValueKey(5))),
);
await tester.pump(kLongPressTimeout + const Duration(milliseconds: 50));
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(0, -48.5));
await tester.pumpAndSettle();
final pushedDown = tester.getTopLeft(row4).dy; // 482: row 4 moved aside.
// Creep upwards half a pixel at a time: row 4 should stay where it is.
for (var move = 0; move < 8; move++) {
await gesture.moveBy(const Offset(0, -0.5));
await tester.pumpAndSettle();
expect(tester.getTopLeft(row4).dy, pushedDown, reason: 'move $move');
}
await gesture.up();
await tester.pumpAndSettle();
});
}Screenshots or Video
Not available, see the logged positions below.
Logs
Row 4's top edge after each 0.5 px move, printed from the test above (resting top is 376, pushed-down top is 482):
at rest: row4 top = 376.0
after -48.5: row4 top = 482.0
after each 0.5 px move: [376.0, 482.0, 376.0, 482.0, 376.0, 482.0, 376.0, 482.0]
The test fails on its first iteration:
Expected: <482.0>
Actual: <376.0>
move 0
Flutter Doctor output
flutter doctor -v
[✓] Flutter (Channel stable, 3.44.1, on macOS 26.6.2 25G83 darwin-arm64, locale en-UA)
• Flutter version 3.44.1 on channel stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 924134a44c (2026-05-29 12:13:22 -0400)
• Engine revision c416acfeb8
• Dart version 3.12.1
• DevTools version 2.57.0
[!] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
• Platform android-37.0, build-tools 36.1.0
• Java version OpenJDK Runtime Environment (build 21+35-2513)
! Some Android licenses not accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 27.0)
• Build 27A266a
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web
[✓] Connected device (3 available)
• iPhone (mobile) • ios • iOS 27.0.1 24A446
• macOS (desktop) • darwin-arm64 • macOS 26.6.2 25G83
• Chrome (web) • Google Chrome 155.0.8059.41
[✓] Network resources
Activity
suryaKommana2662 commentedon Oct 15, 2026
@valentynapolienova Thanks for report. I was able to reproduce the issue locally using the provided widget test. The test fails on the first 0.5-pixel movement: Row 4's top position changes from the pushed-down position (482.0) back to its original position (376.0), matching the reported behavior. routing to the
team-frameworkfor further investigation.Flutter Doctor Output
Fix gap flipping when dragging variable height reorderable items (flu…