Card "swipe-away" dismiss version 3: Uses BlockViewport

Scrollable version of the existing demo.

Includes Ian's BlockViewport fixes.

Still TODO: track the layout and update the ScrollBehavior's
contentsHeight as needed. Stop when we've reached the need.

R=abarth@chromium.org

Review URL: https://codereview.chromium.org/1227963003 .
diff --git a/sky/sdk/example/widgets/card_collection.dart b/sky/sdk/example/widgets/card_collection.dart
index eb4f167..e20648c 100644
--- a/sky/sdk/example/widgets/card_collection.dart
+++ b/sky/sdk/example/widgets/card_collection.dart
@@ -6,35 +6,88 @@
 
 import 'package:vector_math/vector_math.dart';
 import 'package:sky/animation/animation_performance.dart';
+import 'package:sky/animation/scroll_behavior.dart';
 import 'package:sky/base/lerp.dart';
 import 'package:sky/painting/text_style.dart';
 import 'package:sky/theme/colors.dart';
 import 'package:sky/widgets/animated_container.dart';
 import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets/block_viewport.dart';
 import 'package:sky/widgets/card.dart';
 import 'package:sky/widgets/scaffold.dart';
+import 'package:sky/widgets/scrollable.dart';
 import 'package:sky/widgets/theme.dart';
 import 'package:sky/widgets/tool_bar.dart';
 import 'package:sky/widgets/widget.dart';
 
 
 const int _kCardDismissFadeoutMS = 300;
