croupier: Add Card Transitions Layer

(UI) Cards now have the option of being invisible. If they are invisible, it
is up to the Game to capture the Cards that are rendered and compute their
positions.

This asynchronously collected data will be used to render a new Card layer
on top of the rest of the game widgets. The previously invisible Cards are
rendered here as ZCards.

ZCards mirror Cards except that they keep track of a queue of positions to
animate to. This allows them to move across the screen as actions take place.

ZCards and their wrapping Positioned are keyed widgets. Since they have keys,
they are not allowed to be duplicated. Therefore, extra work will need to be
done to have the same underlying logic card animate in two ways.

ZCards also respect the z-index given to them. Right now, this value is given
naively, but there can be superior strategies in the future.

Note: Cards that are drag-and-dropped still animate.
This can probably be avoided in the future by ensuring the callback is aware
of the dropped card(s).

Note: Cards don't appear in the right places sometimes since their 'keyed'ness
makes them "remember" their old offset. This causes it to apply "twice" or
more in some scenarios. https://github.com/flutter/engine/issues/1939

Change-Id: Ib88a1c697c63b06ef1f0cda1d8ffdbed61583020
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 4e98cbb..5614354 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -80,7 +80,8 @@
           cards, false, ori,
           width: i % 2 == 0 ? cccSize : null,
           height: i % 2 != 0 ? cccSize : null,
-          rotation: -math.PI / 2 * i);
+          rotation: -math.PI / 2 * i,
+          useKeys: true);
       Widget w;
       switch (i) {
         case 2:
@@ -138,7 +139,8 @@
           widthCard: this.cardWidth,
           height: this.cardHeight,
           heightCard: this.cardHeight,
-          rotation: -math.PI / 2 * i);
+          rotation: -math.PI / 2 * i,
+          useKeys: true);
       Widget w;
 
       double left02 = (this.width - this.cardWidth) / 2;
diff --git a/lib/components/card.dart b/lib/components/card.dart
index 03f6db7..de932aa 100644
--- a/lib/components/card.dart
+++ b/lib/components/card.dart
@@ -4,44 +4,248 @@
 
 import '../logic/card.dart' as logic_card;
 
+import 'dart:async';
+
+import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart' as widgets;
+import 'package:flutter/rendering.dart';
 import 'package:vector_math/vector_math_64.dart' as vector_math;
 
