AnimationController.fling should respect upper and lower bounds (#4533)

Instead of flinging to between 0.0 and 1.0, we should adapt the default
spring to the controller's upper and lower bounds.

Fixes #3545
diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart
index 1f0e8c8..bb54bc7 100644
--- a/packages/flutter/lib/src/animation/animation_controller.dart
+++ b/packages/flutter/lib/src/animation/animation_controller.dart
@@ -194,10 +194,10 @@
   }
 
   /// Flings the timeline with an optional force (defaults to a critically
-  /// damped spring) and initial velocity. If velocity is positive, the
-  /// animation will complete, otherwise it will dismiss.
+  /// damped spring within [lowerBound] and [upperBound]) and initial velocity.
+  /// If velocity is positive, the animation will complete, otherwise it will dismiss.
   Future<Null> fling({ double velocity: 1.0, Force force }) {
-    force ??= kDefaultSpringForce;
+    force ??= kDefaultSpringForce.copyWith(left: lowerBound, right: upperBound);
     _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
     return animateWith(force.release(value, velocity));
   }
diff --git a/packages/flutter/lib/src/animation/forces.dart b/packages/flutter/lib/src/animation/forces.dart
index 700f9bf..c346442 100644
--- a/packages/flutter/lib/src/animation/forces.dart
+++ b/packages/flutter/lib/src/animation/forces.dart
@@ -33,6 +33,19 @@
   /// Where to put the spring's resting point when releasing right.
   final double right;
 
+  /// Creates a copy of this spring force but with the given fields replaced with the new values.
+  SpringForce copyWith({
+    SpringDescription spring,
+    double left,
+    double right
+  }) {
+    return new SpringForce(
+      spring ?? this.spring,
+      left: left ?? this.left,
+      right: right ?? this.right
+    );
+  }
+
   /// How pricely to terminate the simulation.
   ///
   /// We overshoot the target by this distance, but stop the simulation when
diff --git a/packages/flutter/test/animation/animation_controller_test.dart b/packages/flutter/test/animation/animation_controller_test.dart
index 66fbecd..61387b3 100644
--- a/packages/flutter/test/animation/animation_controller_test.dart
+++ b/packages/flutter/test/animation/animation_controller_test.dart
@@ -7,7 +7,7 @@
 import 'package:test/test.dart';
 
 void main() {
-  test("Can set value during status callback", () {
+  test('Can set value during status callback', () {
     WidgetsFlutterBinding.ensureInitialized();
     AnimationController controller = new AnimationController(
       duration: const Duration(milliseconds: 100)
@@ -39,7 +39,7 @@
     controller.stop();
   });
 
-  test("Receives status callbacks for forward and reverse", () {
+  test('Receives status callbacks for forward and reverse', () {
     WidgetsFlutterBinding.ensureInitialized();
     AnimationController controller = new AnimationController(
       duration: const Duration(milliseconds: 100)
@@ -102,7 +102,7 @@
     controller.stop();
   });
 
-  test("Forward and reverse from values", () {
+  test('Forward and reverse from values', () {
     WidgetsFlutterBinding.ensureInitialized();
     AnimationController controller = new AnimationController(
       duration: const Duration(milliseconds: 100)
@@ -129,4 +129,33 @@
     expect(valueLog, equals(<double>[ 0.0 ]));
     expect(controller.value, equals(0.0));
   });
+
+  test('Can fling to upper and lower bounds', () {
+    WidgetsFlutterBinding.ensureInitialized();
+    AnimationController controller = new AnimationController(
+      duration: const Duration(milliseconds: 100)
+    );
+
+    controller.fling();
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 1));
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 2));
+    expect(controller.value, 1.0);
+    controller.stop();
+
+    AnimationController largeRangeController = new AnimationController(
+      duration: const Duration(milliseconds: 100),
+      lowerBound: -30.0,
+      upperBound: 45.0
+    );
+
+    largeRangeController.fling();
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 3));
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 4));
+    expect(largeRangeController.value, 45.0);
+    largeRangeController.fling(velocity: -1.0);
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 5));
+    WidgetsBinding.instance.handleBeginFrame(const Duration(seconds: 6));
+    expect(largeRangeController.value, -30.0);
+    largeRangeController.stop();
+  });
 }