| Index: sky/framework/animation/scroll_behavior.dart | 
| diff --git a/sky/framework/animation/scroll_behavior.dart b/sky/framework/animation/scroll_behavior.dart | 
| index cea428ad67f4bd7bf4b01a329eb5809504461e77..1c456f2027d3db7e9fc39a39173f89a6f847f336 100644 | 
| --- a/sky/framework/animation/scroll_behavior.dart | 
| +++ b/sky/framework/animation/scroll_behavior.dart | 
| @@ -34,15 +34,40 @@ class BoundedScrollBehavior extends ScrollBehavior { | 
| } | 
|  | 
| class OverscrollBehavior extends ScrollBehavior { | 
| + | 
| +  double _contentsHeight; | 
| +  double get contentsHeight => _contentsHeight; | 
| +  void set contentsHeight (double value) { | 
| +    if (_contentsHeight != value) { | 
| +      _contentsHeight = value; | 
| +      // TODO(ianh) now what? what if we have a simulation ongoing? | 
| +    } | 
| +  } | 
| + | 
| +  double _containerHeight; | 
| +  double get containerHeight => _containerHeight; | 
| +  void set containerHeight (double value) { | 
| +    if (_containerHeight != value) { | 
| +      _containerHeight = value; | 
| +      // TODO(ianh) now what? what if we have a simulation ongoing? | 
| +    } | 
| +  } | 
| + | 
| +  OverscrollBehavior({double contentsHeight: 0.0, double containerHeight: 0.0}) | 
| +    : _contentsHeight = contentsHeight, | 
| +      _containerHeight = containerHeight; | 
| + | 
| +  double get maxScroll => math.max(0.0, _contentsHeight - _containerHeight); | 
| + | 
| Simulation release(Particle particle) { | 
| System system; | 
| -    if (particle.position >= 0.0) { | 
| +    if ((particle.position >= 0.0) && (particle.position < maxScroll)) { | 
| if (particle.velocity == 0.0) | 
| return null; | 
| System slowdownSystem = new ParticleInBoxWithFriction( | 
| particle: particle, | 
| friction: _kScrollFriction, | 
| -        box: new GeofenceBox(min: 0.0, onEscape: () { | 
| +        box: new GeofenceBox(min: 0.0, max: maxScroll, onEscape: () { | 
| (system as Multisystem).transitionToSystem(new ParticleInBoxWithFriction( | 
| particle: particle, | 
| friction: _kOverscrollFriction, | 
| @@ -58,22 +83,31 @@ class OverscrollBehavior extends ScrollBehavior { | 
| } | 
|  | 
| System getBounceBackSystem(Particle particle) { | 
| -    return new ParticleClimbingRamp( | 
| +    if (particle.position < 0.0) | 
| +      return new ParticleClimbingRamp( | 
| particle: particle, | 
| box: new ClosedBox(max: 0.0), | 
| theta: _kBounceSlopeAngle, | 
| targetPosition: 0.0); | 
| +    return new ParticleClimbingRamp( | 
| +      particle: particle, | 
| +      box: new ClosedBox(min: maxScroll), | 
| +      theta: _kBounceSlopeAngle, | 
| +      targetPosition: maxScroll); | 
| } | 
|  | 
| double applyCurve(double scrollOffset, double scrollDelta) { | 
| double newScrollOffset = scrollOffset + scrollDelta; | 
| +    // If we're overscrolling, we want move the scroll offset 2x | 
| +    // slower than we would otherwise. Therefore, we "rewind" the | 
| +    // newScrollOffset by half the amount that we moved it above. | 
| +    // Notice that we clap the "old" value to 0.0 so that we only | 
| +    // reduce the portion of scrollDelta that's applied beyond 0.0. We | 
| +    // do similar things for overscroll in the other direction. | 
| if (newScrollOffset < 0.0) { | 
| -      // If we're overscrolling, we want move the scroll offset 2x slower than | 
| -      // we would otherwise. Therefore, we "rewind" the newScrollOffset by half | 
| -      // the amount that we moved it above. Notice that we clap the "old" value | 
| -      // to 0.0 so that we only reduce the portion of scrollDelta that's applied | 
| -      // beyond 0.0. | 
| newScrollOffset -= (newScrollOffset - math.min(0.0, scrollOffset)) / 2.0; | 
| +    } else if (newScrollOffset > maxScroll) { | 
| +      newScrollOffset -= (newScrollOffset - math.max(maxScroll, scrollOffset)) / 2.0; | 
| } | 
| return newScrollOffset; | 
| } | 
|  |