-class Card extends widgets.StatelessComponent {
-  final logic_card.Card card;
-  final bool faceUp;
-  final double _width;
-  final double _height;
-  final double rotation;
+enum CardAnimationType {
+  NONE, OLD_TO_NEW, IN_TOP
+}
 
-  double get width => _width ?? 40.0;
-  double get height => _height ?? 40.0;
+enum CardUIType {
+  CARD, ZCARD
+}
 
-  Card(this.card, this.faceUp,
-      {double width, double height, this.rotation: 0.0})
-      : _width = width,
-        _height = height;
+class GlobalCardKey extends widgets.GlobalKey {
+  logic_card.Card card;
+  CardUIType type;
 
-  widgets.Widget build(widgets.BuildContext context) {
-    // TODO(alexfandrianto): This isn't a nice way of doing Rotation.
-    // The reason is that you must know the width and height of the image.
-    // Feature Request: https://github.com/flutter/engine/issues/1452
-    return new widgets.Listener(
-        child: new widgets.Container(
-            width: width,
-            height: height,
-            child: new widgets.Transform(
-                child: _imageFromCard(card, faceUp),
-                transform: new vector_math.Matrix4.identity()
-                    .translate(this.width / 2, this.height / 2)
-                    .rotateZ(this.rotation)
-                    .translate(-this.width / 2, -this.height / 2))));
+  GlobalCardKey(this.card, this.type): super.constructor();
+
+  bool operator ==(Object other) {
+    if (other is! GlobalCardKey) {
+      return false;
+    }
+    GlobalCardKey k = other;
+    return k.card == card && k.type == type;
   }
 
-  static widgets.Widget _imageFromCard(logic_card.Card c, bool faceUp) {
-    // TODO(alexfandrianto): Instead of 'default', what if we were told which theme to use?
-    String imageName =
-        "images/default/${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
-    return new widgets.NetworkImage(src: imageName);
+  int get hashCode {
+    return 17 * card.hashCode + 33 * type.hashCode;
+  }
+}
+
+class ZCard extends widgets.StatefulComponent {
+  final logic_card.Card card;
+  final bool faceUp;
+  final double width;
+  final double height;
+  final double rotation;
+  final bool animateEntrance;
+  final double z;
+
+  final Point startingPosition;
+  final Point endingPosition;
+
+  ZCard(Card dataComponent, this.startingPosition, this.endingPosition) :
+    super(key: new GlobalCardKey(dataComponent.card, CardUIType.ZCARD)),
+    card = dataComponent.card,
+    faceUp = dataComponent.faceUp,
+    width = dataComponent.width ?? 40.0,
+    height = dataComponent.height ?? 40.0,
+    rotation = dataComponent.rotation,
+    animateEntrance = dataComponent.animateEntrance,
+    z = dataComponent.z;
+
+  _ZCardState createState() => new _ZCardState();
+}
+
+class Card extends widgets.StatefulComponent {
+  final logic_card.Card card;
+  final bool faceUp;
+  final double width;
+  final double height;
+  final double rotation;
+  final bool useKey;
+  final bool visible;
+  final bool animateEntrance;
+  final double z;
+
+  Card(logic_card.Card card, this.faceUp,
+      {double width, double height, this.rotation: 0.0, bool useKey: false, this.visible: true, this.animateEntrance: true, this.z})
+      : card = card,
+        width = width ?? 40.0,
+        height = height ?? 40.0,
+        useKey = useKey,
+        super(key: useKey ? new GlobalCardKey(card, CardUIType.CARD) : null);
+
+  // Use this helper to help create a Card clone.
+  // Used by the drag and drop layer.
+  Card clone({bool visible}) {
+    return new Card(this.card, this.faceUp,
+      width: width, height: height, rotation: rotation,
+      useKey: false, visible: visible ?? this.visible, animateEntrance: false,
+      z: z);
+  }
+
+  CardState createState() => new CardState();
+}
+
+class CardState extends widgets.State<Card> {
+  // TODO(alexfandrianto): This bug is why some cards appear slightly off.
+  // https://github.com/flutter/engine/issues/1939
+  Point getGlobalPosition() {
+    RenderBox box = context.findRenderObject();
+    return box.localToGlobal(Point.origin);
+  }
+
+  widgets.Widget build(widgets.BuildContext context) {
+    widgets.Widget image = null;
+    if (config.visible) {
+      image = new widgets.Transform(
+                child: _imageFromCard(config.card, config.faceUp),
+                transform: new vector_math.Matrix4.identity()
+                    .rotateZ(config.rotation),
+                alignment: new FractionalOffset(0.5, 0.5));
+    }
+
+    return new widgets.Container(
+            width: config.width,
+            height: config.height,
+            child: image);
+  }
+}
+
+widgets.Widget _imageFromCard(logic_card.Card c, bool faceUp) {
+  // TODO(alexfandrianto): Instead of 'default', what if we were told which theme to use?
+  String imageName =
+      "images/default/${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
+  return new widgets.NetworkImage(src: imageName);
+}
+
+class _ZCardState extends widgets.State<ZCard> {
+  ValuePerformance<Point> _performance;
+  List<Point> _pointQueue; // at least 1 longer than the current animation index.
+  int _animationIndex;
+  bool _cardUpdateScheduled = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _initialize();
+    scheduleUpdatePosition();
+  }
+
+  void _initialize() {
+    _pointQueue = new List<Point>();
+    _animationIndex = 0;
+    if (config.startingPosition != null) {
+      _pointQueue.add(config.startingPosition);
+    }
+    _pointQueue.add(config.endingPosition);
+    _performance = new ValuePerformance<Point>(
+      variable: new AnimatedValue<Point>(Point.origin, curve: Curves.ease),
+      duration: const Duration(milliseconds: 250)
+    );
+    _performance.addStatusListener((PerformanceStatus status) {
+      if (status == PerformanceStatus.completed) {
+        _animationIndex++;
+        _tryAnimate();
+      }
+    });
+  }
+
+  void scheduleUpdatePosition() {
+    if (!_cardUpdateScheduled) {
+      _cardUpdateScheduled = true;
+      scheduleMicrotask(_updatePosition);
+    }
+  }
+
+  // These microtasks are being scheduled on every build change.
+  // Theoretically, this is too often, but to be safe, it is also good to do it.
+  @override
+  void didUpdateConfig(ZCard oldConfig) {
+    if (config.key != oldConfig.key) {
+      _initialize();
+    } else {
+      // Do we need to animate to a new location? If so, add it to the queue.
+      if (config.endingPosition != _pointQueue.last) {
+        setState(() {
+          _pointQueue.add(config.endingPosition);
+          _tryAnimate();
+        });
+      }
+    }
+    scheduleUpdatePosition();
+  }
+
+  // A callback that sets up the animation from point a to point b.
+  void _updatePosition() {
+    _cardUpdateScheduled = false; // allow the next attempt to schedule _updatePosition to succeed.
+    if (!config.animateEntrance || _pointQueue.length == 1) {
+      RenderBox box = context.findRenderObject();
+      Point endingLocation = box.globalToLocal(config.endingPosition);
+      _performance.variable
+        ..begin = endingLocation
+        ..value = endingLocation
+        ..end = endingLocation;
+      _performance.progress = 0.0;
+      return;
+    }
+
+    _tryAnimate();
+  }
+
+  bool _needsAnimation() {
+    return _animationIndex < _pointQueue.length - 1;
+  }
+
+  void _tryAnimate() {
+    // Let animations finish... (Is this a good idea?)
+    if (!_performance.isAnimating && _needsAnimation()) {
+      RenderBox box = context.findRenderObject();
+      Point globalStart = _pointQueue[_animationIndex];
+      Point globalEnd = _pointQueue[_animationIndex + 1];
+      Point startingLocation = box.globalToLocal(globalStart);
+      Point endingLocation = box.globalToLocal(globalEnd);
+      _performance.variable
+        ..begin = startingLocation
+        ..value = startingLocation
+        ..end = endingLocation;
+      _performance.progress = 0.0;
+      _performance.play();
+    }
+  }
+
+  widgets.Widget build(widgets.BuildContext context) {
+    widgets.Widget image = new widgets.Transform(
+      child: _imageFromCard(config.card, config.faceUp),
+      transform: new vector_math.Matrix4.identity()
+          .rotateZ(config.rotation),
+      alignment: new FractionalOffset(0.5, 0.5));
+
+    // Set up the drag listener.
+    widgets.Widget listeningCard = new widgets.Listener(
+        child: new widgets.Container(
+            width: config.width,
+            height: config.height,
+            child: image));
+
+    // Set up the slide transition.
+    // During animation, we must ignore all events.
+    widgets.Widget retWidget = new widgets.IgnorePointer(
+      ignoring: _performance.isAnimating,
+      child: new widgets.SlideTransition(
+        performance: _performance.view,
+        position: _performance.variable,
+        child: listeningCard
+      )
+    );
+
+    return retWidget;
   }
 }
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index 80c76a5..363a269 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -7,7 +7,6 @@
 
 import 'dart:math' as math;
 import 'package:flutter/material.dart';
-import 'package:flutter/material.dart' as material;
 
 enum CardCollectionOrientation { vert, horz, fan, show1, suit }
 enum DropType {
@@ -17,6 +16,7 @@
   // I can see that both would be nice, but I'm not sure how to do that yet.
 }
 
+typedef double PosComputer(int index);
 typedef void AcceptCb(dynamic data, List<logic_card.Card> cards);
 
 const double DEFAULT_WIDTH = 200.0;
@@ -43,10 +43,11 @@
   final Color _backgroundColor;
   final Color _altColor;
   final double rotation; // This angle is in radians.
+  final bool useKeys; // If set, every Card created in this collection will be keyed.
 
   DropType get acceptType => _acceptType ?? DropType.none;
-  Color get backgroundColor => _backgroundColor ?? material.Colors.grey[500];
-  Color get altColor => _altColor ?? material.Colors.grey[500];
+  Color get backgroundColor => _backgroundColor ?? Colors.grey[500];
+  Color get altColor => _altColor ?? Colors.grey[500];
 
   CardCollectionComponent(
       this.cards, this.faceUp, this.orientation,
@@ -60,7 +61,8 @@
       this.heightCard: DEFAULT_CARD_HEIGHT,
       Color backgroundColor,
       Color altColor,
-      this.rotation: 0.0})
+      this.rotation: 0.0,
+      this.useKeys: false})
       : _acceptType = acceptType,
         _backgroundColor = backgroundColor,
         _altColor = altColor;
