anchor.diff:      return const <Type, GestureRecognizerFactory>{};
     }
   
  -  final GlobalKey _gestureDetectorKey = new GlobalKey();
  -
  -  void updateGestureDetector() {
  -    _gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
  -  }
  -
  -  Widget build(BuildContext context) {
  -    return new RawGestureDetector(
  -      key: _gestureDetectorKey,
  -      gestures: buildGestureDetectors(),
  -      behavior: HitTestBehavior.opaque,
  -      child: new Listener(
  -        child: buildContent(context),
  -        onPointerDown: _handlePointerDown
  -      )
  -    );
  -  }
  -
     /// Subclasses should override this function to build the interior of their
     /// scrollable widget. Scrollable wraps the returned widget in a
     /// [GestureDetector] to observe the user's interaction with this widget and
     /// to adjust the scroll offset accordingly.
  +  ///
  +  /// The widgets used by this method should be widgets that provide a
  +  /// layout-time callback that reports the sizes that are relevant to
  +  /// the scroll offset (typically the size of the scrollable
  +  /// container and the scrolled contents). [Viewport] and
  +  /// [MixedViewport] provide an [onPaintOffsetUpdateNeeded] callback
  +  /// for this purpose; [GridViewport], [ListViewport], and
  +  /// [LazyListViewport] provide an [onExtentsChanged] callback for
  +  /// this purpose.
  +  ///
  +  /// This callback should be used to update the scroll behavior, if
  +  /// necessary, and then to call [updateGestureDetector] to update
  +  /// the gesture detectors accordingly.
     Widget buildContent(BuildContext context);
  -
  -  Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
  -    _controller.stop();
  -    _controller.value = scrollOffset;
  -    _dispatchOnScrollStartIfNeeded();
  -    return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_dispatchOnScrollEndIfNeeded);
  -  }
  -
  -  bool _scrollOffsetIsInBounds(double scrollOffset) {
  -    if (scrollBehavior is! ExtentScrollBehavior)
  -      return false;
  -    ExtentScrollBehavior behavior = scrollBehavior;
  -    return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
  -  }
  -
  -  Simulation _createFlingSimulation(double scrollVelocity) {
  -    final Simulation simulation =  scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
  -    if (simulation != null) {
  -      final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * (scrollVelocity < 0.0 ? -1.0 : 1.0);
  -      final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
  -      simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
  -    }
  -    return simulation;
  -  }
  -
  -  /// Returns the snapped offset closest to the given scroll offset.
  -  double snapScrollOffset(double scrollOffset) {
  -    RenderBox box = context.findRenderObject();
  -    return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, box.size);
  -  }
  -
  -  /// Whether this scrollable should attempt to snap scroll offsets.
  -  bool get shouldSnapScrollOffset => config.snapOffsetCallback != null;
  -
  -  Simulation _createSnapSimulation(double scrollVelocity) {
  -    if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
  -      return null;
  -
  -    Simulation simulation = _createFlingSimulation(scrollVelocity);
  -    if (simulation == null)
  -        return null;
  -
  -    final double endScrollOffset = simulation.x(double.INFINITY);
  -    if (endScrollOffset.isNaN)
  -      return null;
  -
  -    final double snappedScrollOffset = snapScrollOffset(endScrollOffset); // invokes the config.snapOffsetCallback callback
  -    if (!_scrollOffsetIsInBounds(snappedScrollOffset))
  -      return null;
  -
  -    final double snapVelocity = scrollVelocity.abs() * (snappedScrollOffset - scrollOffset).sign;
  -    final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * (scrollVelocity < 0.0 ? -1.0 : 1.0);
  -    Simulation toSnapSimulation = scrollBehavior.createSnapScrollSimulation(
  -      scrollOffset, snappedScrollOffset, snapVelocity, endVelocity
  -    );
  -    if (toSnapSimulation == null)
  -      return null;
  -
  -    final double scrollOffsetMin = math.min(scrollOffset, snappedScrollOffset);
  -    final double scrollOffsetMax = math.max(scrollOffset, snappedScrollOffset);
  -    return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
  -  }
  -
  -  Future _startToEndAnimation(double scrollVelocity) {
  -    _controller.stop();
  -    Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
  -    if (simulation == null)
  -      return new Future.value();
  -    _dispatchOnScrollStartIfNeeded();
  -    return _controller.animateWith(simulation).then(_dispatchOnScrollEndIfNeeded);
  -  }
  -
  -  void dispose() {
  -    _controller.stop();
  -    super.dispose();
  -  }
  -
  -  void _handleAnimationChanged() {
  -    _setScrollOffset(_controller.value);
  -  }
  -
  -  void _setScrollOffset(double newScrollOffset) {
  -    if (_scrollOffset == newScrollOffset)
  -      return;
  -    setState(() {
  -      _scrollOffset = newScrollOffset;
  -    });
  -    PageStorage.of(context)?.writeState(context, _scrollOffset);
  -    new ScrollNotification(this, _scrollOffset).dispatch(context);
  -    final needsScrollStart = !_isBetweenOnScrollStartAndOnScrollEnd;
  -    if (needsScrollStart) {
  -      dispatchOnScrollStart();
  -      assert(_isBetweenOnScrollStartAndOnScrollEnd);
  -    }
  -    dispatchOnScroll();
  -    assert(_isBetweenOnScrollStartAndOnScrollEnd);
  -    if (needsScrollStart)
  -      dispatchOnScrollEnd();
  -  }
  -
  -  /// Scroll this widget to the given scroll offset.
  -  ///
  -  /// If a non-null [duration] is provided, the widget will animate to the new
  -  /// scroll offset over the given duration with the given curve.
  -  ///
  -  /// This function does not accept a zero duration. To jump-scroll to
  -  /// the new offset, do not provide a duration, rather than providing
  -  /// a zero duration.
  -  Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: Curves.ease }) {
  -    if (newScrollOffset == _scrollOffset)
  -      return new Future.value();
  -
  -    if (duration == null) {
  -      _controller.stop();
  -      _setScrollOffset(newScrollOffset);
  -      return new Future.value();
  -    }
  -
  -    assert(duration > Duration.ZERO);
  -    return _animateTo(newScrollOffset, duration, curve);
  -  }
  -
  -  /// Scroll this widget by the given scroll delta.
  -  ///
  -  /// If a non-null [duration] is provided, the widget will animate to the new
  -  /// scroll offset over the given duration with the given curve.
  -  Future scrollBy(double scrollDelta, { Duration duration, Curve curve: Curves.ease }) {
  -    double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
  -    return scrollTo(newScrollOffset, duration: duration, curve: curve);
  -  }
  -
  -  /// Fling the scroll offset with the given velocity.
  -  ///
  -  /// Calling this function starts a physics-based animation of the scroll
  -  /// offset with the given value as the initial velocity. The physics
  -  /// simulation used is determined by the scroll behavior.
  -  Future fling(double scrollVelocity) {
  -    if (scrollVelocity != 0.0 || !_controller.isAnimating)
  -      return _startToEndAnimation(scrollVelocity);
  -    return new Future.value();
  -  }
  -
  -  /// Animate the scroll offset to a value with a local minima of energy.
  -  ///
  -  /// Calling this function starts a physics-based animation of the scroll
  -  /// offset either to a snap point or to within the scrolling bounds. The
  -  /// physics simulation used is determined by the scroll behavior.
  -  Future settleScrollOffset() {
  -    return _startToEndAnimation(0.0);
  -  }
  -
  -  bool _isBetweenOnScrollStartAndOnScrollEnd = false;
  -
  -  void _dispatchOnScrollStartIfNeeded() {
  -    if (!_isBetweenOnScrollStartAndOnScrollEnd)
  -      dispatchOnScrollStart();
  -  }
  -
  -  void _dispatchOnScrollEndIfNeeded(_) {
  -    if (_isBetweenOnScrollStartAndOnScrollEnd)
  -      dispatchOnScrollEnd();
  -  }
  -
  -  /// Calls the onScrollStart callback.
  -  ///
  -  /// Subclasses can override this function to hook the scroll start callback.
  -  void dispatchOnScrollStart() {
  -    assert(!_isBetweenOnScrollStartAndOnScrollEnd);
  -    _isBetweenOnScrollStartAndOnScrollEnd = true;
  -    if (config.onScrollStart != null)
  -      config.onScrollStart(_scrollOffset);
  -  }
  -
  -  /// Calls the onScroll callback.
  -  ///
  -  /// Subclasses can override this function to hook the scroll callback.
  -  void dispatchOnScroll() {
  -    assert(_isBetweenOnScrollStartAndOnScrollEnd);
  -    if (config.onScroll != null)
  -      config.onScroll(_scrollOffset);
  -  }
  -
  -  /// Calls the dispatchOnScrollEnd callback.
  -  ///
  -  /// Subclasses can override this function to hook the scroll end callback.
  -  void dispatchOnScrollEnd() {
  -    assert(_isBetweenOnScrollStartAndOnScrollEnd);
  -    _isBetweenOnScrollStartAndOnScrollEnd = false;
  -    if (config.onScrollEnd != null)
  -      config.onScrollEnd(_scrollOffset);
  -  }
  -
  -  void _handlePointerDown(_) {
  -    _controller.stop();
  -  }
  -
  -  void _handleDragStart(_) {
  -    _dispatchOnScrollStartIfNeeded();
  -  }
  -
  -  void _handleDragUpdate(double delta) {
  -    scrollBy(pixelOffsetToScrollOffset(delta));
  -  }
  -
  -  Future _handleDragEnd(Velocity velocity) {
  -    double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
  -    // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
  -    return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_dispatchOnScrollEndIfNeeded);
  -  }
   }
   
   /// Indicates that a descendant scrollable has scrolled.
  
