animateFromOffsets method

void animateFromOffsets(
  1. Map<TKey, ({double x, double y})> priorOffsets,
  2. Map<TKey, ({double x, double y})> currentOffsets, {
  3. required Duration duration,
  4. required Curve curve,
  5. required bool structuralAnimationsDisabled,
  6. double maxSlideDistance = double.infinity,
})

Installs FLIP slides for every node whose offset changed between priorOffsets and currentOffsets. See TreeController.animateSlideFromOffsets for the full contract.

structuralAnimationsDisabled = the calling family's spec is zeroed (reorderSlide for this public path; the drop-settle channel passes its own family's flag), passed in so the engine never reaches back into the controller for it. An explicit zero duration is treated the same way. DISABLED-MODE SPLIT: either gate means this call CREATES no motion, so nothing fresh installs, but existing entries are NOT destroyed: entries whose bases this batch moved are re-based on their OWN duration/curve so they continue seamlessly. Stopping existing motion is an explicit transition event (purgeActive), never a side effect of another family's install call.

Implementation

void animateFromOffsets(
  Map<TKey, ({double y, double x})> priorOffsets,
  Map<TKey, ({double y, double x})> currentOffsets, {
  required Duration duration,
  required Curve curve,
  required bool structuralAnimationsDisabled,
  double maxSlideDistance = double.infinity,
}) {
  if (structuralAnimationsDisabled || duration == Duration.zero) {
    // Disabled-mode split: refuse fresh installs; re-base survivors.
    // O(1) on the dominant no-actives path (`.disabled` configs).
    if (!hasActive) {
      return;
    }
    if (_ticker == null || !_ticker!.isActive) {
      _lastTickElapsed = Duration.zero;
    }
    // Iterate ACTIVES (a handful), not the maps (every visible row).
    // Copy: _clearSlide mutates the set mid-iteration.
    final touched = <int>{};
    for (final nid in _activeSlideNids.toList()) {
      final entry = _slideByNid[nid]!;
      final key = _nids.keyOfUnchecked(nid);
      final current = currentOffsets[key];
      final prior = priorOffsets[key];
      if (current == null || prior == null) {
        continue; // Absent from this batch: re-baselined below.
      }
      final rawDeltaY = prior.y - current.y;
      final rawDeltaX = prior.x - current.x;
      final composedY = entry.currentDelta + rawDeltaY;
      final composedX = entry.currentDeltaX + rawDeltaX;
      if (composedY.abs() > maxSlideDistance) {
        _clearSlide(key);
        continue;
      }
      if (composedY == 0.0 && composedX == 0.0) {
        _clearSlide(key); // Painted where it belongs: no slide left.
        continue;
      }
      if (rawDeltaY == 0.0 && rawDeltaX == 0.0) {
        // No-op for this entry: the un-touched branch below is the
        // only authority over its clock (mirrors the enabled path's
        // no-op composition rule).
        continue;
      }
      final hadX = entry.startDeltaX != 0.0;
      final newHasX = composedX != 0.0;
      if (hadX && !newHasX) {
        _xActiveCount--;
      } else if (!hadX && newHasX) {
        _xActiveCount++;
      }
      entry.startDelta = composedY;
      entry.currentDelta = composedY;
      entry.startDeltaX = composedX;
      entry.currentDeltaX = composedX;
      entry.slideStartElapsed = _lastTickElapsed;
      entry.progress = 0.0;
      // KEEP entry.slideDuration and entry.curve: the staged timing
      // belongs to the fresh installs this branch refuses.
      entry.preserveProgressOnRebatch = false;
      entry.installStamp++;
      touched.add(nid);
    }
    _rebaselineUntouched(touched);
    // "hasActive implies ticker active" holds at every stop site, and
    // this branch only mutates pre-existing actives, so ensure-start keeps
    // the invariant locally enforced WITHOUT threading the enabled
    // tail's `installed == 0` early-return (which always fires here).
    final ticker = _ticker ??= _vsync.createTicker(_onSlideTick);
    if (!ticker.isActive) ticker.start();
    return;
  }

  // If the ticker isn't currently running, reset our mirror of its
  // elapsed value to 0 BEFORE the install loop reads it. The ticker's
  // internal elapsed restarts from 0 on the next [Ticker.start] call,
  // and we want new slides installed in this batch to capture
  // `slideStartElapsed = 0` so the first post-install tick computes
  // progress as `vsync_delta / duration` (not negative).
  if (_ticker == null || !_ticker!.isActive) {
    _lastTickElapsed = Duration.zero;
  }

  int installed = 0;
  final touched = <int>{};
  for (final entry in currentOffsets.entries) {
    final key = entry.key;
    final current = entry.value;
    final prior = priorOffsets[key];
    if (prior == null) continue;
    final rawDeltaY = prior.y - current.y;
    final rawDeltaX = prior.x - current.x;
    final existing = _slideAt(key);

    // Distance gate: applied to the COMPOSED Y delta. When existing is
    // null, composedY == rawDeltaY (subset of the same check). On
    // exceed: drop any in-flight entry, install nothing, row paints at
    // new structural position. The visual jump is bounded by
    // |existing.currentDelta|, which was itself within maxSlideDistance.
    final composedY = (existing?.currentDelta ?? 0.0) + rawDeltaY;
    if (composedY.abs() > maxSlideDistance) {
      if (existing != null) {
        _clearSlide(key);
        // Removing a touched entry mid-iteration is fine: touched is
        // populated only on install or compose.
      }
      continue;
    }

    if (existing == null) {
      if (rawDeltaY == 0.0 && rawDeltaX == 0.0) continue;
      final slide = SlideAnimation<TKey>(
        startDelta: rawDeltaY,
        startDeltaX: rawDeltaX,
        curve: curve,
      );
      slide.slideStartElapsed = _lastTickElapsed;
      slide.slideDuration = duration;
      _setSlide(key, slide);
      if (rawDeltaX != 0.0) _xActiveCount++;
      final nid = _nids[key];
      if (nid != null) touched.add(nid);
      installed++;
    } else {
      // Composition: preserve currently rendered visual position as the
      // new starting delta so the slide continues seamlessly.
      final composedX = existing.currentDeltaX + rawDeltaX;
      if (composedY == 0.0 && composedX == 0.0) {
        _clearSlide(key); // handles _xActiveCount decrement internally
        continue;
      }
      // No-op composition: this batch reports the row's painted
      // position as identical in the baseline and current snapshots
      // (rawDeltaY == 0 && rawDeltaX == 0), so the existing trajectory
      // is still valid. Animating from `currentDelta` toward 0 reaches
      // the same structural target either way.
      //
      // Resetting the clock here instead would STARVE the slide under a
      // rapid burst of batches that keep including this row without
      // moving it. Each reset restarts the animation from a
      // `currentDelta` that has already ticked down, so per-frame motion
      // shrinks toward sub-pixel and the row reads as frozen until the
      // burst stops and one full duration finally runs uninterrupted.
      //
      // Treat the entry as UN-TOUCHED by this batch: it stays in
      // `_activeSlideNids`, so the re-baseline branch below is the sole
      // authority over its clock. That branch honors
      // `preserveProgressOnRebatch`, so a slide already carrying the
      // flag keeps ticking on its original install clock. `installed`
      // is not incremented, because nothing was installed or composed.
      if (rawDeltaY == 0.0 && rawDeltaX == 0.0) {
        continue;
      }
      // Update X-active count based on transition between had-X and
      // has-X states. existing.startDeltaX reflects the entry's
      // current "has X work" status (lerp doesn't cross zero).
      final hadX = existing.startDeltaX != 0.0;
      final newHasX = composedX != 0.0;
      if (hadX && !newHasX) {
        _xActiveCount--;
      } else if (!hadX && newHasX) {
        _xActiveCount++;
      }
      existing.startDelta = composedY;
      existing.currentDelta = composedY;
      existing.startDeltaX = composedX;
      existing.currentDeltaX = composedX;
      existing.slideStartElapsed = _lastTickElapsed;
      // Adapt the slide's effective duration so per-frame motion stays
      // perceptible. Under rapid cascaded `moveNode(animate: true)`,
      // each batch re-composes the row with a new
      // `composedY = currentDelta + rawDeltaY`. When the existing
      // `currentDelta` and the batch's `rawDeltaY` partially cancel,
      // which is common under random reparenting, `composedY` shrinks
      // relative to the original `rawDeltaY`. Applying the caller's
      // `slideDuration` unchanged then spreads that small delta over the
      // full time, so per-frame motion drops below a pixel and the row
      // reads as not animating, even though the slide is active and
      // does settle correctly.
      //
      // Clamping keeps per-tick motion at [_minPxPerTick] or better: a
      // small composedY settles faster, as a brief but visible move,
      // while a large composedY keeps the caller's duration. Neither
      // case jumps, since `composedY` is still the start delta; only
      // the time it animates over changes.
      //
      // The microseconds-per-pixel ratio assumes 60Hz, so a
      // higher-refresh display over-shortens slightly. That errs toward
      // more visible motion, which is the safe direction.
      existing.slideDuration = _adaptDurationToVisibleMotion(
        duration,
        composedY: composedY,
        composedX: composedX,
      );
      // Composition creates a fresh slide semantically; reset the
      // preserve flag. Render layer re-marks via syncPreserveProgressFlags
      // for slides that are still ghosts after the batch.
      existing.preserveProgressOnRebatch = false;
      existing.progress = 0.0;
      existing.curve = curve;
      // Mark the in-place retarget so a completion cleanup collected
      // BEFORE this composition (same tick) cannot mistake the fresh
      // slide for the entry that completed.
      existing.installStamp++;
      final nid = _nids[key];
      if (nid != null) touched.add(nid);
      installed++;
    }
  }

  _rebaselineUntouched(touched);

  if (!hasActive) {
    _ticker?.stop();
    return;
  }
  if (installed == 0) return;

  // Ticker runs continuously while any slide is active; per-slide
  // progress derives from `(elapsed - slide.slideStartElapsed)
  // / slide.slideDuration`. Starting an already-active ticker is a
  // no-op. Per the class docstring: [Ticker.start] does NOT fire
  // callbacks synchronously, so this is safe inside
  // [RenderObject.performLayout]. `_lastTickElapsed` was already reset
  // above for fresh-ticker batches.
  final ticker = _ticker ??= _vsync.createTicker(_onSlideTick);
  if (!ticker.isActive) ticker.start();
}