@@ -135,30 +137,49 @@
     }
   }
 
-  double get _produceColumnWidth => config.widthCard + CARD_MARGIN * 2;
-  Widget _produceColumn(List<Widget> cardWidgets) {
-    // Let's do a stack of positioned cards!
-    List<Widget> kids = new List<Widget>();
+  List<Widget> _makeDraggableAndPositioned(List<component_card.Card> cardWidgets, PosComputer topComputer, PosComputer leftComputer) {
+    List<Widget> ret = new List<Widget>();
+    for (int i = 0; i < cardWidgets.length; i++) {
+      Point p = new Point(leftComputer(i), topComputer(i));
 
+      component_card.Card w = cardWidgets[i];
+      Widget widgetToAdd = w;
+      if (config.dragChildren) {
+        widgetToAdd = new Draggable(
+          child: w,
+          data: w,
+          feedback: new Opacity(child: w.clone(visible: true), opacity: 0.5));
+      }
+      widgetToAdd = new Positioned(
+        left: p.x,
+        top: p.y,
+        child: widgetToAdd
+      );
+
+      ret.add(widgetToAdd);
+    }
+    return ret;
+  }
+
+  double get _produceColumnWidth => config.widthCard + CARD_MARGIN * 2;
+  Widget _produceColumn(List<component_card.Card> cardWidgets) {
     double h = config.height ?? config.heightCard * 5;
     double spacing = math.min(config.heightCard + CARD_MARGIN * 2,
         (h - config.heightCard - 2 * CARD_MARGIN) / (cardWidgets.length - 1));
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN + spacing * i,
-          left: CARD_MARGIN,
-          child: cardWidgets[i]));
-    }
+    PosComputer topComputer = (int i) => CARD_MARGIN + spacing * i;
+    PosComputer leftComputer = (int i) => CARD_MARGIN;
+
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: config.height,
         width: _produceColumnWidth,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
   double get _produceRowHeight => config.heightCard + CARD_MARGIN * 2;
