anchor.diff:      }
     }
   
  -  ScrollBehavior _scrollBehavior;
  -
  -  /// Subclasses should override this function to create the [ScrollBehavior]
  -  /// they desire.
  -  ScrollBehavior createScrollBehavior();
  -
     /// 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 {
  -    if (_scrollBehavior == null)
  -      _scrollBehavior = createScrollBehavior();
  -    return _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) {
  
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:     return const <Type, GestureRecognizerFactory>{};
    }
  
    /// 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);
  }
  
  /// Indicates that a descendant scrollable has scrolled.
  