Implement an OverscrollCurve for Scrollable

When using OverscrollCurve, we continue to scroll beyond the top of the
scrollable area but the scroll delta is reduced by 2x. A future CL will add an
animation at gesturescrollend to relax back to scroll position 0.0.

R=eseidel@chromium.org

Review URL: https://codereview.chromium.org/1002953003
diff --git a/sky/examples/stocks-fn/stocklist.dart b/sky/examples/stocks-fn/stocklist.dart
index d904ef1..883ee37 100644
--- a/sky/examples/stocks-fn/stocklist.dart
+++ b/sky/examples/stocks-fn/stocklist.dart
@@ -8,7 +8,7 @@
     Object key,
     this.stocks,
     this.query
-  }) : super(key: key, scrollCurve: new BoundedScrollCurve(minOffset: 0.0));
+  }) : super(key: key, scrollCurve: new OverscrollCurve());
 
   List<Node> buildItems(int start, int count) {
     return stocks
diff --git a/sky/framework/animation/scroll_curve.dart b/sky/framework/animation/scroll_curve.dart
index 07940ae..54b6385 100644
--- a/sky/framework/animation/scroll_curve.dart
+++ b/sky/framework/animation/scroll_curve.dart
@@ -24,3 +24,18 @@
     return newScrollOffset;
   }
 }
+
+class OverscrollCurve extends ScrollCurve {
+  double apply(double scrollOffset, double scrollDelta) {
+    double newScrollOffset = scrollOffset + scrollDelta;
+    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;
+    }
+    return newScrollOffset;
+  }
+}
diff --git a/sky/framework/components/fixed_height_scrollable.dart b/sky/framework/components/fixed_height_scrollable.dart
index 4f3a9d9..b388b17 100644
--- a/sky/framework/components/fixed_height_scrollable.dart
+++ b/sky/framework/components/fixed_height_scrollable.dart
@@ -4,6 +4,7 @@
 
 import '../animation/scroll_curve.dart';
 import '../fn.dart';
+import 'dart:math' as math;
 import 'dart:sky' as sky;
 import 'scrollable.dart';
 
@@ -52,17 +53,24 @@
     var transformStyle = '';
 
     if (_height > 0.0) {
-      drawCount = (_height / _itemHeight).round() + 1;
-      double alignmentDelta = -scrollOffset % _itemHeight;
-      if (alignmentDelta != 0.0) {
-        alignmentDelta -= _itemHeight;
+      if (scrollOffset < 0.0) {
+        double visibleHeight = _height + scrollOffset;
+        drawCount = (visibleHeight / _itemHeight).round() + 1;
+        transformStyle =
+          'transform: translateY(${(-scrollOffset).toStringAsFixed(2)}px)';
+      } else {
+        drawCount = (_height / _itemHeight).round() + 1;
+        double alignmentOffset = math.max(0.0, scrollOffset);
+        double alignmentDelta = -scrollOffset % _itemHeight;
+        if (alignmentDelta != 0.0)
+          alignmentDelta -= _itemHeight;
+
+        double drawStart = scrollOffset + alignmentDelta;
+        itemNumber = (drawStart / _itemHeight).floor();
+
+        transformStyle =
+            'transform: translateY(${(alignmentDelta).toStringAsFixed(2)}px)';
       }
-
-      double drawStart = scrollOffset + alignmentDelta;
-      itemNumber = (drawStart / _itemHeight).floor();
-
-      transformStyle =
-          'transform: translateY(${(alignmentDelta).toStringAsFixed(2)}px)';
     }
 
     return new Container(