-  Widget _produceRow(List<Widget> cardWidgets, {emptyBackgroundImage: ""}) {
+  Widget _produceRow(List<component_card.Card> cardWidgets, {emptyBackgroundImage: ""}) {
     if (cardWidgets.length == 0) {
       // Just return a centered background image.
       return new Container(
@@ -176,43 +197,36 @@
                           : new NetworkImage(src: emptyBackgroundImage)))));
     }
 
-    // Let's do a stack of positioned cards!
-    List<Widget> kids = new List<Widget>();
-
     double w = config.width ?? config.widthCard * 5;
     double spacing = math.min(config.widthCard + CARD_MARGIN * 2,
         (w - config.widthCard - 2 * CARD_MARGIN) / (cardWidgets.length - 1));
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN,
-          left: CARD_MARGIN + spacing * i,
-          child: cardWidgets[i]));
-    }
+    PosComputer topComputer = (int i) => CARD_MARGIN;
+    PosComputer leftComputer = (int i) => CARD_MARGIN + spacing * i;
+
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: _produceRowHeight,
         width: config.width,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
-  Widget _produceSingle(List<Widget> cardWidgets) {
-    List<Widget> kids = new List<Widget>();
+  Widget _produceSingle(List<component_card.Card> cardWidgets) {
+    PosComputer topComputer = (int i) => CARD_MARGIN;
+    PosComputer leftComputer = (int i) => CARD_MARGIN;
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN, left: CARD_MARGIN, child: cardWidgets[i]));
-    }
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: _produceRowHeight,
         width: _produceColumnWidth,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
   double get _whiteLineHeight => WHITE_LINE_HEIGHT;
 
-  Widget wrapCards(List<Widget> cardWidgets) {
+  Widget wrapCards(List<component_card.Card> cardWidgets) {
     switch (config.orientation) {
       case CardCollectionOrientation.vert:
         return _produceColumn(cardWidgets);
@@ -224,10 +238,10 @@
       case CardCollectionOrientation.show1:
         return _produceSingle(cardWidgets);
       case CardCollectionOrientation.suit:
-        List<Widget> cs = new List<Widget>();
-        List<Widget> ds = new List<Widget>();
-        List<Widget> hs = new List<Widget>();
-        List<Widget> ss = new List<Widget>();
+        List<component_card.Card> cs = new List<component_card.Card>();
+        List<component_card.Card> ds = new List<component_card.Card>();
+        List<component_card.Card> hs = new List<component_card.Card>();
+        List<component_card.Card> ss = new List<component_card.Card>();
 
         List<logic_card.Card> theCards =
             config.comparator != null ? this._sortedCards : config.cards;
@@ -253,7 +267,7 @@
         }
         return new Container(
             decoration:
-                new BoxDecoration(backgroundColor: material.Colors.white),
+                new BoxDecoration(backgroundColor: Colors.white),
             child: new Stack(<Widget>[
               new Positioned(
                   top: 0.0,
@@ -283,7 +297,7 @@
   }
 
   Widget _buildCollection() {
-    List<Widget> cardComponents = new List<Widget>();
+    List<component_card.Card> cardComponents = new List<component_card.Card>();
     List<logic_card.Card> cs =
         config.comparator != null ? this._sortedCards : config.cards;
 
@@ -291,16 +305,12 @@
       component_card.Card c = new component_card.Card(cs[i], config.faceUp,
           width: config.widthCard,
           height: config.heightCard,
-          rotation: config.rotation);
+          rotation: config.rotation,
+          visible: !config.useKeys, // TODO(alexfandrianto): Is there a case where you want an invisible card and a key?
+          useKey: config.useKeys,
+          z: 0.0 + i);
 
-      if (config.dragChildren) {
-        cardComponents.add(new Draggable(
-            child: c,
-            data: c,
-            feedback: new Opacity(child: c, opacity: 0.5)));
-      } else {
-        cardComponents.add(c);
-      }
+      cardComponents.add(c);
     }
 
     // Let's draw a stack of cards with DragTargets.
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index bd1bc62..26e5e99 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -149,7 +149,7 @@
                 config.croupier.game, () {
               config.croupier.game.quit();
               makeSetStateCallback(logic_croupier.CroupierState.Welcome)();
-            }, width: screenSize.width, height: screenSize.height));
+            }, width: screenSize.width, height: screenSize.height - ui.window.padding.top));
       default:
         assert(false);
         return null;
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 3bb7105..fb376d8 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -9,11 +9,13 @@
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType;
 import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase;
 import 'board.dart' show HeartsBoard;