positive.code:   /// The axis along which this widget should scroll.
    final Axis scrollDirection;
  
    final ViewportAnchor scrollAnchor;
  
    /// Called whenever this widget starts to scroll.
  
negative.code:     }
    }
  
    /// The current scroll behavior of this widget.
    ///
    /// Scroll behaviors control where the boundaries of the scrollable are placed
    /// and how the scrolling physics should behave near those boundaries and
    /// after the user stops directly manipulating the scrollable.
    ScrollBehavior get scrollBehavior {
      return _scrollBehavior ??= createScrollBehavior();
    }
    ScrollBehavior _scrollBehavior;
  
    /// Subclasses should override this function to create the [ScrollBehavior]
    /// they desire.
    ScrollBehavior createScrollBehavior();
  
    bool _scrollOffsetIsInBounds(double scrollOffset) {
      if (scrollBehavior is! ExtentScrollBehavior)
        return false;
      ExtentScrollBehavior behavior = scrollBehavior;
      return scrollOffset >= behavior.minScrollOffset && scrollOffset < behavior.maxScrollOffset;
    }
  
    void _handleAnimationChanged() {
      _setScrollOffset(_controller.value);
    }
  
    void _setScrollOffset(double newScrollOffset) {
      if (_scrollOffset == newScrollOffset)
        return;
      setState(() {
        _scrollOffset = newScrollOffset;
      });
      PageStorage.of(context)?.writeState(context, _scrollOffset);
      new ScrollNotification(this, _scrollOffset).dispatch(context);
      final needsScrollStart = !_isBetweenOnScrollStartAndOnScrollEnd;
      if (needsScrollStart) {
        dispatchOnScrollStart();
        assert(_isBetweenOnScrollStartAndOnScrollEnd);
      }
      dispatchOnScroll();
      assert(_isBetweenOnScrollStartAndOnScrollEnd);
      if (needsScrollStart)
        dispatchOnScrollEnd();
    }
  
    /// Scroll this widget by the given scroll delta.
    ///
    /// If a non-null [duration] is provided, the widget will animate to the new
    /// scroll offset over the given duration with the given curve.
    Future scrollBy(double scrollDelta, { Duration duration, Curve curve: Curves.ease }) {
      double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
      return scrollTo(newScrollOffset, duration: duration, curve: curve);
    }
  
    /// Scroll this widget to the given scroll offset.
    ///
    /// If a non-null [duration] is provided, the widget will animate to the new
    /// scroll offset over the given duration with the given curve.
    ///
    /// This function does not accept a zero duration. To jump-scroll to
    /// the new offset, do not provide a duration, rather than providing
    /// a zero duration.
    Future scrollTo(double newScrollOffset, { Duration duration, Curve curve: Curves.ease }) {
      if (newScrollOffset == _scrollOffset)
        return new Future.value();
  
      if (duration == null) {
        _controller.stop();
        _setScrollOffset(newScrollOffset);
        return new Future.value();
      }
  
      assert(duration > Duration.ZERO);
      return _animateTo(newScrollOffset, duration, curve);
    }
  
    Future _animateTo(double newScrollOffset, Duration duration, Curve curve) {
      _controller.stop();
      _controller.value = scrollOffset;
      _dispatchOnScrollStartIfNeeded();
      return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_dispatchOnScrollEndIfNeeded);
    }
  
    /// Fling the scroll offset with the given velocity.
    ///
    /// Calling this function starts a physics-based animation of the scroll
    /// offset with the given value as the initial velocity. The physics
    /// simulation used is determined by the scroll behavior.
    Future fling(double scrollVelocity) {
      if (scrollVelocity != 0.0 || !_controller.isAnimating)
        return _startToEndAnimation(scrollVelocity);
      return new Future.value();
    }
  
    /// Animate the scroll offset to a value with a local minima of energy.
    ///
    /// Calling this function starts a physics-based animation of the scroll
    /// offset either to a snap point or to within the scrolling bounds. The
    /// physics simulation used is determined by the scroll behavior.
    Future settleScrollOffset() {
      return _startToEndAnimation(0.0);
    }
  
    Future _startToEndAnimation(double scrollVelocity) {
      _controller.stop();
      Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity);
      if (simulation == null)
        return new Future.value();
      _dispatchOnScrollStartIfNeeded();
      return _controller.animateWith(simulation).then(_dispatchOnScrollEndIfNeeded);
    }
  
    /// Whether this scrollable should attempt to snap scroll offsets.
    bool get shouldSnapScrollOffset => config.snapOffsetCallback != null;
  
    /// Returns the snapped offset closest to the given scroll offset.
    double snapScrollOffset(double scrollOffset) {
      RenderBox box = context.findRenderObject();
      return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, box.size);
    }
  
    Simulation _createSnapSimulation(double scrollVelocity) {
      if (!shouldSnapScrollOffset || scrollVelocity == 0.0 || !_scrollOffsetIsInBounds(scrollOffset))
        return null;
  
      Simulation simulation = _createFlingSimulation(scrollVelocity);
      if (simulation == null)
          return null;
  
      final double endScrollOffset = simulation.x(double.INFINITY);
      if (endScrollOffset.isNaN)
        return null;
  
      final double snappedScrollOffset = snapScrollOffset(endScrollOffset); // invokes the config.snapOffsetCallback callback
      if (!_scrollOffsetIsInBounds(snappedScrollOffset))
        return null;
  
      final double snapVelocity = scrollVelocity.abs() * (snappedScrollOffset - scrollOffset).sign;
      final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * (scrollVelocity < 0.0 ? -1.0 : 1.0);
      Simulation toSnapSimulation = scrollBehavior.createSnapScrollSimulation(
        scrollOffset, snappedScrollOffset, snapVelocity, endVelocity
      );
      if (toSnapSimulation == null)
        return null;
  
      final double scrollOffsetMin = math.min(scrollOffset, snappedScrollOffset);
      final double scrollOffsetMax = math.max(scrollOffset, snappedScrollOffset);
      return new ClampedSimulation(toSnapSimulation, xMin: scrollOffsetMin, xMax: scrollOffsetMax);
    }
  
    Simulation _createFlingSimulation(double scrollVelocity) {
      final Simulation simulation =  scrollBehavior.createFlingScrollSimulation(scrollOffset, scrollVelocity);
      if (simulation != null) {
        final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs() * (scrollVelocity < 0.0 ? -1.0 : 1.0);
        final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
        simulation.tolerance = new Tolerance(velocity: endVelocity, distance: endDistance);
      }
      return simulation;
    }
  
  
    bool _isBetweenOnScrollStartAndOnScrollEnd = false;
  
    /// Calls the onScroll callback.
    ///
    /// Subclasses can override this function to hook the scroll callback.
    void dispatchOnScroll() {
      assert(_isBetweenOnScrollStartAndOnScrollEnd);
      if (config.onScroll != null)
        config.onScroll(_scrollOffset);
    }
  
    void _handlePointerDown(_) {
      _controller.stop();
    }
  
    void _handleDragStart(_) {
      _dispatchOnScrollStartIfNeeded();
    }
  
    void _dispatchOnScrollStartIfNeeded() {
      if (!_isBetweenOnScrollStartAndOnScrollEnd)
        dispatchOnScrollStart();
    }
  
    /// Calls the onScrollStart callback.
    ///
    /// Subclasses can override this function to hook the scroll start callback.
    void dispatchOnScrollStart() {
      assert(!_isBetweenOnScrollStartAndOnScrollEnd);
      _isBetweenOnScrollStartAndOnScrollEnd = true;
      if (config.onScrollStart != null)
        config.onScrollStart(_scrollOffset);
    }
  
    void _handleDragUpdate(double delta) {
      scrollBy(pixelOffsetToScrollOffset(delta));
    }
  
    Future _handleDragEnd(Velocity velocity) {
      double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
      // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
      return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_dispatchOnScrollEndIfNeeded);
    }
  
    void _dispatchOnScrollEndIfNeeded(_) {
      if (_isBetweenOnScrollStartAndOnScrollEnd)
        dispatchOnScrollEnd();
    }
  
    /// Calls the dispatchOnScrollEnd callback.
    ///
    /// Subclasses can override this function to hook the scroll end callback.
    void dispatchOnScrollEnd() {
      assert(_isBetweenOnScrollStartAndOnScrollEnd);
      _isBetweenOnScrollStartAndOnScrollEnd = false;
      if (config.onScrollEnd != null)
        config.onScrollEnd(_scrollOffset);
    }
  
    final GlobalKey _gestureDetectorKey = new GlobalKey();
  
    Widget build(BuildContext context) {
      return new RawGestureDetector(
        key: _gestureDetectorKey,
        gestures: buildGestureDetectors(),
        behavior: HitTestBehavior.opaque,
        child: new Listener(
          child: buildContent(context),
          onPointerDown: _handlePointerDown
        )
      );
    }
  
    /// Fixes up the gesture detector to listen to the appropriate
    /// gestures based on the current information about the layout.
    ///
    /// This method should be called from the
    /// [onPaintOffsetUpdateNeeded] or [onExtentsChanged] handler given
    /// to the [Viewport] or equivalent used by the subclass's
    /// [buildContent] method. See the [buildContent] method's
    /// description for details.
    void updateGestureDetector() {
      _gestureDetectorKey.currentState.replaceGestureRecognizers(buildGestureDetectors());
    }
  
    /// Return the gesture detectors, in the form expected by
    /// [RawGestureDetector.gestures] and
    /// [RawGestureDetectorState.replaceGestureRecognizers], that are
    /// applicable to this [Scrollable] in its current state.
    ///
    /// This is called by [build] and [updateGestureDetector].
    Map<Type, GestureRecognizerFactory> buildGestureDetectors() {
      if (scrollBehavior.isScrollable) {
        switch (config.scrollDirection) {
  