mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge e0ed7c3ef30321d9adad436589cb39670439f4fd into 06df71c51446e96939c6a615b7c34ce9123806ba
This commit is contained in:
commit
7bc7bb23e9
@ -242,6 +242,11 @@ class AnimationController extends Animation<double>
|
||||
/// * `vsync` is the required [TickerProvider] for the current context. It can
|
||||
/// be changed by calling [resync]. See [TickerProvider] for advice on
|
||||
/// obtaining a ticker provider.
|
||||
///
|
||||
/// * [pauseWhenNoListeners] controls whether the animation controller should
|
||||
/// pause frame scheduling when there are no listeners. When set to true,
|
||||
/// the controller will not request frames (saving CPU/GPU resources) until
|
||||
/// a listener is added. Defaults to false for backward compatibility.
|
||||
AnimationController({
|
||||
double? value,
|
||||
this.duration,
|
||||
@ -250,6 +255,7 @@ class AnimationController extends Animation<double>
|
||||
this.lowerBound = 0.0,
|
||||
this.upperBound = 1.0,
|
||||
this.animationBehavior = AnimationBehavior.normal,
|
||||
this.pauseWhenNoListeners = false,
|
||||
required TickerProvider vsync,
|
||||
}) : assert(upperBound >= lowerBound),
|
||||
_direction = _AnimationDirection.forward {
|
||||
@ -272,6 +278,9 @@ class AnimationController extends Animation<double>
|
||||
/// be changed by calling [resync]. See [TickerProvider] for advice on
|
||||
/// obtaining a ticker provider.
|
||||
///
|
||||
/// * [pauseWhenNoListeners] controls whether the animation controller should
|
||||
/// pause frame scheduling when there are no listeners. Defaults to false.
|
||||
///
|
||||
/// This constructor is most useful for animations that will be driven using a
|
||||
/// physics simulation, especially when the physics simulation has no
|
||||
/// pre-determined bounds.
|
||||
@ -282,6 +291,7 @@ class AnimationController extends Animation<double>
|
||||
this.debugLabel,
|
||||
required TickerProvider vsync,
|
||||
this.animationBehavior = AnimationBehavior.preserve,
|
||||
this.pauseWhenNoListeners = false,
|
||||
}) : lowerBound = double.negativeInfinity,
|
||||
upperBound = double.infinity,
|
||||
_direction = _AnimationDirection.forward {
|
||||
@ -308,6 +318,24 @@ class AnimationController extends Animation<double>
|
||||
/// [AnimationController.unbounded] constructor.
|
||||
final AnimationBehavior animationBehavior;
|
||||
|
||||
/// Whether the animation controller should pause frame scheduling when there
|
||||
/// are no listeners.
|
||||
///
|
||||
/// When a listener is added, frame scheduling resumes automatically.
|
||||
///
|
||||
/// Defaults to false for backward compatibility. Set to true to enable this
|
||||
/// optimization.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final controller = AnimationController(
|
||||
/// vsync: this,
|
||||
/// duration: const Duration(seconds: 1),
|
||||
/// pauseWhenNoListeners: true, // Enable optimization
|
||||
/// );
|
||||
/// ```
|
||||
final bool pauseWhenNoListeners;
|
||||
|
||||
/// Returns an [Animation<double>] for this animation controller, so that a
|
||||
/// pointer to this object can be passed around without allowing users of that
|
||||
/// pointer to mutate the [AnimationController] state.
|
||||
@ -327,6 +355,44 @@ class AnimationController extends Animation<double>
|
||||
|
||||
Ticker? _ticker;
|
||||
|
||||
/// The number of registered listeners (both value and status listeners).
|
||||
///
|
||||
/// This is used to pause the ticker when there are no listeners, avoiding
|
||||
/// unnecessary frame scheduling. Only used when [pauseWhenNoListeners] is true.
|
||||
int _listenerCount = 0;
|
||||
|
||||
/// Called when a listener is registered with [addListener] or [addStatusListener].
|
||||
///
|
||||
/// When [pauseWhenNoListeners] is true, this resumes the ticker when the
|
||||
/// first listener is added while the animation is running.
|
||||
@override
|
||||
void didRegisterListener() {
|
||||
if (!pauseWhenNoListeners) {
|
||||
return;
|
||||
}
|
||||
_listenerCount += 1;
|
||||
if (_listenerCount > 0 && isAnimating) {
|
||||
// Resume ticker when we get our first listener while animating.
|
||||
_ticker?.resume();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when a listener is unregistered with [removeListener] or [removeStatusListener].
|
||||
///
|
||||
/// When [pauseWhenNoListeners] is true, this pauses the ticker when the
|
||||
/// last listener is removed while the animation is running.
|
||||
@override
|
||||
void didUnregisterListener() {
|
||||
if (!pauseWhenNoListeners) {
|
||||
return;
|
||||
}
|
||||
_listenerCount -= 1;
|
||||
if (_listenerCount == 0 && isAnimating) {
|
||||
// Pause ticker when we lose our last listener while animating.
|
||||
_ticker?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/// Recreates the [Ticker] with the new [TickerProvider].
|
||||
void resync(TickerProvider vsync) {
|
||||
final Ticker oldTicker = _ticker!;
|
||||
@ -864,6 +930,18 @@ class AnimationController extends Animation<double>
|
||||
_lastElapsedDuration = Duration.zero;
|
||||
_value = clampDouble(simulation.x(0.0), lowerBound, upperBound);
|
||||
final TickerFuture result = _ticker!.start();
|
||||
|
||||
if (pauseWhenNoListeners) {
|
||||
if (_listenerCount == 0) {
|
||||
if (!_ticker!.isPaused) {
|
||||
_ticker!.pause();
|
||||
}
|
||||
} else {
|
||||
if (_ticker!.isPaused) {
|
||||
_ticker!.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
_status = (_direction == _AnimationDirection.forward)
|
||||
? AnimationStatus.forward
|
||||
: AnimationStatus.reverse;
|
||||
|
||||
@ -131,8 +131,8 @@ class Ticker {
|
||||
/// Whether this [Ticker] has scheduled a call to call its callback
|
||||
/// on the next frame.
|
||||
///
|
||||
/// A ticker that is [muted] can be active (see [isActive]) yet not be
|
||||
/// ticking. In that case, the ticker will not call its callback, and
|
||||
/// A ticker that is [muted] or [isPaused] can be active (see [isActive]) yet
|
||||
/// not be ticking. In that case, the ticker will not call its callback, and
|
||||
/// [isTicking] will be false, but time will still be progressing.
|
||||
///
|
||||
/// This will return false if the [SchedulerBinding.lifecycleState] is one
|
||||
@ -145,6 +145,9 @@ class Ticker {
|
||||
if (muted) {
|
||||
return false;
|
||||
}
|
||||
if (isPaused) {
|
||||
return false;
|
||||
}
|
||||
if (SchedulerBinding.instance.framesEnabled) {
|
||||
return true;
|
||||
}
|
||||
@ -256,6 +259,57 @@ class Ticker {
|
||||
@protected
|
||||
bool get scheduled => _animationId != null;
|
||||
|
||||
/// Whether this ticker is currently paused by its consumer.
|
||||
///
|
||||
/// A paused ticker will not schedule frames, but time continues to elapse.
|
||||
///
|
||||
/// Unlike [muted], which is controlled by the [TickerProvider] (e.g., based
|
||||
/// on [TickerMode]), this property is controlled by the ticker's consumer
|
||||
/// (e.g., [AnimationController]) to temporarily suspend frame scheduling.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pause], which sets this to true.
|
||||
/// * [resume], which sets this to false.
|
||||
bool _isPaused = false;
|
||||
|
||||
/// Whether this ticker is currently paused by its consumer.
|
||||
bool get isPaused => _isPaused;
|
||||
|
||||
/// Pauses frame scheduling for this ticker.
|
||||
///
|
||||
/// When paused, the ticker remains active and time continues to elapse,
|
||||
/// but no frames are scheduled and the callback is not called.
|
||||
///
|
||||
/// This is typically used by [AnimationController] to suspend frame
|
||||
/// scheduling when there are no listeners.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [resume], which resumes frame scheduling.
|
||||
void pause() {
|
||||
if (!_isPaused) {
|
||||
_isPaused = true;
|
||||
unscheduleTick();
|
||||
}
|
||||
}
|
||||
|
||||
/// Resumes frame scheduling for this ticker after a [pause].
|
||||
///
|
||||
/// If [shouldScheduleTick] is true, the ticker will schedule a frame.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pause], which pauses frame scheduling.
|
||||
void resume() {
|
||||
if (_isPaused) {
|
||||
_isPaused = false;
|
||||
if (shouldScheduleTick) {
|
||||
scheduleTick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a tick should be scheduled.
|
||||
///
|
||||
/// If this is true, then calling [scheduleTick] should succeed.
|
||||
@ -265,8 +319,9 @@ class Ticker {
|
||||
/// * A tick has already been scheduled for the coming frame.
|
||||
/// * The ticker is not active ([start] has not been called).
|
||||
/// * The ticker is not ticking, e.g. because it is [muted] (see [isTicking]).
|
||||
/// * The ticker is [isPaused].
|
||||
@protected
|
||||
bool get shouldScheduleTick => !muted && isActive && !scheduled;
|
||||
bool get shouldScheduleTick => !muted && !isPaused && isActive && !scheduled;
|
||||
|
||||
void _tick(Duration timeStamp) {
|
||||
assert(isTicking);
|
||||
@ -331,6 +386,7 @@ class Ticker {
|
||||
assert(_future == null);
|
||||
assert(_startTime == null);
|
||||
assert(_animationId == null);
|
||||
assert(!_isPaused);
|
||||
assert(
|
||||
(originalTicker._future == null) == (originalTicker._startTime == null),
|
||||
'Cannot absorb Ticker after it has been disposed.',
|
||||
@ -338,6 +394,7 @@ class Ticker {
|
||||
if (originalTicker._future != null) {
|
||||
_future = originalTicker._future;
|
||||
_startTime = originalTicker._startTime;
|
||||
_isPaused = originalTicker._isPaused;
|
||||
if (shouldScheduleTick) {
|
||||
scheduleTick();
|
||||
}
|
||||
|
||||
@ -1333,6 +1333,103 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('pauseWhenNoListeners', () {
|
||||
test('does not schedule frames when pauseWhenNoListeners is true and no listeners', () {
|
||||
final controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: const TestVSync(),
|
||||
pauseWhenNoListeners: true,
|
||||
);
|
||||
|
||||
// Start animation without listeners
|
||||
controller.forward();
|
||||
|
||||
// Animation should be in forward status but value should not update
|
||||
// because ticker is muted (no frames scheduled)
|
||||
expect(controller.status, AnimationStatus.forward);
|
||||
expect(controller.isAnimating, isTrue);
|
||||
|
||||
tick(const Duration(milliseconds: 10));
|
||||
tick(const Duration(milliseconds: 50));
|
||||
|
||||
// Value should still be 0 because no frames were scheduled
|
||||
expect(controller.value, 0.0);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('schedules frames when listener is added with pauseWhenNoListeners true', () {
|
||||
final controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: const TestVSync(),
|
||||
pauseWhenNoListeners: true,
|
||||
);
|
||||
|
||||
// Start animation without listeners
|
||||
controller.forward();
|
||||
expect(controller.value, 0.0);
|
||||
|
||||
// Add a listener - this should unmute the ticker
|
||||
controller.addListener(() {});
|
||||
|
||||
tick(const Duration(milliseconds: 10));
|
||||
tick(const Duration(milliseconds: 60));
|
||||
|
||||
// Now value should update because ticker is no longer muted
|
||||
expect(controller.value, moreOrLessEquals(0.5));
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('stops scheduling frames when last listener is removed with pauseWhenNoListeners true', () {
|
||||
final controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: const TestVSync(),
|
||||
pauseWhenNoListeners: true,
|
||||
);
|
||||
|
||||
void listener() {}
|
||||
controller.addListener(listener);
|
||||
|
||||
controller.forward();
|
||||
tick(const Duration(milliseconds: 10));
|
||||
tick(const Duration(milliseconds: 30));
|
||||
|
||||
// Value should update because we have a listener
|
||||
expect(controller.value, moreOrLessEquals(0.2));
|
||||
|
||||
// Remove the listener
|
||||
controller.removeListener(listener);
|
||||
|
||||
// Advance time - value should not change because ticker is now muted
|
||||
final valueBeforeRemoval = controller.value;
|
||||
tick(const Duration(milliseconds: 60));
|
||||
tick(const Duration(milliseconds: 90));
|
||||
|
||||
expect(controller.value, valueBeforeRemoval);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('default pauseWhenNoListeners is false (backward compatible)', () {
|
||||
final controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: const TestVSync(),
|
||||
);
|
||||
|
||||
// Start animation without listeners
|
||||
controller.forward();
|
||||
|
||||
tick(const Duration(milliseconds: 10));
|
||||
tick(const Duration(milliseconds: 60));
|
||||
|
||||
// Value should update even without listeners (backward compatible behavior)
|
||||
expect(controller.value, moreOrLessEquals(0.5));
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestSimulation extends Simulation {
|
||||
|
||||
@ -211,4 +211,113 @@ void main() {
|
||||
areCreateAndDispose,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Ticker pause control test', (WidgetTester tester) async {
|
||||
var tickCount = 0;
|
||||
void handleTick(Duration duration) {
|
||||
tickCount += 1;
|
||||
}
|
||||
|
||||
final ticker = Ticker(handleTick);
|
||||
addTearDown(ticker.dispose);
|
||||
|
||||
expect(ticker.isPaused, isFalse);
|
||||
expect(ticker.isTicking, isFalse);
|
||||
expect(ticker.isActive, isFalse);
|
||||
|
||||
ticker.start();
|
||||
|
||||
expect(ticker.isPaused, isFalse);
|
||||
expect(ticker.isTicking, isTrue);
|
||||
expect(ticker.isActive, isTrue);
|
||||
expect(tickCount, equals(0));
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(tickCount, equals(1));
|
||||
|
||||
ticker.pause();
|
||||
expect(ticker.isPaused, isTrue);
|
||||
expect(ticker.isTicking, isFalse);
|
||||
expect(ticker.isActive, isTrue);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(tickCount, equals(1));
|
||||
|
||||
ticker.resume();
|
||||
expect(ticker.isPaused, isFalse);
|
||||
expect(ticker.isTicking, isTrue);
|
||||
expect(ticker.isActive, isTrue);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
|
||||
expect(tickCount, equals(2));
|
||||
|
||||
ticker.stop();
|
||||
});
|
||||
|
||||
testWidgets('Ticker pause before start has no effect', (WidgetTester tester) async {
|
||||
var tickCount = 0;
|
||||
void handleTick(Duration duration) {
|
||||
tickCount += 1;
|
||||
}
|
||||
|
||||
final ticker = Ticker(handleTick);
|
||||
addTearDown(ticker.dispose);
|
||||
|
||||
ticker.pause();
|
||||
expect(ticker.isPaused, isTrue);
|
||||
expect(ticker.isActive, isFalse);
|
||||
|
||||
ticker.start();
|
||||
expect(ticker.isPaused, isTrue);
|
||||
expect(ticker.isActive, isTrue);
|
||||
expect(ticker.isTicking, isFalse);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tickCount, equals(0));
|
||||
|
||||
ticker.resume();
|
||||
expect(ticker.isTicking, isTrue);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tickCount, equals(1));
|
||||
|
||||
ticker.stop();
|
||||
});
|
||||
|
||||
testWidgets('Ticker stop clears pause state implicitly via restart', (WidgetTester tester) async {
|
||||
var tickCount = 0;
|
||||
void handleTick(Duration duration) {
|
||||
tickCount += 1;
|
||||
}
|
||||
|
||||
final ticker = Ticker(handleTick);
|
||||
addTearDown(ticker.dispose);
|
||||
|
||||
ticker.start();
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tickCount, equals(1));
|
||||
|
||||
ticker.pause();
|
||||
expect(ticker.isPaused, isTrue);
|
||||
|
||||
ticker.stop();
|
||||
expect(ticker.isActive, isFalse);
|
||||
|
||||
ticker.start();
|
||||
expect(ticker.isActive, isTrue);
|
||||
expect(ticker.isPaused, isTrue);
|
||||
expect(ticker.isTicking, isFalse);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tickCount, equals(1)); // Still paused
|
||||
|
||||
ticker.resume();
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(tickCount, equals(2));
|
||||
|
||||
ticker.stop();
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user