+import 'card.dart' as component_card;
 import 'card_collection.dart'
     show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb;
 
+import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/material.dart' as material;
+import 'package:flutter/rendering.dart';
 
 part 'hearts/hearts.part.dart';
 part 'proto/proto.part.dart';
@@ -32,12 +34,19 @@
 }
 
 abstract class GameComponentState<T extends GameComponent> extends State<T> {
+  Map<logic_card.Card, CardAnimationData> cardLevelMap;
+
   void initState() {
     super.initState();
 
+    cardLevelMap = new Map<logic_card.Card, CardAnimationData>();
     config.game.updateCallback = update;
   }
 
+  void _reset() {
+    cardLevelMap.clear();
+  }
+
   // This callback is used to force the UI to draw when state changes occur
   // outside of the UIs control (e.g., synced data).
   void update() {
@@ -58,6 +67,84 @@
 
   @override
   Widget build(BuildContext context); // still UNIMPLEMENTED
+
+
+  void _cardLevelMapProcessAllVisible(List<int> visibleCardCollections) {
+    Game game = config.game;
+
+    for (int i = 0; i < visibleCardCollections.length; i++) {
+      int index = visibleCardCollections[i];
+      for (int j = 0; j < game.cardCollections[index].length; j++) {
+        _cardLevelMapProcess(game.cardCollections[index][j]);
+      }
+    }
+  }
+
+  void _cardLevelMapProcess(logic_card.Card logicCard) {
+    component_card.GlobalCardKey key = new component_card.GlobalCardKey(logicCard, component_card.CardUIType.CARD);
+    component_card.CardState cardState = key.currentState;
+    if (cardState == null) {
+      return; // There's nothing we can really do about this card since it hasn't drawn yet.
+    }
+    Point p = cardState.getGlobalPosition();
+    double z = cardState.config.z;
+    component_card.Card c = key.currentWidget;
+
+    assert(c == cardState.config);
+
+    CardAnimationData cad = cardLevelMap[logicCard];
+    if (cad == null || cad.newPoint != p || cad.z != z) {
+      setState(() {
+        cardLevelMap[logicCard] = new CardAnimationData(c, cad?.newPoint, p, z);
+      });
+    }
+  }
+
+  // Helper to build the card animation layer.
+  // Note: This isn't a component because of its dependence on Widgets.
+  Widget buildCardAnimationLayer(List<int> visibleCardCollections) {
+    // It's possible that some cards need to be moved after this build.
+    // If so, we can catch it in the next frame.
+    scheduler.requestPostFrameCallback((Duration d) {
+      _cardLevelMapProcessAllVisible(visibleCardCollections);
+    });
+
+    List<Widget> positionedCards = new List<Widget>();
+
+    // Sort the cards by z-index.
+    List<logic_card.Card> orderedKeys = cardLevelMap.keys.toList()..sort((logic_card.Card a, logic_card.Card b) {
+      double diff = cardLevelMap[a].z - cardLevelMap[b].z;
+      return diff.sign.toInt();
+    });
+
+    orderedKeys.forEach((logic_card.Card c) {
+      // Don't show a card if it isn't part of a visible collection.
+      if (!visibleCardCollections.contains(config.game.findCard(c))) {
+        cardLevelMap.remove(c); // It is an old card, which we can clean up.
+        return;
+      }
+
+      CardAnimationData data = cardLevelMap[c];
+      RenderBox box = context.findRenderObject();
+      Point p = data.newPoint;
+      Point trueP = box.globalToLocal(p);
+
+      positionedCards.add(new Positioned(
+        key: new GlobalObjectKey(c.toString()), //needed, or else the Positioned wrapper may be traded out and animations fail.
+        top: trueP.y, // must pass x and y or else it expands to the maximum Stack size.
+        left: trueP.x, // must pass x and y or else it expands to the maximum Stack size.
+        child: new component_card.ZCard(data.comp_card, data.oldPoint, data.newPoint)));
+    });
+
+    return new IgnorePointer(
+      ignoring: true,
+      child: new Container(
+        width: config.width,
+        height: config.height,
+        child: new Stack(positionedCards)
+      )
+    );
+  }
 }
 
 GameComponent createGameComponent(
@@ -79,3 +166,15 @@
       return null;
   }
 }
