Add slider minimum interaction time (#15358)

This change makes the discrete slider show the value indicator for a minimum amount of time for any interaction (tap, drag).
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index c53cb77..e5d780b 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:math' as math;
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart' show timeDilation;
 import 'package:flutter/widgets.dart';
 
 import 'constants.dart';
@@ -212,41 +214,55 @@
 
 class _SliderState extends State<Slider> with TickerProviderStateMixin {
   static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
-  static const Duration positionAnimationDuration = const Duration(milliseconds: 75);
+  static const Duration valueIndicatorAnimationDuration = const Duration(milliseconds: 100);
 
-  // Animation controller that is run when interactions occur (taps, drags,
-  // etc.).
-  AnimationController reactionController;
+  // Animation controller that is run when the overlay (a.k.a radial reaction)
+  // is shown in response to user interaction.
+  AnimationController overlayController;
+  // Animation controller that is run when the value indicator is being shown
+  // or hidden.
+  AnimationController valueIndicatorController;
   // Animation controller that is run when enabling/disabling the slider.
   AnimationController enableController;
   // Animation controller that is run when transitioning between one value
   // and the next on a discrete slider.
   AnimationController positionController;
+  Timer interactionTimer;
 
   @override
   void initState() {
     super.initState();
-    reactionController = new AnimationController(
+    overlayController = new AnimationController(
       duration: kRadialReactionDuration,
       vsync: this,
     );
+    valueIndicatorController = new AnimationController(
+      duration: valueIndicatorAnimationDuration,
+      vsync: this,
+    );
     enableController = new AnimationController(
       duration: enableAnimationDuration,
       vsync: this,
     );
     positionController = new AnimationController(
-      duration: positionAnimationDuration,
+      duration: Duration.zero,
       vsync: this,
     );
+    // Create timer in a cancelled state, so that we don't have to
+    // check for null below.
+    interactionTimer = new Timer(Duration.zero, () {});
+    interactionTimer.cancel();
     enableController.value = widget.onChanged != null ? 1.0 : 0.0;
-    positionController.value = widget.value;
+    positionController.value = _unlerp(widget.value);
   }
 
   @override
   void dispose() {
-    reactionController.dispose();
+    overlayController.dispose();
+    valueIndicatorController.dispose();
     enableController.dispose();
     positionController.dispose();
+    interactionTimer?.cancel();
     super.dispose();
   }
 
@@ -358,15 +374,6 @@
   }
 }
 
-const double _overlayRadius = 16.0;
-const double _overlayDiameter = _overlayRadius * 2.0;
-const double _railHeight = 2.0;
-const double _preferredRailWidth = 144.0;
-const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
-
-const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
-final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
-
 class _RenderSlider extends RenderBox {
   _RenderSlider({
     @required double value,
@@ -403,8 +410,12 @@
       ..onTapDown = _handleTapDown
       ..onTapUp = _handleTapUp
       ..onTapCancel = _endInteraction;
-    _reaction = new CurvedAnimation(
-      parent: _state.reactionController,
+    _overlayAnimation = new CurvedAnimation(
+      parent: _state.overlayController,
+      curve: Curves.fastOutSlowIn,
+    );
+    _valueIndicatorAnimation = new CurvedAnimation(
+      parent: _state.valueIndicatorController,
       curve: Curves.fastOutSlowIn,
     );
     _enableAnimation = new CurvedAnimation(
@@ -413,11 +424,34 @@
     );
   }
 
-  double get value => _value;
-  double _value;
+  static const Duration _positionAnimationDuration = const Duration(milliseconds: 75);
+  static const double _overlayRadius = 16.0;
+  static const double _overlayDiameter = _overlayRadius * 2.0;
+  static const double _railHeight = 2.0;
+  static const double _preferredRailWidth = 144.0;
+  static const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
+  static const Duration _minimumInteractionTime = const Duration(milliseconds: 500);
+  static const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
+  static final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
 
   _SliderState _state;
+  Animation<double> _overlayAnimation;
+  Animation<double> _valueIndicatorAnimation;
+  Animation<double> _enableAnimation;
+  final TextPainter _labelPainter = new TextPainter();
+  HorizontalDragGestureRecognizer _drag;
+  TapGestureRecognizer _tap;
+  bool _active = false;
+  double _currentDragValue = 0.0;
 
+  double get _railLength => size.width - _overlayDiameter;
+
+  bool get isInteractive => onChanged != null;
+
+  bool get isDiscrete => divisions != null && divisions > 0;
+
+  double get value => _value;
+  double _value;
   set value(double newValue) {
     assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
     final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
@@ -426,6 +460,14 @@
     }
     _value = convertedValue;
     if (isDiscrete) {
+      // Reset the duration to match the distance that we're traveling, so that
+      // whatever the distance, we still do it in _positionAnimationDuration,
+      // and if we get re-targeted in the middle, it still takes that long to
+      // get to the new location.
+      final double distance = (_value - _state.positionController.value).abs();
+      _state.positionController.duration = distance != 0.0
+        ? _positionAnimationDuration * (1.0 / distance)
+        : 0.0;
       _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
     } else {
       _state.positionController.value = convertedValue;
@@ -514,6 +556,25 @@
     _updateLabelPainter();
   }
 
+  bool get showValueIndicator {
+    bool showValueIndicator;
+    switch (_sliderTheme.showValueIndicator) {
+      case ShowValueIndicator.onlyForDiscrete:
+        showValueIndicator = isDiscrete;
+        break;
+      case ShowValueIndicator.onlyForContinuous:
+        showValueIndicator = !isDiscrete;
+        break;
+      case ShowValueIndicator.always:
+        showValueIndicator = true;
+        break;
+      case ShowValueIndicator.never:
+        showValueIndicator = false;
+        break;
+    }
+    return showValueIndicator;
+  }
+
   void _updateLabelPainter() {
     if (label != null) {
       _labelPainter
@@ -530,31 +591,19 @@
     markNeedsLayout();
   }
 
-  double get _railLength => size.width - _overlayDiameter;
-
-  Animation<double> _reaction;
-  Animation<double> _enableAnimation;
-  final TextPainter _labelPainter = new TextPainter();
-  HorizontalDragGestureRecognizer _drag;
-  TapGestureRecognizer _tap;
-  bool _active = false;
-  double _currentDragValue = 0.0;
-
-  bool get isInteractive => onChanged != null;
-
-  bool get isDiscrete => divisions != null && divisions > 0;
-
   @override
   void attach(PipelineOwner owner) {
     super.attach(owner);
-    _reaction.addListener(markNeedsPaint);
+    _overlayAnimation.addListener(markNeedsPaint);
+    _valueIndicatorAnimation.addListener(markNeedsPaint);
     _enableAnimation.addListener(markNeedsPaint);
     _state.positionController.addListener(markNeedsPaint);
   }
 
   @override
   void detach() {
-    _reaction.removeListener(markNeedsPaint);
+    _overlayAnimation.removeListener(markNeedsPaint);
+    _valueIndicatorAnimation.removeListener(markNeedsPaint);
     _enableAnimation.removeListener(markNeedsPaint);
     _state.positionController.removeListener(markNeedsPaint);
     super.detach();
@@ -588,7 +637,19 @@
       _active = true;
       _currentDragValue = _getValueFromGlobalPosition(globalPosition);
       onChanged(_discretize(_currentDragValue));
-      _state.reactionController.forward();
+      _state.overlayController.forward();
+      if (showValueIndicator) {
+        _state.valueIndicatorController.forward();
+        if (_state.interactionTimer.isActive) {
+          _state.interactionTimer.cancel();
+        }
+        _state.interactionTimer = new Timer(_minimumInteractionTime * timeDilation, () {
+          if (!_active && _state.valueIndicatorController.status == AnimationStatus.completed) {
+            _state.valueIndicatorController.reverse();
+          }
+          _state.interactionTimer.cancel();
+        });
+      }
     }
   }
 
@@ -596,7 +657,10 @@
     if (_active) {
       _active = false;
       _currentDragValue = 0.0;
-      _state.reactionController.reverse();
+      _state.overlayController.reverse();
+      if (showValueIndicator && !_state.interactionTimer.isActive) {
+        _state.valueIndicatorController.reverse();
+      }
     }
   }
 
@@ -696,15 +760,15 @@
   }
 
   void _paintOverlay(Canvas canvas, Offset center) {
-    if (!_reaction.isDismissed) {
+    if (!_overlayAnimation.isDismissed) {
       // TODO(gspencer) : We don't really follow the spec here for overlays.
       // The spec says to use 16% opacity for drawing over light material,
       // and 32% for colored material, but we don't really have a way to
       // know what the underlying color is, so there's no easy way to
       // implement this. Choosing the "light" version for now.
-      final Paint reactionPaint = new Paint()..color = _sliderTheme.overlayColor;
-      final double radius = _overlayRadiusTween.evaluate(_reaction);
-      canvas.drawCircle(center, radius, reactionPaint);
+      final Paint overlayPaint = new Paint()..color = _sliderTheme.overlayColor;
+      final double radius = _overlayRadiusTween.evaluate(_overlayAnimation);
+      canvas.drawCircle(center, radius, overlayPaint);
     }
   }
 
@@ -781,29 +845,15 @@
       rightTickMarkPaint,
     );
 
-    if (isInteractive && _reaction.status != AnimationStatus.dismissed && label != null) {
-      bool showValueIndicator;
-      switch (_sliderTheme.showValueIndicator) {
-        case ShowValueIndicator.onlyForDiscrete:
-          showValueIndicator = isDiscrete;
-          break;
-        case ShowValueIndicator.onlyForContinuous:
-          showValueIndicator = !isDiscrete;
-          break;
-        case ShowValueIndicator.always:
-          showValueIndicator = true;
-          break;
-        case ShowValueIndicator.never:
-          showValueIndicator = false;
-          break;
-      }
+    if (isInteractive && label != null &&
+        _valueIndicatorAnimation.status != AnimationStatus.dismissed) {
       if (showValueIndicator) {
         _sliderTheme.valueIndicatorShape.paint(
           this,
           context,
           isDiscrete,
           thumbCenter,
-          _reaction,
+          _valueIndicatorAnimation,
           _enableAnimation,
           _labelPainter,
           _sliderTheme,
@@ -818,7 +868,7 @@
       context,
       isDiscrete,
       thumbCenter,
-      _reaction,
+      _overlayAnimation,
       _enableAnimation,
       label != null ? _labelPainter : null,
       _sliderTheme,
diff --git a/packages/flutter/lib/src/material/slider_theme.dart b/packages/flutter/lib/src/material/slider_theme.dart
index 1603e5b..9983db2 100644
--- a/packages/flutter/lib/src/material/slider_theme.dart
+++ b/packages/flutter/lib/src/material/slider_theme.dart
@@ -469,19 +469,19 @@
     );
     description.add(new DiagnosticsProperty<Color>('activeRailColor', activeRailColor, defaultValue: defaultData.activeRailColor));
     description.add(new DiagnosticsProperty<Color>('inactiveRailColor', inactiveRailColor, defaultValue: defaultData.inactiveRailColor));
-    description.add(new DiagnosticsProperty<Color>('disabledActiveRailColor', disabledActiveRailColor, defaultValue: defaultData.disabledActiveRailColor));
-    description.add(new DiagnosticsProperty<Color>('disabledInactiveRailColor', disabledInactiveRailColor, defaultValue: defaultData.disabledInactiveRailColor));
-    description.add(new DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor));
-    description.add(new DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor));
-    description.add(new DiagnosticsProperty<Color>('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor));
-    description.add(new DiagnosticsProperty<Color>('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor));
+    description.add(new DiagnosticsProperty<Color>('disabledActiveRailColor', disabledActiveRailColor, defaultValue: defaultData.disabledActiveRailColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('disabledInactiveRailColor', disabledInactiveRailColor, defaultValue: defaultData.disabledInactiveRailColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor, level: DiagnosticLevel.debug));
     description.add(new DiagnosticsProperty<Color>('thumbColor', thumbColor, defaultValue: defaultData.thumbColor));
-    description.add(new DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor));
-    description.add(new DiagnosticsProperty<Color>('overlayColor', overlayColor, defaultValue: defaultData.overlayColor));
+    description.add(new DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<Color>('overlayColor', overlayColor, defaultValue: defaultData.overlayColor, level: DiagnosticLevel.debug));
     description.add(new DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor));
-    description.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape));
-    description.add(new DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape));
-    description.add(new DiagnosticsProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
+    description.add(new DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug));
+    description.add(new DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug));
+    description.add(new EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
   }
 }
 
@@ -595,8 +595,8 @@
 
   // Radius of the top lobe of the value indicator.
   static const double _topLobeRadius = 16.0;
-  // Baseline size of the label text. This is the size that the value indicator
-  // was designed to contain. We scale if from here to fit other sizes.
+  // Designed size of the label text. This is the size that the value indicator
+  // was designed to contain. We scale it from here to fit other sizes.
   static const double _labelTextDesignSize = 14.0;
   // Radius of the bottom lobe of the value indicator.
   static const double _bottomLobeRadius = 6.0;
diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart
index 670f301..a005bb0 100644
--- a/packages/flutter/test/material/slider_test.dart
+++ b/packages/flutter/test/material/slider_test.dart
@@ -177,6 +177,168 @@
     expect(updates, equals(1));
   });
 