-const double _kMinCardFlingVelocity = 0.4;
-const double _kDismissCardThreshold = 0.70;
+const double _kMinFlingVelocity = 700.0;
+const double _kMinFlingVelocityDelta = 400.0;
+const double _kDismissCardThreshold = 0.6;
+
+class VariableHeightScrollable extends Scrollable {
+  VariableHeightScrollable({
+    String key,
+    this.builder,
+    this.token
+  }) : super(key: key);
+
+  IndexedBuilder builder;
+  Object token;
+
+  void syncFields(VariableHeightScrollable source) {
+    builder = source.builder;
+    token = source.token;
+    super.syncFields(source);
+  }
+
+  ScrollBehavior createScrollBehavior() => new OverscrollBehavior();
+  OverscrollBehavior get scrollBehavior => super.scrollBehavior;
+
+  void _handleSizeChanged(Size newSize) {
+    setState(() {
+      scrollBehavior.containerHeight = newSize.height;
+      scrollBehavior.contentsHeight = 5000.0;
+    });
+  }
+
+  Widget buildContent() {
+    return new SizeObserver(
+      callback: _handleSizeChanged,
+      child: new BlockViewport(
+        builder: builder,
+        startOffset: scrollOffset,
+        token: token
+      )
+    );
+  }
+}
 
 class CardCollectionApp extends App {
 
   final TextStyle cardLabelStyle =
     new TextStyle(color: White, fontSize: 18.0, fontWeight: bold);
 
+  final List<double> cardHeights = [
+    48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 
+    48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0,
+    48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0
+  ];
+
+  List<int> visibleCardIndices;
+  
   CardCollectionApp() {
     _activeCardTransform = new AnimatedContainer()
       ..position = new AnimatedType<Point>(Point.origin)
       ..opacity = new AnimatedType<double>(1.0, end: 0.0);
+
     _activeCardAnimation = _activeCardTransform.createPerformance(
         [_activeCardTransform.position, _activeCardTransform.opacity],
         duration: new Duration(milliseconds: _kCardDismissFadeoutMS));
     _activeCardAnimation.addListener(_handleAnimationProgressChanged);
+
+    visibleCardIndices = new List.generate(cardHeights.length, (i) => i);
   }
 
   int _activeCardIndex = -1;
@@ -43,7 +96,6 @@
   double _activeCardWidth;
   double _activeCardDragX = 0.0;
   bool _activeCardDragUnderway = false;
-  Set<int> _dismissedCardIndices = new Set<int>();
 
   Point get _activeCardDragEndPoint {
     return new Point(_activeCardDragX.sign * _activeCardWidth * _kDismissCardThreshold, 0.0);
@@ -52,7 +104,7 @@
   void _handleAnimationProgressChanged() {
     setState(() {
       if (_activeCardAnimation.isCompleted && !_activeCardDragUnderway)
-        _dismissedCardIndices.add(_activeCardIndex);
+        visibleCardIndices.remove(_activeCardIndex);
     });
   }
 
@@ -92,30 +144,37 @@
     setState(() {
       _activeCardDragUnderway = false;
       if (_activeCardAnimation.isCompleted)
-        _dismissedCardIndices.add(_activeCardIndex);
+        visibleCardIndices.remove(_activeCardIndex);
       else if (!_activeCardAnimation.isAnimating)
         _activeCardAnimation.progress = 0.0;
     });
   }
 
+  bool _isHorizontalFlingGesture(sky.GestureEvent event) {
+    double vx = event.velocityX.abs();
+    double vy = event.velocityY.abs();
+    return vx - vy > _kMinFlingVelocityDelta && vx > _kMinFlingVelocity;
+  }
+
   void _handleFlingStart(sky.GestureEvent event) {
     if (_activeCardWidth == null || _activeCardIndex < 0)
       return;
 
     _activeCardDragUnderway = false;
-    double velocityX = event.velocityX / 1000;
-    if (velocityX.abs() >= _kMinCardFlingVelocity) {
+
+    if (_isHorizontalFlingGesture(event)) {
       double distance = 1.0 - _activeCardAnimation.progress;
       if (distance > 0.0) {
-        double duration = 150.0 * distance / velocityX.abs();
-        _activeCardDragX = velocityX.sign;
+        double duration = 250.0 * 1000.0 * distance / event.velocityX.abs();
+        _activeCardDragX = event.velocityX.sign;
         _activeCardAnimation.timeline.animateTo(1.0, duration: duration);
       }
     }
   }
 
-  Widget _buildCard(int index, Color color) {
-    Widget label = new Center(child: new Text("Item ${index}", style: cardLabelStyle));
+  Widget _buildCard(int cardIndex) {
+    Widget label = new Center(child: new Text("Item ${cardIndex}", style: cardLabelStyle));
+    Color color = lerpColor(Red[500], Blue[500], cardIndex / cardHeights.length);
     Widget card = new Card(
       child: new Padding(child: label, padding: const EdgeDims.all(8.0)),
       color: color
@@ -125,7 +184,7 @@
     // the user starts dragging it. Currently this causes Sky to drop the
     // rest of the pointer gesture, see https://github.com/domokit/mojo/issues/312.
     // As a workaround, always create the Transform and Opacity nodes.
-    if (index == _activeCardIndex) {
+    if (cardIndex == _activeCardIndex) {
       card = _activeCardTransform.build(card);
     } else {
       card = new Transform(child: card, transform: new Matrix4.identity());
@@ -133,8 +192,9 @@
     }
 
     return new Listener(
-      child: card,
-      onPointerDown: (event) { _handlePointerDown(event, index); },
+      key: "$cardIndex",
+      child: new Container(child: card, height: cardHeights[cardIndex]),
+      onPointerDown: (event) { _handlePointerDown(event, cardIndex); },
       onPointerMove: _handlePointerMove,
       onPointerUp: _handlePointerUpOrCancel,
       onPointerCancel: _handlePointerUpOrCancel,
@@ -142,31 +202,25 @@
     );
   }
 
-  Widget _buildCardCollection(List<double> heights) {
-    List<Widget> items = <Widget>[];
-    for(int index = 0; index < heights.length; index++) {
-      if (_dismissedCardIndices.contains(index))
-        continue;
-      Color color = lerpColor(Red[500], Blue[500], index / heights.length);
-      items.add(new Container(
-        child: _buildCard(index, color),
-        height: heights[index]
-      ));
-    }
-
-    return new Container(
-      child: new SizeObserver(child: new Block(items), callback: _handleSizeChanged),
-      padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
-      decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50])
-    );
+  Widget _builder(int index) {
+    if (index >= visibleCardIndices.length)
+      return null;
+    return _buildCard(visibleCardIndices[index]);
   }
 
   Widget build() {
+    Widget cardCollection = new Container(
+      padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0),
+      decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]),
+      child: new VariableHeightScrollable(
+        builder: _builder,
+        token: visibleCardIndices.length
+      )
+    );
+
     return new Scaffold(
       toolbar: new ToolBar(center: new Text('Swipe Away')),
-      body: _buildCardCollection(
-          [48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0,
-           48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0])
+      body: new SizeObserver(child: cardCollection, callback: _handleSizeChanged)
     );
   }
 }
diff --git a/sky/sdk/lib/widgets/block_viewport.dart b/sky/sdk/lib/widgets/block_viewport.dart
index e53240a..8aa0f0c 100644
--- a/sky/sdk/lib/widgets/block_viewport.dart
+++ b/sky/sdk/lib/widgets/block_viewport.dart
@@ -100,7 +100,7 @@
     return right;
   }
 
-  bool _dirty = false;
+  bool _dirty = true;
 
   bool retainStatefulNodeIfPossible(BlockViewport newNode) {
     retainStatefulRenderObjectWrapper(newNode);
@@ -126,7 +126,7 @@
         assert(_currentStartIndex >= 0);
         assert(builder != null);
         assert(root != null);
-        int lastIndex = _currentStartIndex + _currentChildCount;
+        int lastIndex = _currentStartIndex + _currentChildCount - 1;
         for (int index = _currentStartIndex; index <= lastIndex; index += 1) {
           Widget widget = builder(index);
           assert(widget != null);