+
+/// CardAnimationData contains the relevant information for a ZCard to be built.
+/// It uses the comp_card's properties, the oldPoint, newPoint, and z-index to
+/// determine how it needs to animate.
+class CardAnimationData {
+  component_card.Card comp_card;
+  Point oldPoint;
+  Point newPoint;
+  double z;
+
+  CardAnimationData(this.comp_card, this.oldPoint, this.newPoint, this.z);
+}
\ No newline at end of file
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 2785ff0..966b3b3 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -17,12 +17,79 @@
   List<logic_card.Card> passingCards2 = new List<logic_card.Card>();
   List<logic_card.Card> passingCards3 = new List<logic_card.Card>();
 
+  HeartsType _lastViewType;
+
+  @override
+  void initState() {
+    super.initState();
+    _reset();
+  }
+
+  @override
+  void _reset() {
+    super._reset();
+    HeartsGame game = config.game as HeartsGame;
+    _lastViewType = game.viewType;
+  }
+
   @override
   Widget build(BuildContext context) {
-    return new Container(
+    HeartsGame game = config.game as HeartsGame;
+
+    // check if we need to swap out our 's map.
+    if (_lastViewType != game.viewType) {
+      _reset();
+    }
+
+    // Hearts Widget
+    Widget heartsWidget = new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.grey[300]),
+            new BoxDecoration(backgroundColor: Colors.grey[300]),
         child: buildHearts());
+
+    List<Widget> children = new List<Widget>();
+    children.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: Colors.grey[300]),
+          width: config.width,
+          height: config.height,
+          child: heartsWidget));
+    if (game.phase == HeartsPhase.Deal || game.phase != HeartsPhase.Score) {
+      List<int> visibleCardCollections = new List<int>();
+      int playerNum = game.playerNumber;
+      if (game.viewType == HeartsType.Player) {
+        switch(game.phase) {
+          case HeartsPhase.Pass:
+            visibleCardCollections.add(HeartsGame.OFFSET_PASS + playerNum);
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            break;
+          case HeartsPhase.Take:
+            visibleCardCollections.add(HeartsGame.OFFSET_PASS + game.takeTarget);
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            break;
+          case HeartsPhase.Play:
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            visibleCardCollections.add(HeartsGame.OFFSET_PLAY + playerNum);
+            break;
+          default:
+            break;
+        }
+      } else {
+        // A board will need to see these things.
+        for (int i = 0; i < 4; i++) {
+          visibleCardCollections.add(HeartsGame.OFFSET_PLAY + i);
+          visibleCardCollections.add(HeartsGame.OFFSET_PASS + i);
+          visibleCardCollections.add(HeartsGame.OFFSET_HAND + i);
+        }
+      }
+      children.add(this.buildCardAnimationLayer(visibleCardCollections));
+    }
+
+    return new Container(
+      width: config.width,
+      height: config.height,
+      child: new Stack(children)
+    );
   }
 
   void _switchViewCallback() {
@@ -160,8 +227,8 @@
   @override
   Widget _makeButton(String text, NoArgCb callback, {bool inactive: false}) {
     var borderColor =
-        inactive ? material.Colors.grey[500] : material.Colors.white;
-    var backgroundColor = inactive ? material.Colors.grey[500] : null;
+        inactive ? Colors.grey[500] : Colors.white;
+    var backgroundColor = inactive ? Colors.grey[500] : null;
     return new FlatButton(
         child: new Container(
             decoration: new BoxDecoration(
@@ -238,6 +305,8 @@
 
     List<Widget> cardCollections = new List<Widget>();
 
+    // Note that this shouldn't normally be shown.
+    // Since this is a duplicate card collection, it will not have keyed cards.
     List<Widget> plays = new List<Widget>();
     for (int i = 0; i < 4; i++) {
       plays.add(new CardCollectionComponent(
@@ -248,7 +317,7 @@
     }
     cardCollections.add(new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.teal[600]),
+            new BoxDecoration(backgroundColor: Colors.teal[600]),
         width: config.width,
         child:
             new Flex(plays, justifyContent: FlexJustifyContent.spaceAround)));
@@ -257,22 +326,23 @@
 
     Widget playArea = new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.teal[500]),
+            new BoxDecoration(backgroundColor: Colors.teal[500]),
         width: config.width,
         child: new Center(
             child: new CardCollectionComponent(
                 game.cardCollections[p + HeartsGame.OFFSET_PLAY],
                 true,
                 CardCollectionOrientation.show1,
+                useKeys: true,
                 acceptCallback: _makeGameMoveCallback,
                 acceptType: p == game.whoseTurn ? DropType.card : DropType.none,
                 width: config.width,
                 backgroundColor: p == game.whoseTurn
-                    ? material.Colors.white
-                    : material.Colors.grey[500],
+                    ? Colors.white
+                    : Colors.grey[500],
                 altColor: p == game.whoseTurn
-                    ? material.Colors.grey[200]
-                    : material.Colors.grey[600])));
+                    ? Colors.grey[200]
+                    : Colors.grey[600])));
     cardCollections.add(playArea);
 
     List<logic_card.Card> cards = game.cardCollections[p];
