First pass at support for pinch gestures; panning issues (needs testing)
Conflicts:
	sky/packages/sky/lib/gestures/drag.dart
diff --git a/sky/packages/sky/lib/gestures/constants.dart b/sky/packages/sky/lib/gestures/constants.dart
index 6f1f2e0..281ff59 100644
--- a/sky/packages/sky/lib/gestures/constants.dart
+++ b/sky/packages/sky/lib/gestures/constants.dart
@@ -17,6 +17,8 @@
 const double kTouchSlop = 8.0;  // Logical pixels
 const double kDoubleTapTouchSlop = kTouchSlop;  // Logical pixels
 const double kPagingTouchSlop = kTouchSlop * 2.0;  // Logical pixels
+const double kPanSlop = kTouchSlop * 2.0;  // Logical pixels
+const double kPinchSlop = kTouchSlop;  // Logical pixels
 const double kDoubleTapSlop = 100.0;  // Logical pixels
 const double kWindowTouchSlop = 16.0;  // Logical pixels
 const double kMinFlingVelocity = 50.0;  // Logical pixels / second
diff --git a/sky/packages/sky/lib/gestures/drag.dart b/sky/packages/sky/lib/gestures/drag.dart
index df12580..867e5d4 100644
--- a/sky/packages/sky/lib/gestures/drag.dart
+++ b/sky/packages/sky/lib/gestures/drag.dart
@@ -79,7 +79,7 @@
     if (_state != DragState.accepted) {
       _state = DragState.accepted;
       T delta = _pendingDragDelta;
-      _pendingDragDelta = null;
+      _pendingDragDelta = _initialPendingDragDelta;
       if (onStart != null)
         onStart();
       if (delta != _initialPendingDragDelta && onUpdate != null)
@@ -149,6 +149,6 @@
   sky.Offset get _initialPendingDragDelta => sky.Offset.zero;
   sky.Offset _getDragDelta(sky.PointerEvent event) => new sky.Offset(event.dx, event.dy);
   bool get _hasSufficientPendingDragDeltaToAccept {
-    return _pendingDragDelta.dx.abs() > kTouchSlop || _pendingDragDelta.dy.abs() > kTouchSlop;
+    return _pendingDragDelta.distance > kPanSlop;
   }
 }
diff --git a/sky/packages/sky/lib/gestures/pinch.dart b/sky/packages/sky/lib/gestures/pinch.dart
new file mode 100644
index 0000000..800cf4b
--- /dev/null
+++ b/sky/packages/sky/lib/gestures/pinch.dart
@@ -0,0 +1,139 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:math' as math;
+import 'dart:sky' as sky;
+
+import 'package:sky/gestures/arena.dart';
+import 'package:sky/gestures/recognizer.dart';
+import 'package:sky/gestures/constants.dart';
+
+enum PinchState {
+  ready,
+  possible,
+  started,
+  ended
+}
+
+typedef void GesturePinchStartCallback();
+typedef void GesturePinchUpdateCallback(double scale);
+typedef void GesturePinchEndCallback();
+
+class PinchGestureRecognizer extends GestureRecognizer {
+  PinchGestureRecognizer({ PointerRouter router, this.onStart, this.onUpdate, this.onEnd })
+    : super(router: router);
+
+  GesturePinchStartCallback onStart;
+  GesturePinchUpdateCallback onUpdate;
+  GesturePinchEndCallback onEnd;
+
+  PinchState _state = PinchState.ready;
+
+  double _initialSpan;
+  double _currentSpan;
+  Map<int, sky.Point> _pointerLocations;
+
+  double get _scaleFactor => _initialSpan > 0 ? _currentSpan / _initialSpan : 1.0;
+
+  void addPointer(sky.PointerEvent event) {
+    startTrackingPointer(event.pointer);
+    if (_state == PinchState.ready) {
+      _state = PinchState.possible;
+      _initialSpan = 0.0;
+      _currentSpan = 0.0;
+      _pointerLocations = new Map<int, sky.Point>();
+    }
+  }
+
+  void handleEvent(sky.PointerEvent event) {
+    assert(_state != PinchState.ready);
+    bool configChanged = false;
+    switch(event.type) {
+      case 'pointerup':
+        configChanged = true;
+        _pointerLocations.remove(event.pointer);
+        break;
+      case 'pointerdown':
+        configChanged = true;
+        _pointerLocations[event.pointer] = new sky.Point(event.x, event.y);
+        break;
+      case 'pointermove':
+        _pointerLocations[event.pointer] = new sky.Point(event.x, event.y);
+        break;
+    }
+
+    int count = _pointerLocations.keys.length;
+
+    // Compute the focal point
+    sky.Point focalPoint = sky.Point.origin;
+    for (int pointer in _pointerLocations.keys)
+      focalPoint += _pointerLocations[pointer].toOffset();
+    focalPoint = new sky.Point(focalPoint.x / count, focalPoint.y / count);
+
+    // Span is the average deviation from focal point
+    double totalDeviation = 0.0;
+    for (int pointer in _pointerLocations.keys)
+      totalDeviation += (focalPoint - _pointerLocations[pointer]).distance;
+    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
+
+    if (configChanged) {
+      _initialSpan = _currentSpan;
+      if (_state == PinchState.started) {
+        _state = PinchState.ended;
+        if (onEnd != null)
+          onEnd();
+      }
+    }
+
+    if (_state == PinchState.ready)
+      _state = PinchState.possible;
+
+    if (_state == PinchState.possible &&
+        (_currentSpan - _initialSpan).abs() > kPinchSlop) {
+      resolve(GestureDisposition.accepted);
+    }
+
+    if (_state == PinchState.ended && _currentSpan != _initialSpan) {
+      _state = PinchState.started;
+      if (onStart != null)
+        onStart();
+    }
+
+    if (_state == PinchState.started && onUpdate != null)
+      onUpdate(_scaleFactor);
+
+    stopTrackingIfPointerNoLongerDown(event);
+  }
+
+  void acceptGesture(int pointer) {
+    if (_state != PinchState.started) {
+      _state = PinchState.started;
+      if (onStart != null)
+        onStart();
+      if (onUpdate != null)
+        onUpdate(_scaleFactor);
+    }
+  }
+
+  void didStopTrackingLastPointer(int pointer) {
+    switch(_state) {
+      case PinchState.possible:
+        resolve(GestureDisposition.rejected);
+        break;
+      case PinchState.ready:
+        assert(false);
+        break;
+      case PinchState.started:
+        assert(false);
+        break;
+      case PinchState.ended:
+        break;
+    }
+    _state = PinchState.ready;
+  }
+
+  void dispose() {
+    super.dispose();
+  }
+}
diff --git a/sky/packages/sky/lib/src/widgets/gesture_detector.dart b/sky/packages/sky/lib/src/widgets/gesture_detector.dart
index 6ed6e45..423227b 100644
--- a/sky/packages/sky/lib/src/widgets/gesture_detector.dart
+++ b/sky/packages/sky/lib/src/widgets/gesture_detector.dart
@@ -6,6 +6,7 @@
 
 import 'package:sky/gestures/drag.dart';
 import 'package:sky/gestures/long_press.dart';
