startDrag method
- required TKey key,
- required ReorderRenderPort<
TKey> renderPort, - required ScrollableState scrollable,
- required Offset pointerGlobal,
- int depthForPointerX(
- double sliverLocalX
- double proxyCrossOffset()?,
- bool makeRoom = false,
- bool settleFromRelease = false,
Begins a drag session for key.
renderPort is the render surface currently displaying
treeController (the RenderSliverTree). scrollable is the
ancestor scrollable whose viewport clips the tree, used for
pointer-to-scroll-space conversion and autoscroll.
Returns true when the session started. Returns false, starting
nothing, when canReorder refuses key, or when renderPort has
not been laid out yet (no painted rows to resolve against). A policy
refusal is a normal runtime answer, not misuse, so it is a return
value rather than an exception.
depthForPointerX maps the pointer's sliver-local x to an UNCLAMPED
preferred depth (e.g. x ~/ indentWidth, since the widget layer
owns the pixel constant); the resolver clamps it to the legal candidate
chain when a below-zone drop sits at a subtree right-boundary. Omit
it to always resolve at the deepest legal level.
proxyCrossOffset reports the floating drag proxy's VISUAL cross
offset, in sliver cross space, sampled at settle-glide install time
(release, cancel, or the dead-commit fallback) so the proxy-to-row
handoff starts exactly where the card visually is, even when
released mid-animation. Presentation-supplied, following the
depthForPointerX precedent. Omit it (imperative drags with no
proxy) and the glides carry no x motion while the commit baseline
preserves the captured x, the classic y-only handoff. Only
consulted when settleFromRelease builds a settler.
When makeRoom AND settleFromRelease are BOTH set (touch-first
make-room mode), slot resolution probes at the PROXY MIDPOINT
(pointer + rowExtent/2 - grabDy, a session constant) instead of
the raw pointer: on touch there is no visible cursor, so selection
tracks the card in hand regardless of where it was grabbed. Every
other configuration resolves at the raw pointer.
Throws ArgumentError for genuine wiring misuse: renderPort not
driven by treeController (cross-controller drag is out of scope).
Implementation
bool startDrag({
required TKey key,
required ReorderRenderPort<TKey> renderPort,
required ScrollableState scrollable,
required Offset pointerGlobal,
int Function(double sliverLocalX)? depthForPointerX,
double Function()? proxyCrossOffset,
bool makeRoom = false,
bool settleFromRelease = false,
}) {
if (!renderPort.drivesController(treeController)) {
throw ArgumentError.value(
renderPort,
"renderPort",
"renderPort must be driven by the same TreeController passed to "
"TreeReorderController. Cross-controller drag is not supported.",
);
}
if (!renderPort.isLaidOut) {
return false;
}
if (canReorder != null && !canReorder!(key)) {
return false;
}
if (_session != null) {
cancelDrag();
}
final pointerSpace = PointerSpace<TKey>(
scrollable: scrollable,
renderPort: renderPort,
);
final startSample = pointerSpace.sample(pointerGlobal);
if (startSample == null) {
// The scrollable is unmounted or its viewport is detached: there
// is nothing to drag within, so refuse like any other policy check.
return false;
}
// The probe is the single owner of grab geometry + probeDy; capture
// runs once here against the start sample.
final probe = DragProbe<TKey>(
renderPort: renderPort,
resolver: _resolver,
draggedKey: key,
depthForPointerX: depthForPointerX,
);
probe.captureGrab(
start: startSample,
midpointProbe: makeRoom && settleFromRelease,
);
// Per-session animation resolution: the tree controller's style is
// read ONCE here: a mid-drag restyle never retimes a live session;
// the next drag picks it up.
final style = treeController.animationStyle;
final session = DragSession<TKey>(
draggedKey: key,
renderPort: renderPort,
pointerSpace: pointerSpace,
probe: probe,
pointerGlobal: pointerGlobal,
commitSlideSpec: style.reorderSlide,
);
// Behavior collaborators capture the session's identity in their
// callbacks, hence assignment after construction.
session.autoScroller = AutoScroller<TKey>(
vsync: _vsync,
space: pointerSpace,
pointerGlobal: () => session.pointerGlobal,
edgeZone: autoScrollEdgeZone,
maxVelocity: autoScrollMaxVelocity,
);
session.dwell = DwellExpander<TKey>(
treeController: treeController,
delay: autoExpandDelay,
sessionLive: () => identical(_session, session),
requestResolve: () => _resolveAndNotify(session),
);
if (makeRoom) {
session.makeRoomDriver = MakeRoomDriver<TKey>(
treeController: treeController,
draggedKey: key,
duration: style.effectiveMakeRoom.duration,
curve: style.effectiveMakeRoom.curve,
);
}
if (settleFromRelease) {
session.settler = DropSettler<TKey>(
treeController: treeController,
space: pointerSpace,
pointerGlobal: () => session.pointerGlobal,
grabDy: () => probe.grabDy,
draggedKey: key,
duration: style.effectiveDropSettle.duration,
curve: style.effectiveDropSettle.curve,
proxyCrossOffset: proxyCrossOffset,
);
}
_dragGeneration++;
_session = session;
// Pin the dragged row against stale eviction for the session's
// lifetime: the drag gesture's recognizer lives on the row's own
// `State`, so autoscrolling it out of the cache region would otherwise
// evict the row, its `Drag`'s end/cancel would never fire, and the
// session (plus the autoscroll ticker) would run forever.
renderPort.pinNode(key);
session.subscribeScroll(scrollable.position, _onScrollPositionChanged);
_pointerPosition.value = pointerGlobal;
// One choreography site: probe + resolver + every behavior. The
// make-room gap of a session born over a valid slot opens here, and
// the edge-zone-at-start autoscroll evaluation runs here too.
session.resolve();
// Drag session just started; currentTarget may have become non-null.
notifyListeners();
return true;
}