@@ -280,7 +350,8 @@
         cards, game.playerNumber == p, CardCollectionOrientation.suit,
         dragChildren: game.whoseTurn == p,
         comparator: _compareCards,
-        width: config.width);
+        width: config.width,
+        useKeys: true);
     cardCollections.add(c); // flex
 
     cardCollections.add(new Text("Player ${game.whoseTurn}'s turn"));
@@ -305,7 +376,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           // TODO(alexfandrianto): we want to show round by round, deltas too, don't we?
@@ -321,7 +392,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton('Deal', game.dealCards),
@@ -351,7 +422,7 @@
     topCardWidgets.add(_makeButton(name, buttoncb, inactive: completed));
 
     Color bgColor =
-        completed ? material.Colors.teal[600] : material.Colors.teal[500];
+        completed ? Colors.teal[600] : Colors.teal[500];
 
     Widget topArea = new Container(
         decoration: new BoxDecoration(backgroundColor: bgColor),
@@ -367,8 +438,9 @@
         acceptType: draggable ? DropType.card : null,
         comparator: _compareCards,
         width: config.width,
-        backgroundColor: material.Colors.grey[500],
-        altColor: material.Colors.grey[700]);
+        backgroundColor: Colors.grey[500],
+        altColor: Colors.grey[700],
+        useKeys: true);
 
     return new Column(<Widget>[
       topArea,
@@ -384,8 +456,9 @@
         acceptCallback: cb,
         dragChildren: cb != null,
         acceptType: cb != null ? DropType.card : null,
-        backgroundColor: material.Colors.white,
-        altColor: material.Colors.grey[200]);
+        backgroundColor: Colors.white,
+        altColor: Colors.grey[200],
+        useKeys: true);
 
     if (cb == null) {
       ccc = new Container(child: ccc);
diff --git a/lib/components/proto/proto.part.dart b/lib/components/proto/proto.part.dart
index d714e89..7711ba0 100644
--- a/lib/components/proto/proto.part.dart
+++ b/lib/components/proto/proto.part.dart
@@ -32,7 +32,7 @@
 
     cardCollections.add(new Container(
         decoration: new BoxDecoration(
-            backgroundColor: material.Colors.green[500], borderRadius: 5.0),
+            backgroundColor: Colors.green[500], borderRadius: 5.0),
         child: new CardCollectionComponent(
             config.game.cardCollections[4], true, CardCollectionOrientation.show1,
             dragChildren: true,
@@ -44,7 +44,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex(cardCollections, direction: FlexDirection.vertical));
   }
 
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index 15ebe8f..a1fde84 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -15,12 +15,37 @@
 
 class SolitaireGameComponentState
     extends GameComponentState<SolitaireGameComponent> {
+
   @override
   Widget build(BuildContext context) {
+    SolitaireGame game = config.game as SolitaireGame;
+
+    print("Building Solitaire!");
+
+    // Build Solitaire and have it fill up the card level map.
+    // Unfortunately, this is required so that we can know which card components
+    // to collect.
+    Widget solitaireWidget = buildSolitaire();
+
+    List<Widget> children = new List<Widget>();
+    children.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: Colors.grey[300]),
+          width: config.width,
+          height: config.height,
+          child: solitaireWidget));
+    if (game.phase == SolitairePhase.Play) {
+      // All cards are visible.
+      List<int> visibleCardCollections = game.cardCollections.asMap().keys.toList();
+
+      children.add(this.buildCardAnimationLayer(visibleCardCollections));
+    }
+
     return new Container(
-        decoration:
-            new BoxDecoration(backgroundColor: material.Colors.grey[300]),
-        child: buildSolitaire());
+      width: config.width,
+      height: config.height,
+      child: new Stack(children)
+    );
   }
 
   void _cheatCallback() {
@@ -50,8 +75,8 @@
   @override
   Widget _makeButton(String text, NoArgCb callback, {bool inactive: false}) {
     var borderColor =
-        inactive ? material.Colors.grey[500] : material.Colors.white;
-    var backgroundColor = inactive ? material.Colors.grey[500] : null;
+        inactive ? Colors.grey[500] : Colors.white;
+    var backgroundColor = inactive ? Colors.grey[500] : null;
     return new FlatButton(
         child: new Container(
             decoration: new BoxDecoration(
@@ -104,44 +129,19 @@
     double cardSize = config.width / 8.0;
 
     List<Widget> row1 = new List<Widget>();
-    row1.add(new Row([
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 0],
+    List<CardCollectionComponent> aces = [0, 1, 2, 3].map((int i) {
+      return new CardCollectionComponent(
+          game.cardCollections[SolitaireGame.OFFSET_ACES + i],
           true,
           CardCollectionOrientation.show1,
           widthCard: cardSize,
           heightCard: cardSize,
           acceptCallback: _moveCallback,
           dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 1],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 2],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 3],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-    ]));
+          acceptType: DropType.card,
+          useKeys: true);
+    }).toList();
+    row1.add(new Row(aces));
 
     row1.add(new Row([
       new CardCollectionComponent(
@@ -150,14 +150,16 @@
           CardCollectionOrientation.show1,
           widthCard: cardSize,
           heightCard: cardSize,
-          dragChildren: true),
+          dragChildren: true,
+          useKeys: true),
       new InkWell(
           child: new CardCollectionComponent(
               game.cardCollections[SolitaireGame.OFFSET_DRAW],
               false,
               CardCollectionOrientation.show1,
               widthCard: cardSize,
-              heightCard: cardSize),
+              heightCard: cardSize,
+              useKeys: true),
           onTap: game.canDrawCard ? game.drawCardUI : null),
     ]));
 