+import 'package:sky/gestures/pinch.dart';
 import 'package:sky/gestures/recognizer.dart';
 import 'package:sky/gestures/show_press.dart';
 import 'package:sky/gestures/tap.dart';
@@ -27,7 +28,10 @@
     this.onHorizontalDragEnd,
     this.onPanStart,
     this.onPanUpdate,
-    this.onPanEnd
+    this.onPanEnd,
+    this.onPinchStart,
+    this.onPinchUpdate,
+    this.onPinchEnd
   }) : super(key: key);
 
   Widget child;
@@ -47,6 +51,10 @@
   GesturePanUpdateCallback onPanUpdate;
   GesturePanEndCallback onPanEnd;
 
+  GesturePinchStartCallback onPinchStart;
+  GesturePinchUpdateCallback onPinchUpdate;
+  GesturePinchEndCallback onPinchEnd;
+
   void syncConstructorArguments(GestureDetector source) {
     child = source.child;
     onTap = source.onTap;
@@ -61,6 +69,9 @@
     onPanStart = source.onPanStart;
     onPanUpdate = source.onPanUpdate;
     onPanEnd = source.onPanEnd;
+    onPinchStart = source.onPinchStart;
+    onPinchUpdate = source.onPinchUpdate;
+    onPinchEnd = source.onPinchEnd;
     _syncGestureListeners();
   }
 
@@ -108,6 +119,13 @@
     return _pan;
   }
 
+  PinchGestureRecognizer _pinch;
+  PinchGestureRecognizer _ensurePinch() {
+    if (_pinch == null)
+      _pinch = new PinchGestureRecognizer(router: _router);
+    return _pinch;
+  }
+
   void didMount() {
     super.didMount();
     _syncGestureListeners();
@@ -121,6 +139,7 @@
     _verticalDrag = _ensureDisposed(_verticalDrag);
     _horizontalDrag = _ensureDisposed(_horizontalDrag);
     _pan = _ensureDisposed(_pan);
+    _pinch = _ensureDisposed(_pinch);
   }
 
   void _syncGestureListeners() {
@@ -130,6 +149,7 @@
     _syncVerticalDrag();
     _syncHorizontalDrag();
     _syncPan();
+    _syncPinch();
   }
 
   void _syncTap() {
@@ -186,6 +206,17 @@
     }
   }
 
+  void _syncPinch() {
+    if (onPinchStart == null && onPinchUpdate == null && onPinchEnd == null) {
+      _pinch = _ensureDisposed(_pan);
+    } else {
+      _ensurePinch()
+        ..onStart = onPinchStart
+        ..onUpdate = onPinchUpdate
+        ..onEnd = onPinchEnd;
+    }
+  }
+
   GestureRecognizer _ensureDisposed(GestureRecognizer recognizer) {
     recognizer?.dispose();
     return null;
@@ -204,6 +235,8 @@
       _horizontalDrag.addPointer(event);
     if (_pan != null)
       _pan.addPointer(event);
+    if (_pinch != null)
+      _pinch.addPointer(event);
     return EventDisposition.processed;
   }