+  testWidgets('Value indicator shows for a bit after being tapped', (WidgetTester tester) async {
+    final Key sliderKey = new UniqueKey();
+    double value = 0.0;
+
+    await tester.pumpWidget(
+      new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new StatefulBuilder(
+          builder: (BuildContext context, StateSetter setState) {
+            return new MediaQuery(
+              data: new MediaQueryData.fromWindow(window),
+              child: new Material(
+                child: new Center(
+                  child: new Slider(
+                    key: sliderKey,
+                    value: value,
+                    divisions: 4,
+                    onChanged: (double newValue) {
+                      setState(() {
+                        value = newValue;
+                      });
+                    },
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+
+    expect(value, equals(0.0));
+    await tester.tap(find.byKey(sliderKey));
+    expect(value, equals(0.5));
+    await tester.pump(const Duration(milliseconds: 100));
+    // Starts with the position animation and value indicator
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
+    await tester.pump(const Duration(milliseconds: 100));
+    // Value indicator is longer than position.
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
+    await tester.pump(const Duration(milliseconds: 100));
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
+    await tester.pump(const Duration(milliseconds: 100));
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
+    await tester.pump(const Duration(milliseconds: 100));
+    // Shown for long enough, value indicator is animated closed.
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
+    await tester.pump(const Duration(milliseconds: 101));
+    expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
+  });
+
+  testWidgets('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async {
+    final Key sliderKey = new UniqueKey();
+    double value = 0.0;
+    final List<Offset> log = <Offset>[];
+    final LoggingThumbShape loggingThumb = new LoggingThumbShape(log);
+    await tester.pumpWidget(
+      new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new StatefulBuilder(
+          builder: (BuildContext context, StateSetter setState) {
+            final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb);
+            return new MediaQuery(
+              data: new MediaQueryData.fromWindow(window),
+              child: new Material(
+                child: new Center(
+                  child: new SliderTheme(
+                    data: sliderTheme,
+                    child: new Slider(
+                      key: sliderKey,
+                      value: value,
+                      divisions: 4,
+                      onChanged: (double newValue) {
+                        setState(() {
+                          value = newValue;
+                        });
+                      },
+                    ),
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+
+    final List<Offset> expectedLog = <Offset>[
+      const Offset(16.0, 300.0),
+      const Offset(16.0, 300.0),
+      const Offset(400.0, 300.0),
+    ];
+    final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 100));
+    expect(value, equals(0.5));
+    expect(log.length, 3);
+    expect(log, orderedEquals(expectedLog));
+    await gesture.moveBy(const Offset(-500.0, 0.0));
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 10));
+    expect(value, equals(0.0));
+    expect(log.length, 5);
+    expect(log.last.dx, closeTo(386.3, 0.1));
+    // With no more gesture or value changes, the thumb position should still
+    // be redrawn in the animated position.
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 10));
+    expect(value, equals(0.0));
+    expect(log.length, 7);
+    expect(log.last.dx, closeTo(343.3, 0.1));
+    // Final position.
+    await tester.pump(const Duration(milliseconds: 80));
+    expectedLog.add(const Offset(16.0, 300.0));
+    expect(value, equals(0.0));
+    expect(log.length, 8);
+    expect(log.last.dx, closeTo(16.0, 0.1));
+    await gesture.up();
+  });
+
+  testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async {
+    final Key sliderKey = new UniqueKey();
+    double value = 0.0;
+    int updates = 0;
+
+    await tester.pumpWidget(
+      new Directionality(
+        textDirection: TextDirection.ltr,
+        child: new StatefulBuilder(
+          builder: (BuildContext context, StateSetter setState) {
+            return new MediaQuery(
+              data: new MediaQueryData.fromWindow(window),
+              child: new Material(
+                child: new Center(
+                  child: new Slider(
+                    key: sliderKey,
+                    value: value,
+                    onChanged: (double newValue) {
+                      setState(() {
+                        updates++;
+                        value = newValue;
+                      });
+                    },
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+
+    expect(value, equals(0.0));
+    await tester.tap(find.byKey(sliderKey));
+    expect(value, equals(0.5));
+    await tester.pump();
+    await tester.tap(find.byKey(sliderKey));
+    expect(value, equals(0.5));
+    await tester.pump();
+    expect(updates, equals(1));
+  });
+
   testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async {
     final Key sliderKey = new UniqueKey();
     double value = 0.0;
@@ -229,14 +391,14 @@
     await tester.pump(const Duration(milliseconds: 10));
     expect(value, equals(0.0));
     expect(log.length, 5);
-    expect(log.last.dx, closeTo(343.3, 0.1));
+    expect(log.last.dx, closeTo(386.3, 0.1));
     // With no more gesture or value changes, the thumb position should still
     // be redrawn in the animated position.
     await tester.pump();
     await tester.pump(const Duration(milliseconds: 10));
     expect(value, equals(0.0));
     expect(log.length, 7);
-    expect(log.last.dx, closeTo(185.5, 0.1));
+    expect(log.last.dx, closeTo(343.3, 0.1));
     // Final position.
     await tester.pump(const Duration(milliseconds: 80));
     expectedLog.add(const Offset(16.0, 300.0));
@@ -443,7 +605,11 @@
     expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveRailColor)));
 
     // Test colors for discrete slider with inactiveColor and activeColor set.
-    await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, divisions: 3));
+    await tester.pumpWidget(buildApp(
+      activeColor: customColor1,
+      inactiveColor: customColor2,
+      divisions: 3,
+    ));
     expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
     expect(
         sliderBox,
@@ -462,7 +628,7 @@
 
     // Test default theme for disabled widget.
     await tester.pumpWidget(buildApp(enabled: false));
-    await tester.pump(const Duration(seconds: 1)); // wait for disable animation to finish.
+    await tester.pumpAndSettle();
     expect(
         sliderBox,
         paints
@@ -489,9 +655,8 @@
     await tester.pumpWidget(buildApp(divisions: 3));
     Offset center = tester.getCenter(find.byType(Slider));
     TestGesture gesture = await tester.startGesture(center);
-    await tester.pump();
     // Wait for value indicator animation to finish.
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
     expect(value, equals(2.0 / 3.0));
     expect(
       sliderBox,
@@ -507,9 +672,8 @@
         ..circle(color: sliderTheme.thumbColor),
     );
     await gesture.up();
-    await tester.pump();
     // Wait for value indicator animation to finish.
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
 
     // Testing the custom colors are used for the indicator.
     await tester.pumpWidget(buildApp(
@@ -519,9 +683,8 @@
     ));
     center = tester.getCenter(find.byType(Slider));
     gesture = await tester.startGesture(center);
-    await tester.pump();
     // Wait for value indicator animation to finish.
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
     expect(value, equals(2.0 / 3.0));
     expect(
       sliderBox,
@@ -734,24 +897,22 @@
     await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
     Offset center = tester.getCenter(find.byType(Slider));
     TestGesture gesture = await tester.startGesture(center);
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
 
     expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
 
     await gesture.up();
-    await tester.pump(const Duration(seconds: 1));
+    await tester.pumpAndSettle();
 
     await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
     center = tester.getCenter(find.byType(Slider));
     gesture = await tester.startGesture(center);
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
 
     expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
 
     await gesture.up();
-    await tester.pump(const Duration(seconds: 1));
+    await tester.pumpAndSettle();
 
     // Check continuous
     await tester.pumpWidget(buildSlider(
@@ -761,13 +922,12 @@
     ));
     center = tester.getCenter(find.byType(Slider));
     gesture = await tester.startGesture(center);
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
 
     expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
 
     await gesture.up();
-    await tester.pump(const Duration(seconds: 1));
+    await tester.pumpAndSettle();
 
     await tester.pumpWidget(buildSlider(
       textScaleFactor: 2.0,
@@ -776,13 +936,12 @@
     ));
     center = tester.getCenter(find.byType(Slider));
     gesture = await tester.startGesture(center);
-    await tester.pump();
-    await tester.pump(const Duration(milliseconds: 500));
+    await tester.pumpAndSettle();
 
     expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
 
     await gesture.up();
-    await tester.pump(const Duration(seconds: 1));
+    await tester.pumpAndSettle();
   });
 
   testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async {
@@ -830,7 +989,7 @@
       TestGesture gesture = await tester.startGesture(Offset.zero);
       await tester.pump();
       await gesture.up();
-      await tester.pump(const Duration(seconds: 1));
+      await tester.pumpAndSettle();
       expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
       expect(
         sliderBox,
@@ -850,13 +1009,13 @@
       expect(
         sliderBox,
         paints
-          ..circle(x: 310.9375, y: 16.0, radius: 3.791776657104492)
+          ..circle(x: 105.0625, y: 16.0, radius: 3.791776657104492)
           ..circle(x: 17.0, y: 16.0, radius: 1.0)
           ..circle(x: 208.5, y: 16.0, radius: 1.0)
           ..circle(x: 400.0, y: 16.0, radius: 1.0)
           ..circle(x: 591.5, y: 16.0, radius: 1.0)
           ..circle(x: 783.0, y: 16.0, radius: 1.0)
-          ..circle(x: 310.9375, y: 16.0, radius: 6.0),
+          ..circle(x: 105.0625, y: 16.0, radius: 6.0),
       );
 
       // Reparenting in the middle of an animation should do nothing.
@@ -870,15 +1029,16 @@
       expect(
         sliderBox,
         paints
-          ..circle(x: 396.6802978515625, y: 16.0, radius: 8.0)
+          ..circle(x: 185.5457763671875, y: 16.0, radius: 8.0)
           ..circle(x: 17.0, y: 16.0, radius: 1.0)
           ..circle(x: 208.5, y: 16.0, radius: 1.0)
+          ..circle(x: 400.0, y: 16.0, radius: 1.0)
           ..circle(x: 591.5, y: 16.0, radius: 1.0)
           ..circle(x: 783.0, y: 16.0, radius: 1.0)
-          ..circle(x: 396.6802978515625, y: 16.0, radius: 6.0),
+          ..circle(x: 185.5457763671875, y: 16.0, radius: 6.0),
       );
       // Wait for animations to finish.
-      await tester.pump(const Duration(milliseconds: 300));
+      await tester.pumpAndSettle();
       expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
       expect(
         sliderBox,
@@ -891,8 +1051,7 @@
           ..circle(x: 400.0, y: 16.0, radius: 6.0),
       );
       await gesture.up();
-      await tester.pump();
-      await tester.pump(const Duration(seconds: 1));
+      await tester.pumpAndSettle();
       expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
       expect(
         sliderBox,
@@ -903,21 +1062,6 @@
           ..circle(x: 783.0, y: 16.0, radius: 1.0)
           ..circle(x: 400.0, y: 16.0, radius: 6.0),
       );
-      // Move to 0.0 again.
-      gesture = await tester.startGesture(Offset.zero);
-      await tester.pump();
-      await gesture.up();
-      await tester.pump(const Duration(seconds: 1));
-      expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
-      expect(
-        sliderBox,
-        paints
-          ..circle(x: 208.5, y: 16.0, radius: 1.0)
-          ..circle(x: 400.0, y: 16.0, radius: 1.0)
-          ..circle(x: 591.5, y: 16.0, radius: 1.0)
-          ..circle(x: 783.0, y: 16.0, radius: 1.0)
-          ..circle(x: 16.0, y: 16.0, radius: 6.0),
-      );
     }
 
     await tester.pumpWidget(buildSlider(1));
@@ -925,7 +1069,6 @@
     await testReparenting(false);
     // Now do it again with reparenting in the middle of an animation.
     await testReparenting(true);
-
   });
 
   testWidgets('Slider Semantics', (WidgetTester tester) async {
@@ -1025,9 +1168,8 @@
       await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled));
       final Offset center = tester.getCenter(find.byType(Slider));
       final TestGesture gesture = await tester.startGesture(center);
-      await tester.pump();
       // Wait for value indicator animation to finish.
-      await tester.pump(const Duration(milliseconds: 500));
+      await tester.pumpAndSettle();
 
       final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
       expect(