@@ -169,7 +171,8 @@
               false,
               CardCollectionOrientation.show1,
               widthCard: cardSize,
-              heightCard: cardSize),
+              heightCard: cardSize,
+              useKeys: true),
           onTap: game.cardCollections[SolitaireGame.OFFSET_UP + i].length == 0
               ? _makeFlipCallback(i)
               : null));
@@ -185,7 +188,8 @@
           height: config.height * 0.6,
           acceptCallback: _moveCallback,
           dragChildren: true,
-          acceptType: DropType.card));
+          acceptType: DropType.card,
+          useKeys: true));
     }
 
     return new Column([
@@ -201,7 +205,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton("Return to Lobby", _quitGameCallback),
@@ -214,7 +218,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton('Deal', game.dealCardsUI),
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
index 3a9b8af..1bc1ece 100644
--- a/lib/src/syncbase/croupier_client.dart
+++ b/lib/src/syncbase/croupier_client.dart
@@ -52,10 +52,12 @@
     if (!(await app.exists())) {
       await app.create(util.openPerms);
     }
+    util.log('CroupierClient.got app');
     var db = app.noSqlDatabase(util.dbName);
     if (!(await db.exists())) {
       await db.create(util.openPerms);
     }
+    util.log('CroupierClient.got db');
     return db;
   }
 
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 367ee76..c7ef239 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -198,7 +198,14 @@
     util.log(
         "SettingsScanHandler Found ${s.instanceUuid} ${s.instanceName} ${s.addrs}");
 
-    _cc.joinSyncgroup(s.addrs[0]);
+    // TODO(alexfandrianto): Filter based on instanceName?
+    if (s.addrs.length > 0) {
+      _cc.joinSyncgroup(s.addrs[0]);
+    } else {
+      // An unexpected service was found. Who is advertising it?
+      // https://github.com/vanadium/issues/issues/846
+      util.log("Unexpected service found: ${s.toString()}");
+    }
   }
 
   void lost(List<int> instanceId) {