// Copyright 2014 The Flutter 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 'package:flutter/foundation.dart'; import 'package:flutter/physics.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../scheduler/scheduler_tester.dart'; void main() { setUp(() { WidgetsFlutterBinding.ensureInitialized(); WidgetsBinding.instance ..resetEpoch() ..platformDispatcher.onBeginFrame = null ..platformDispatcher.onDrawFrame = null; }); test('Can set value during status callback', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); bool didComplete = false; bool didDismiss = false; controller.addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { didComplete = true; controller.value = 0.0; controller.forward(); } else if (status == AnimationStatus.dismissed) { didDismiss = true; controller.value = 0.0; controller.forward(); } }); controller.forward(); expect(didComplete, isFalse); expect(didDismiss, isFalse); tick(const Duration(seconds: 1)); expect(didComplete, isFalse); expect(didDismiss, isFalse); tick(const Duration(seconds: 2)); expect(didComplete, isTrue); expect(didDismiss, isTrue); controller.stop(); controller.dispose(); }); test('Receives status callbacks for forward and reverse', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); final List<double> valueLog = <double>[]; final List<AnimationStatus> log = <AnimationStatus>[]; controller ..addStatusListener(log.add) ..addListener(() { valueLog.add(controller.value); }); expect(log, equals(<AnimationStatus>[])); expect(valueLog, equals(<AnimationStatus>[])); controller.forward(); expect(log, equals(<AnimationStatus>[AnimationStatus.forward])); expect(valueLog, equals(<AnimationStatus>[])); controller.reverse(); expect(log, equals(<AnimationStatus>[AnimationStatus.forward, AnimationStatus.dismissed])); expect(valueLog, equals(<AnimationStatus>[])); controller.reverse(); expect(log, equals(<AnimationStatus>[AnimationStatus.forward, AnimationStatus.dismissed])); expect(valueLog, equals(<AnimationStatus>[])); log.clear(); controller.forward(); expect(log, equals(<AnimationStatus>[AnimationStatus.forward])); expect(valueLog, equals(<AnimationStatus>[])); controller.forward(); expect(log, equals(<AnimationStatus>[AnimationStatus.forward])); expect(valueLog, equals(<AnimationStatus>[])); controller.reverse(); log.clear(); tick(const Duration(seconds: 10)); expect(log, equals(<AnimationStatus>[])); expect(valueLog, equals(<AnimationStatus>[])); tick(const Duration(seconds: 20)); expect(log, equals(<AnimationStatus>[])); expect(valueLog, equals(<AnimationStatus>[])); tick(const Duration(seconds: 30)); expect(log, equals(<AnimationStatus>[])); expect(valueLog, equals(<AnimationStatus>[])); tick(const Duration(seconds: 40)); expect(log, equals(<AnimationStatus>[])); expect(valueLog, equals(<AnimationStatus>[])); controller.stop(); controller.dispose(); }); test('Forward and reverse from values', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); final List<double> valueLog = <double>[]; final List<AnimationStatus> statusLog = <AnimationStatus>[]; controller ..addStatusListener(statusLog.add) ..addListener(() { valueLog.add(controller.value); }); controller.reverse(from: 0.2); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse ])); expect(valueLog, equals(<double>[ 0.2 ])); expect(controller.value, equals(0.2)); statusLog.clear(); valueLog.clear(); controller.forward(from: 0.0); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.dismissed, AnimationStatus.forward ])); expect(valueLog, equals(<double>[ 0.0 ])); expect(controller.value, equals(0.0)); controller.dispose(); }); test('Forward and reverse with different durations', () { AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), reverseDuration: const Duration(milliseconds: 50), vsync: const TestVSync(), ); controller.forward(); tick(const Duration(milliseconds: 10)); tick(const Duration(milliseconds: 30)); expect(controller.value, moreOrLessEquals(0.2)); tick(const Duration(milliseconds: 60)); expect(controller.value, moreOrLessEquals(0.5)); tick(const Duration(milliseconds: 90)); expect(controller.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 120)); expect(controller.value, moreOrLessEquals(1.0)); controller.stop(); controller.reverse(); tick(const Duration(milliseconds: 210)); tick(const Duration(milliseconds: 220)); expect(controller.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 230)); expect(controller.value, moreOrLessEquals(0.6)); tick(const Duration(milliseconds: 240)); expect(controller.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 260)); expect(controller.value, moreOrLessEquals(0.0)); controller.stop(); // Swap which duration is longer. controller = AnimationController( duration: const Duration(milliseconds: 50), reverseDuration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); controller.forward(); tick(const Duration(milliseconds: 10)); tick(const Duration(milliseconds: 30)); expect(controller.value, moreOrLessEquals(0.4)); tick(const Duration(milliseconds: 60)); expect(controller.value, moreOrLessEquals(1.0)); tick(const Duration(milliseconds: 90)); expect(controller.value, moreOrLessEquals(1.0)); controller.stop(); controller.reverse(); tick(const Duration(milliseconds: 210)); tick(const Duration(milliseconds: 220)); expect(controller.value, moreOrLessEquals(0.9)); tick(const Duration(milliseconds: 230)); expect(controller.value, moreOrLessEquals(0.8)); tick(const Duration(milliseconds: 240)); expect(controller.value, moreOrLessEquals(0.7)); tick(const Duration(milliseconds: 260)); expect(controller.value, moreOrLessEquals(0.5)); tick(const Duration(milliseconds: 310)); expect(controller.value, moreOrLessEquals(0.0)); controller.stop(); controller.dispose(); }); test('Forward only from value', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); final List<double> valueLog = <double>[]; final List<AnimationStatus> statusLog = <AnimationStatus>[]; controller ..addStatusListener(statusLog.add) ..addListener(() { valueLog.add(controller.value); }); controller.forward(from: 0.2); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward ])); expect(valueLog, equals(<double>[ 0.2 ])); expect(controller.value, equals(0.2)); controller.dispose(); }); test('Can fling to upper and lower bounds', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); controller.fling(); tick(const Duration(seconds: 1)); tick(const Duration(seconds: 2)); expect(controller.value, 1.0); controller.stop(); final AnimationController largeRangeController = AnimationController( duration: const Duration(milliseconds: 100), lowerBound: -30.0, upperBound: 45.0, vsync: const TestVSync(), ); largeRangeController.fling(); tick(const Duration(seconds: 3)); tick(const Duration(seconds: 4)); expect(largeRangeController.value, 45.0); largeRangeController.fling(velocity: -1.0); tick(const Duration(seconds: 5)); tick(const Duration(seconds: 6)); expect(largeRangeController.value, -30.0); largeRangeController.stop(); controller.dispose(); largeRangeController.dispose(); }); test('Custom springDescription can be applied', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); final AnimationController customSpringController = AnimationController( vsync: const TestVSync(), ); controller.fling(); // Will produce longer and smoother animation than the default. customSpringController.fling( springDescription: SpringDescription.withDampingRatio( mass: 0.01, stiffness: 10.0, ratio: 2.0, ), ); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(customSpringController.value < controller.value, true); controller.dispose(); customSpringController.dispose(); }); test('lastElapsedDuration control test', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); controller.forward(); tick(const Duration(milliseconds: 20)); tick(const Duration(milliseconds: 30)); tick(const Duration(milliseconds: 40)); expect(controller.lastElapsedDuration, equals(const Duration(milliseconds: 20))); controller.stop(); controller.dispose(); }); test('toString control test', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); expect(controller, hasOneLineDescription); controller.forward(); tick(const Duration(milliseconds: 10)); tick(const Duration(milliseconds: 20)); expect(controller, hasOneLineDescription); tick(const Duration(milliseconds: 30)); expect(controller, hasOneLineDescription); controller.reverse(); tick(const Duration(milliseconds: 40)); tick(const Duration(milliseconds: 50)); expect(controller, hasOneLineDescription); controller.stop(); controller.dispose(); }); test('velocity test - linear', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 1000), vsync: const TestVSync(), ); // mid-flight controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 500)); expect(controller.velocity, inInclusiveRange(0.9, 1.1)); // edges controller.forward(); expect(controller.velocity, inInclusiveRange(0.4, 0.6)); tick(Duration.zero); expect(controller.velocity, inInclusiveRange(0.4, 0.6)); tick(const Duration(milliseconds: 5)); expect(controller.velocity, inInclusiveRange(0.9, 1.1)); controller.forward(from: 0.5); expect(controller.velocity, inInclusiveRange(0.4, 0.6)); tick(Duration.zero); expect(controller.velocity, inInclusiveRange(0.4, 0.6)); tick(const Duration(milliseconds: 5)); expect(controller.velocity, inInclusiveRange(0.9, 1.1)); // stopped controller.forward(from: 1.0); expect(controller.velocity, 0.0); tick(Duration.zero); expect(controller.velocity, 0.0); tick(const Duration(milliseconds: 500)); expect(controller.velocity, 0.0); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 1000)); expect(controller.velocity, 0.0); controller.stop(); controller.dispose(); }); test('Disposed AnimationController toString works', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); controller.dispose(); expect(controller, hasOneLineDescription); }); test('AnimationController error handling', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); expect(controller.forward, throwsFlutterError); expect(controller.reverse, throwsFlutterError); expect(() { controller.animateTo(0.5); }, throwsFlutterError); expect(controller.repeat, throwsFlutterError); controller.dispose(); FlutterError? result; try { controller.dispose(); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result!.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' AnimationController.dispose() called more than once.\n' ' A given AnimationController cannot be disposed more than once.\n' ' The following AnimationController object was disposed multiple\n' ' times:\n' ' AnimationController#00000(⏮ 0.000; paused; DISPOSED)\n', ), ); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); result.debugFillProperties(builder); final DiagnosticsNode controllerProperty = builder.properties.last; expect(controllerProperty.name, 'The following AnimationController object was disposed multiple times'); expect(controllerProperty.value, controller); }); test('AnimationController repeat() throws if period is not specified', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); expect(() { controller.repeat(); }, throwsFlutterError); expect(() { controller.repeat(); }, throwsFlutterError); controller.dispose(); }); test('Do not animate if already at target', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( value: 0.5, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, equals(0.5)); controller.animateTo(0.5, duration: const Duration(milliseconds: 100)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ])); expect(controller.value, equals(0.5)); controller.dispose(); }); test('Do not animate to upperBound if already at upperBound', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( value: 1.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, equals(1.0)); controller.animateTo(1.0, duration: const Duration(milliseconds: 100)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ])); expect(controller.value, equals(1.0)); controller.dispose(); }); test('Do not animate to lowerBound if already at lowerBound', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( value: 0.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, equals(0.0)); controller.animateTo(0.0, duration: const Duration(milliseconds: 100)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ])); expect(controller.value, equals(0.0)); controller.dispose(); }); test('Do not animate if already at target mid-flight (forward)', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( value: 0.0, duration: const Duration(milliseconds: 1000), vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, equals(0.0)); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 500)); expect(controller.value, inInclusiveRange(0.4, 0.6)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward ])); final double currentValue = controller.value; controller.animateTo(currentValue, duration: const Duration(milliseconds: 100)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); expect(controller.value, currentValue); controller.dispose(); }); test('Do not animate if already at target mid-flight (reverse)', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( value: 1.0, duration: const Duration(milliseconds: 1000), vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, equals(1.0)); controller.reverse(); tick(Duration.zero); tick(const Duration(milliseconds: 500)); expect(controller.value, inInclusiveRange(0.4, 0.6)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse ])); final double currentValue = controller.value; controller.animateTo(currentValue, duration: const Duration(milliseconds: 100)); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.completed ])); expect(controller.value, currentValue); controller.dispose(); }); test('animateTo can deal with duration == Duration.zero', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), vsync: const TestVSync(), ); controller.forward(from: 0.2); expect(controller.value, 0.2); controller.animateTo(1.0, duration: Duration.zero); expect(SchedulerBinding.instance.transientCallbackCount, equals(0), reason: 'Expected no animation.'); expect(controller.value, 1.0); controller.dispose(); }); test('resetting animation works at all phases', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 0.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.reset(); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); statusLog.clear(); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(controller.status, AnimationStatus.forward); controller.reset(); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.dismissed ])); controller.value = 1.0; statusLog.clear(); controller.reverse(); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(controller.status, AnimationStatus.reverse); controller.reset(); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.dismissed ])); statusLog.clear(); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.status, AnimationStatus.completed); controller.reset(); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed, AnimationStatus.dismissed ])); controller.dispose(); }); test('setting value directly sets correct status', () { final AnimationController controller = AnimationController( value: 0.0, vsync: const TestVSync(), ); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.value = 0.5; expect(controller.value, 0.5); expect(controller.status, AnimationStatus.forward); controller.value = 1.0; expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); controller.value = 0.5; expect(controller.value, 0.5); expect(controller.status, AnimationStatus.forward); controller.value = 0.0; expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.dispose(); }); test('animateTo sets correct status', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 0.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); // Animate from 0.0 to 0.5 controller.animateTo(0.5); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.5); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); // Animate from 0.5 to 1.0 controller.animateTo(1.0); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 1.0); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); // Animate from 1.0 to 0.5 controller.animateTo(0.5); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.5); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); // Animate from 0.5 to 1.0 controller.animateTo(0.0); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.0); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); controller.dispose(); }); test('after a reverse call animateTo sets correct status', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 1.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); controller.reverse(); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.0); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.dismissed ])); statusLog.clear(); controller.animateTo(0.5); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.5); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); controller.dispose(); }); test('after a forward call animateTo sets correct status', () { final List<AnimationStatus> statusLog = <AnimationStatus>[]; final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 0.0, vsync: const TestVSync(), )..addStatusListener(statusLog.add); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.forward(); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 1.0); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); controller.animateTo(0.5); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.5); expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ])); statusLog.clear(); controller.dispose(); }); test( 'calling repeat with reverse set to true makes the animation alternate ' 'between lowerBound and upperBound values on each repeat', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 0.0, vsync: const TestVSync(), ); expect(controller.value, 0.0); controller.repeat(reverse: true); tick(Duration.zero); tick(const Duration(milliseconds: 25)); expect(controller.value, 0.25); tick(Duration.zero); tick(const Duration(milliseconds: 125)); expect(controller.value, 0.75); controller.reset(); controller.value = 1.0; expect(controller.value, 1.0); controller.repeat(reverse: true); tick(Duration.zero); tick(const Duration(milliseconds: 25)); expect(controller.value, 0.75); tick(Duration.zero); tick(const Duration(milliseconds: 125)); expect(controller.value, 0.25); controller.reset(); controller.value = 0.5; expect(controller.value, 0.5); controller.repeat(reverse: true); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(controller.value, 1.0); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 0.0); controller.dispose(); }, ); test( 'calling repeat with specified min and max values makes the animation ' 'alternate between min and max values on each repeat', () { final AnimationController controller = AnimationController( duration: const Duration(milliseconds: 100), value: 0.0, vsync: const TestVSync(), ); expect(controller.value, 0.0); controller.repeat(reverse: true, min: 0.5, max: 1.0); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(controller.value, 0.75); tick(Duration.zero); tick(const Duration(milliseconds: 100)); expect(controller.value, 1.00); tick(Duration.zero); tick(const Duration(milliseconds: 200)); expect(controller.value, 0.5); controller.reset(); controller.value = 0.0; expect(controller.value, 0.0); controller.repeat(reverse: true, min: 1.0, max: 1.0); tick(Duration.zero); tick(const Duration(milliseconds: 25)); expect(controller.value, 1.0); tick(Duration.zero); tick(const Duration(milliseconds: 125)); expect(controller.value, 1.0); controller.dispose(); }, ); group('AnimationBehavior', () { test('Default values for constructor', () { final AnimationController controller = AnimationController(vsync: const TestVSync()); expect(controller.animationBehavior, AnimationBehavior.normal); final AnimationController repeating = AnimationController.unbounded(vsync: const TestVSync()); expect(repeating.animationBehavior, AnimationBehavior.preserve); controller.dispose(); repeating.dispose(); }); test('AnimationBehavior.preserve runs at normal speed when animatingTo', () { debugSemanticsDisableAnimations = true; final AnimationController controller = AnimationController( vsync: const TestVSync(), animationBehavior: AnimationBehavior.preserve, ); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.animateTo(1.0, duration: const Duration(milliseconds: 100)); tick(Duration.zero); tick(const Duration(milliseconds: 50)); expect(controller.value, 0.5); expect(controller.status, AnimationStatus.forward); tick(Duration.zero); tick(const Duration(milliseconds: 150)); expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); debugSemanticsDisableAnimations = false; controller.dispose(); }); test('AnimationBehavior.normal runs at 20x speed when animatingTo', () { debugSemanticsDisableAnimations = true; final AnimationController controller = AnimationController( vsync: const TestVSync(), ); expect(controller.value, 0.0); expect(controller.status, AnimationStatus.dismissed); controller.animateTo(1.0, duration: const Duration(milliseconds: 100)); tick(Duration.zero); tick(const Duration(microseconds: 2500)); expect(controller.value, 0.5); expect(controller.status, AnimationStatus.forward); tick(Duration.zero); tick(const Duration(milliseconds: 5, microseconds: 1000)); expect(controller.value, 1.0); expect(controller.status, AnimationStatus.completed); debugSemanticsDisableAnimations = null; controller.dispose(); }); test('AnimationBehavior.normal runs "faster" than AnimationBehavior.preserve', () { debugSemanticsDisableAnimations = true; final AnimationController controller = AnimationController( vsync: const TestVSync(), ); final AnimationController fastController = AnimationController( vsync: const TestVSync(), ); controller.fling(animationBehavior: AnimationBehavior.preserve); fastController.fling(animationBehavior: AnimationBehavior.normal); tick(Duration.zero); tick(const Duration(milliseconds: 50)); // We don't assert a specific faction that normal animation. expect(controller.value < fastController.value, true); debugSemanticsDisableAnimations = null; controller.dispose(); fastController.dispose(); }); }); test('AnimationController methods assert _ticker is not null', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); controller.dispose(); expect(() => controller.animateBack(0), throwsAssertionError); expect(() => controller.animateTo(0), throwsAssertionError); expect(() => controller.animateWith(GravitySimulation(0, 0, 0, 0)), throwsAssertionError); expect(() => controller.stop(), throwsAssertionError); expect(() => controller.forward(), throwsAssertionError); expect(() => controller.reverse(), throwsAssertionError); }); test('Simulations run forward', () { final List<AnimationStatus> statuses = <AnimationStatus>[]; final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(seconds: 1), )..addStatusListener((AnimationStatus status) { statuses.add(status); }); controller.animateWith(TestSimulation()); tick(Duration.zero); tick(const Duration(seconds: 2)); expect(statuses, <AnimationStatus>[AnimationStatus.forward]); controller.dispose(); }); test('Simulations run forward even after a reverse run', () { final List<AnimationStatus> statuses = <AnimationStatus>[]; final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(seconds: 1), )..addStatusListener((AnimationStatus status) { statuses.add(status); }); controller.reverse(from: 1.0); tick(Duration.zero); tick(const Duration(seconds: 2)); expect(statuses, <AnimationStatus>[AnimationStatus.completed, AnimationStatus.reverse, AnimationStatus.dismissed]); statuses.clear(); controller.animateWith(TestSimulation()); tick(Duration.zero); tick(const Duration(seconds: 2)); expect(statuses, <AnimationStatus>[AnimationStatus.forward]); controller.dispose(); }); test('Repeating animation with reverse: true report as forward and reverse', () { final List<AnimationStatus> statuses = <AnimationStatus>[]; final AnimationController controller = AnimationController( vsync: const TestVSync(), duration: const Duration(seconds: 1), )..addStatusListener((AnimationStatus status) { statuses.add(status); }); controller.repeat(reverse: true); tick(Duration.zero); tick(const Duration(milliseconds: 999)); expect(statuses, <AnimationStatus>[AnimationStatus.forward]); statuses.clear(); tick(const Duration(seconds: 1)); expect(statuses, <AnimationStatus>[AnimationStatus.reverse]); controller.dispose(); }); test('AnimateBack can runs successfully with just "reverseDuration" property set', () { final List<AnimationStatus> statuses = <AnimationStatus>[]; final AnimationController controller = AnimationController( reverseDuration: const Duration(seconds: 2), vsync: const TestVSync(), )..addStatusListener((AnimationStatus status) { statuses.add(status); }); controller.animateBack(0.8); expect(statuses, <AnimationStatus>[AnimationStatus.reverse]); statuses.clear(); tick(Duration.zero); tick(const Duration(seconds: 2)); expect(statuses, <AnimationStatus>[AnimationStatus.dismissed]); controller.dispose(); }); group('AnimationController "duration" error test', () { test('AnimationController forward() will throw an error if there is no default duration', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); late FlutterError error; try { controller.forward(); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect( error.toStringDeep(), 'FlutterError\n' ' AnimationController.forward() called with no default duration.\n' ' The "duration" property should be set, either in the constructor\n' ' or later, before calling the forward() function.\n', ); controller.dispose(); }); test( 'AnimationController animateTo() will throw an error if there is no explicit duration ' 'and default duration', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); late FlutterError error; try { controller.animateTo(0.8); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect( error.toStringDeep(), 'FlutterError\n' ' AnimationController.animateTo() called with no explicit duration\n' ' and no default duration.\n' ' Either the "duration" argument to the animateTo() method should\n' ' be provided, or the "duration" property should be set, either in\n' ' the constructor or later, before calling the animateTo()\n' ' function.\n', ); controller.dispose(); }, ); test('AnimationController reverse() will throw an error if there is no default duration or reverseDuration', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); late FlutterError error; try { controller.reverse(); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect( error.toStringDeep(), 'FlutterError\n' ' AnimationController.reverse() called with no default duration or\n' ' reverseDuration.\n' ' The "duration" or "reverseDuration" property should be set,\n' ' either in the constructor or later, before calling the reverse()\n' ' function.\n', ); controller.dispose(); }); test( 'AnimationController animateBack() will throw an error if there is no explicit duration and ' 'no default duration or reverseDuration', () { final AnimationController controller = AnimationController( vsync: const TestVSync(), ); late FlutterError error; try { controller.animateBack(0.8); } on FlutterError catch (e) { error = e; } expect(error, isNotNull); expect( error.toStringDeep(), 'FlutterError\n' ' AnimationController.animateBack() called with no explicit\n' ' duration and no default duration or reverseDuration.\n' ' Either the "duration" argument to the animateBack() method should\n' ' be provided, or the "duration" or "reverseDuration" property\n' ' should be set, either in the constructor or later, before calling\n' ' the animateBack() function.\n', ); controller.dispose(); }, ); }); } class TestSimulation extends Simulation { @override double dx(double time) => time; @override bool isDone(double time) => false; @override double x(double time) => time; }