[Animation] Add granular frame forcing to animations (#173862)

This change adds opt-in, subtree-scoped frame forcing for Flutter
animations as described in
https://github.com/flutter/flutter/issues/174356#issuecomment-3287185284

Fixes https://github.com/flutter/flutter/issues/174356

This change could help fix #133533. 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Thomas Guerin 2025-11-12 00:34:27 +01:00 committed by GitHub
parent 1c09ce6285
commit 4ef262721c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 559 additions and 23 deletions

View File

@ -91,6 +91,14 @@ class Ticker {
TickerFuture? _future;
/// If true, this ticker will request frames using
/// [SchedulerBinding.scheduleForcedFrame] instead of [SchedulerBinding.scheduleFrame].
///
/// This allows granular control to advance frames even when frames are
/// typically disabled (e.g. when the app is in the background). This should be
/// used sparingly as it can increase battery usage.
bool forceFrames = false;
/// Whether this ticker has been silenced.
///
/// While silenced, a ticker's clock can still run, but the callback will not
@ -282,9 +290,15 @@ class Ticker {
void scheduleTick({bool rescheduling = false}) {
assert(!scheduled);
assert(shouldScheduleTick);
if (forceFrames) {
SchedulerBinding.instance.scheduleForcedFrame();
} else {
SchedulerBinding.instance.scheduleFrame();
}
_animationId = SchedulerBinding.instance.scheduleFrameCallback(
_tick,
rescheduling: rescheduling,
scheduleNewFrame: false,
);
}

View File

@ -8,6 +8,7 @@ library;
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
export 'package:flutter/scheduler.dart' show TickerProvider;
@ -22,8 +23,13 @@ export 'package:flutter/scheduler.dart' show TickerProvider;
/// widget-aware ticker providers. For example, using a
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
class TickerMode extends StatefulWidget {
/// Creates a widget that enables or disables tickers.
const TickerMode({super.key, required this.enabled, required this.child});
/// Creates a widget that enables or disables tickers and optionally forces frames.
const TickerMode({
super.key,
required this.enabled,
required this.child,
this.forceFrames = false,
});
/// The requested ticker mode for this subtree.
///
@ -38,6 +44,14 @@ class TickerMode extends StatefulWidget {
/// they just don't call their callbacks. Time still elapses.
final bool enabled;
/// If true, tickers in this subtree will force frames even if
/// frames would normally not be scheduled (e.g. even if the
/// device's screen is turned off).
///
/// Use sparingly as this will cause significantly higher battery
/// usage when the device should be idle.
final bool forceFrames;
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.child}
@ -56,6 +70,10 @@ class TickerMode extends StatefulWidget {
/// ```dart
/// bool tickingEnabled = TickerMode.of(context);
/// ```
@Deprecated(
'Use TickerMode.valuesOf to get both enabled and forceFrames. '
'This feature was deprecated after v3.35.0-0.0.pre.',
)
static bool of(BuildContext context) {
final _EffectiveTickerMode? widget = context
.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
@ -65,7 +83,7 @@ class TickerMode extends StatefulWidget {
/// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`,
/// which indicates whether tickers are enabled in the given subtree.
///
/// When that [TickerMode] enabled or disabled tickers, the listenable notifies
/// When that [TickerMode] enables or disables tickers, the listenable notifies
/// its listeners.
///
/// While the [ValueListenable] is stable for the lifetime of the surrounding
@ -87,29 +105,119 @@ class TickerMode extends StatefulWidget {
/// dependency between the provided `context` and the ancestor [TickerMode].
/// In this case, the widget automatically rebuilds when the ticker mode
/// changes or when it is moved to a new [TickerMode] ancestor, which
/// simplifies the management cost in the widget at the expensive of some
/// simplifies the management cost in the widget at the expense of some
/// potential unnecessary rebuilds.
///
/// In the absence of a [TickerMode] widget, this function returns a
/// [ValueListenable], whose [ValueListenable.value] is always true.
@Deprecated(
'Use TickerMode.getValuesNotifier to get both enabled and forceFrames. '
'This feature was deprecated after v3.35.0-0.0.pre.',
)
static ValueListenable<bool> getNotifier(BuildContext context) {
final _EffectiveTickerMode? widget = context
.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
return widget?.notifier ?? const _ConstantValueListenable<bool>(true);
}
/// Returns the requested ticker mode values for this subtree and establishes
/// a dependency on the ancestor [TickerMode], if any.
///
/// This is used automatically by [TickerProviderStateMixin] and
/// [SingleTickerProviderStateMixin] to decide if their tickers should be
/// enabled or disabled.
///
/// In the absence of a [TickerMode] widget, this defaults to enabled
/// tickers that don't force frames.
///
/// Typical usage is as follows:
///
/// ```dart
/// bool tickingEnabled = TickerMode.valuesOf(context).enabled;
/// ```
static TickerModeData valuesOf(BuildContext context) {
final _EffectiveTickerMode? widget = context
.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
return widget?.values ?? TickerModeData.fallback;
}
/// Obtains a [ValueListenable] from the [TickerMode] surrounding the `context`,
/// which indicates whether tickers are enabled in the given subtree.
///
/// When that [TickerMode] enabled or disabled tickers, the listenable notifies
/// its listeners.
///
/// While the [ValueListenable] is stable for the lifetime of the surrounding
/// [TickerMode], calling this method does not establish a dependency between
/// the `context` and the [TickerMode] and the widget owning the `context`
/// does not rebuild when the ticker mode data changes. This is preferable
/// when the ticker mode does not impact what is currently rendered on screen,
/// e.g. because it is only used to mute/unmute a [Ticker]. Since no dependency
/// is established, the widget owning the `context` is also not informed when
/// it is moved to a new location in the tree where it may have a different
/// [TickerMode] ancestor. When this happens, the widget must manually
/// unsubscribe from the old listenable, obtain a new one from the new ancestor
/// [TickerMode] by calling this method again, and re-subscribe to it.
/// [StatefulWidget]s can, for example, do this in [State.activate],
/// which is called after the widget has been moved to a new location.
///
/// Alternatively, [of] can be used instead of this method to create a
/// dependency between the provided `context` and the ancestor [TickerMode].
/// In this case, the widget automatically rebuilds when the ticker mode
/// changes or when it is moved to a new [TickerMode] ancestor, which
/// simplifies the management cost in the widget at the expensive of some
/// potential unnecessary rebuilds.
///
/// In the absence of a [TickerMode] widget, this function returns a
/// [ValueListenable], whose [ValueListenable.value] is
/// [TickerModeData.fallback].
static ValueListenable<TickerModeData> getValuesNotifier(BuildContext context) {
final _EffectiveTickerMode? widget = context
.getInheritedWidgetOfExactType<_EffectiveTickerMode>();
return widget?.valuesNotifier ??
const _ConstantTickerModeDataListenable(TickerModeData.fallback);
}
/// Creates a [TickerMode] that overrides the ambient ticker mode values.
///
/// The given `enabled` and `forceFrames` override the ambient values when not null;
/// otherwise the ambient values are preserved.
static Widget merge({Key? key, bool? enabled, bool? forceFrames, required Widget child}) {
return Builder(
builder: (BuildContext context) {
final _EffectiveTickerMode? parent = context
.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
final bool parentEnabled = parent?.enabled ?? TickerModeData.fallback.enabled;
final bool parentForce = parent?.forceFrames ?? TickerModeData.fallback.forceFrames;
return TickerMode(
key: key,
enabled: enabled ?? parentEnabled,
forceFrames: forceFrames ?? parentForce,
child: child,
);
},
);
}
@override
State<TickerMode> createState() => _TickerModeState();
}
class _TickerModeState extends State<TickerMode> {
bool _ancestorTicketMode = true;
final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(true);
bool _ancestorTickerMode = TickerModeData.fallback.enabled;
bool _ancestorForceFrames = TickerModeData.fallback.forceFrames;
final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(TickerModeData.fallback.enabled);
final ValueNotifier<TickerModeData> _effectiveValues = ValueNotifier<TickerModeData>(
TickerModeData.fallback,
);
@override
void didChangeDependencies() {
super.didChangeDependencies();
_ancestorTicketMode = TickerMode.of(context);
final _EffectiveTickerMode? parent = context
.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>();
_ancestorTickerMode = parent?.enabled ?? TickerModeData.fallback.enabled;
_ancestorForceFrames = parent?.forceFrames ?? TickerModeData.fallback.forceFrames;
_updateEffectiveMode();
}
@ -122,18 +230,24 @@ class _TickerModeState extends State<TickerMode> {
@override
void dispose() {
_effectiveMode.dispose();
_effectiveValues.dispose();
super.dispose();
}
void _updateEffectiveMode() {
_effectiveMode.value = _ancestorTicketMode && widget.enabled;
final bool enabled = _ancestorTickerMode && widget.enabled;
final bool force = _ancestorForceFrames || widget.forceFrames;
_effectiveMode.value = enabled;
_effectiveValues.value = TickerModeData(enabled: enabled, forceFrames: force);
}
@override
Widget build(BuildContext context) {
return _EffectiveTickerMode(
enabled: _effectiveMode.value,
forceFrames: _effectiveValues.value.forceFrames,
notifier: _effectiveMode,
valuesNotifier: _effectiveValues,
child: widget.child,
);
}
@ -154,13 +268,24 @@ class _TickerModeState extends State<TickerMode> {
}
class _EffectiveTickerMode extends InheritedWidget {
const _EffectiveTickerMode({required this.enabled, required this.notifier, required super.child});
const _EffectiveTickerMode({
required this.enabled,
required this.forceFrames,
required this.notifier,
required this.valuesNotifier,
required super.child,
});
final bool enabled;
final bool forceFrames;
final ValueNotifier<bool> notifier;
final ValueNotifier<TickerModeData> valuesNotifier;
TickerModeData get values => valuesNotifier.value;
@override
bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
bool updateShouldNotify(_EffectiveTickerMode oldWidget) =>
enabled != oldWidget.enabled || forceFrames != oldWidget.forceFrames;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -247,7 +372,7 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T>
super.dispose();
}
ValueListenable<bool>? _tickerModeNotifier;
ValueListenable<TickerModeData>? _tickerModeNotifier;
@override
void activate() {
@ -257,10 +382,16 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T>
_updateTicker();
}
void _updateTicker() => _ticker?.muted = !_tickerModeNotifier!.value;
void _updateTicker() {
final TickerModeData values = _tickerModeNotifier!.value;
if (_ticker != null) {
_ticker!.muted = !values.enabled;
_ticker!.forceFrames = values.forceFrames;
}
}
void _updateTickerModeNotifier() {
final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
final ValueListenable<TickerModeData> newNotifier = TickerMode.getValuesNotifier(context);
if (newNotifier == _tickerModeNotifier) {
return;
}
@ -318,11 +449,15 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
}
assert(_tickerModeNotifier != null);
_tickers ??= <_WidgetTicker>{};
final _WidgetTicker result = _WidgetTicker(
onTick,
this,
debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null,
)..muted = !_tickerModeNotifier!.value;
final TickerModeData values = _tickerModeNotifier!.value;
final _WidgetTicker result =
_WidgetTicker(
onTick,
this,
debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null,
)
..muted = !values.enabled
..forceFrames = values.forceFrames;
_tickers!.add(result);
return result;
}
@ -333,7 +468,7 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
_tickers!.remove(ticker);
}
ValueListenable<bool>? _tickerModeNotifier;
ValueListenable<TickerModeData>? _tickerModeNotifier;
@override
void activate() {
@ -345,15 +480,17 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
void _updateTickers() {
if (_tickers != null) {
final bool muted = !_tickerModeNotifier!.value;
final TickerModeData values = _tickerModeNotifier!.value;
final bool muted = !values.enabled;
for (final Ticker ticker in _tickers!) {
ticker.muted = muted;
ticker.forceFrames = values.forceFrames;
}
}
}
void _updateTickerModeNotifier() {
final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
final ValueListenable<TickerModeData> newNotifier = TickerMode.getValuesNotifier(context);
if (newNotifier == _tickerModeNotifier) {
return;
}
@ -442,3 +579,76 @@ class _ConstantValueListenable<T> implements ValueListenable<T> {
@override
final T value;
}
/// Immutable compound values that describe the effective ticker behavior
/// for a subtree.
///
/// Instances of this class are produced by [TickerMode.valuesOf] and
/// [TickerMode.getValuesNotifier] and reflect the values that apply at a given
/// location in the widget tree after taking ancestor [TickerMode] widgets into
/// account.
///
/// Semantics of the fields:
/// - [enabled]: A ticker is considered enabled only if all ancestor
/// [TickerMode.enabled] values and the local [TickerMode.enabled] are true
/// (logical AND). When false, tickers are muted (time still elapses but
/// callbacks are not invoked).
/// - [forceFrames]: When true, tickers in the subtree request frames using
/// [SchedulerBinding.scheduleForcedFrame] while active. This value is
/// combined across ancestors using logical OR, so any ancestor requesting
/// forced frames enables it for the subtree.
///
/// For most widgets, reading these values is unnecessary; mixins such as
/// [SingleTickerProviderStateMixin] and [TickerProviderStateMixin] apply them
/// automatically to the [Ticker]s they vend. Use this class when you need to
/// observe or react to ticker policy explicitly.
@immutable
class TickerModeData {
/// Creates a [TickerModeData].
const TickerModeData({required this.enabled, required this.forceFrames});
/// Fallback values used when there is no ancestor [TickerMode].
///
/// This corresponds to tickers being enabled and not forcing frames.
static const TickerModeData fallback = TickerModeData(enabled: true, forceFrames: false);
/// Whether tickers are enabled (not muted) for the subtree.
///
/// Effective value is the logical AND of all ancestor and local
/// [TickerMode.enabled] values.
final bool enabled;
/// Whether tickers should request forced frames while active.
///
/// Effective value is the logical OR of all ancestor and local
/// [TickerMode.forceFrames] values. Forcing frames may increase battery
/// usage, so use sparingly.
final bool forceFrames;
@override
bool operator ==(Object other) {
if (identical(other, this)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is TickerModeData && other.enabled == enabled && other.forceFrames == forceFrames;
}
@override
int get hashCode => Object.hash(enabled, forceFrames);
}
class _ConstantTickerModeDataListenable implements ValueListenable<TickerModeData> {
const _ConstantTickerModeDataListenable(this.value);
@override
void addListener(VoidCallback listener) {}
@override
void removeListener(VoidCallback listener) {}
@override
final TickerModeData value;
}

View File

@ -348,6 +348,9 @@ class FakeTicker implements Ticker {
@override
bool muted = false;
@override
bool forceFrames = false;
@override
void absorbTicker(Ticker originalTicker) {}

View File

@ -9,12 +9,24 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
test('scheduleForcedFrame sets up frame callbacks', () async {
test('scheduleForcedFrame sets up frame callbacks', () {
SchedulerBinding.instance.scheduleForcedFrame();
expect(SchedulerBinding.instance.platformDispatcher.onBeginFrame, isNotNull);
});
test('debugAssertNoTimeDilation does not throw if time dilate already reset', () async {
test('Ticker.forceFrames requests forced frames', () async {
final Ticker t = Ticker((_) {});
t.forceFrames = true;
final TickerFuture f = t.start();
addTearDown(() async {
t.stop();
await f;
});
// A forced frame should be scheduled even if frames are otherwise disabled.
expect(SchedulerBinding.instance.hasScheduledFrame, isTrue);
});
test('debugAssertNoTimeDilation does not throw if time dilate already reset', () {
timeDilation = 2.0;
timeDilation = 1.0;
SchedulerBinding.instance.debugAssertNoTimeDilation('reason'); // no error

View File

@ -2,6 +2,7 @@
// 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/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
@ -216,6 +217,302 @@ void main() {
expect(tickingState().ticker.isTicking, isFalse);
expect(tickingState().buildCount, 1);
});
testWidgets('TickerMode.forceFrames propagates to SingleTickerProviderStateMixin', (
WidgetTester tester,
) async {
Widget widgetUnderTest({required bool forceFrames}) {
return TickerMode(enabled: true, forceFrames: forceFrames, child: const _TickingWidget());
}
_TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: true));
expect(state().ticker.forceFrames, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
});
testWidgets('TickerMode.forceFrames propagates to TickerProviderStateMixin', (
WidgetTester tester,
) async {
Widget widgetUnderTest({required bool forceFrames}) {
return TickerMode(
enabled: true,
forceFrames: forceFrames,
child: const _MultiTickingWidget(),
);
}
_MultiTickingWidgetState state() =>
tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: true));
expect(state().ticker.forceFrames, isTrue);
expect(state().buildCount, 1);
await tester.pumpWidget(widgetUnderTest(forceFrames: false));
expect(state().ticker.forceFrames, isFalse);
expect(state().buildCount, 1);
});
testWidgets('Nested TickerMode.forceFrames uses OR semantics', (WidgetTester tester) async {
Widget nestedTickerModes({required bool innerForce, required bool outerForce}) {
return Directionality(
textDirection: TextDirection.rtl,
child: TickerMode(
enabled: true,
forceFrames: outerForce,
child: Row(
children: <Widget>[
const _TickingWidget(key: ValueKey<String>('outer')),
TickerMode(
enabled: true,
forceFrames: innerForce,
child: const _TickingWidget(key: ValueKey<String>('inner')),
),
],
),
),
);
}
_TickingWidgetState outerState() =>
tester.state<_TickingWidgetState>(find.byKey(const ValueKey<String>('outer')));
_TickingWidgetState innerState() =>
tester.state<_TickingWidgetState>(find.byKey(const ValueKey<String>('inner')));
// Both false -> both should not force frames
await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: false));
expect(outerState().ticker.forceFrames, isFalse);
expect(innerState().ticker.forceFrames, isFalse);
// Outer true -> both should force frames (OR semantics)
await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: false));
expect(outerState().ticker.forceFrames, isTrue);
expect(innerState().ticker.forceFrames, isTrue);
// Inner true -> only inner should force frames
await tester.pumpWidget(nestedTickerModes(outerForce: false, innerForce: true));
expect(outerState().ticker.forceFrames, isFalse);
expect(innerState().ticker.forceFrames, isTrue);
// Both true -> both should force frames
await tester.pumpWidget(nestedTickerModes(outerForce: true, innerForce: true));
expect(outerState().ticker.forceFrames, isTrue);
expect(innerState().ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.merge preserves ambient enabled and overrides forceFrames', (
WidgetTester tester,
) async {
await tester.pumpWidget(
TickerMode(
enabled: false,
child: TickerMode.merge(forceFrames: true, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled should be false (inherited from ancestor)
expect(state.ticker.muted, isTrue);
// forceFrames should be true (merged override)
expect(state.ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.merge respects AND semantics for enabled', (WidgetTester tester) async {
// Test that merge cannot override parent's enabled=false due to AND semantics
await tester.pumpWidget(
TickerMode(
enabled: false,
child: TickerMode.merge(enabled: true, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled uses AND semantics - child cannot re-enable when parent disables
expect(state.ticker.muted, isTrue);
// forceFrames should be false (inherited)
expect(state.ticker.forceFrames, isFalse);
});
testWidgets('TickerMode.merge can disable when parent is enabled', (WidgetTester tester) async {
// Test that merge can set enabled=false when parent is enabled=true
await tester.pumpWidget(
TickerMode(
enabled: true,
child: TickerMode.merge(enabled: false, child: const _TickingWidget()),
),
);
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled=false overrides parent's enabled=true (AND: true && false = false)
expect(state.ticker.muted, isTrue);
// forceFrames should be false (inherited)
expect(state.ticker.forceFrames, isFalse);
});
testWidgets('TickerMode.merge with no ancestor uses fallback values', (
WidgetTester tester,
) async {
await tester.pumpWidget(TickerMode.merge(forceFrames: true, child: const _TickingWidget()));
final _TickingWidgetState state = tester.state<_TickingWidgetState>(
find.byType(_TickingWidget),
);
// enabled should be true (fallback)
expect(state.ticker.muted, isFalse);
// forceFrames should be true (merged override)
expect(state.ticker.forceFrames, isTrue);
});
testWidgets('TickerMode.valuesOf returns correct values', (WidgetTester tester) async {
late TickerModeData capturedValues;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
capturedValues = TickerMode.valuesOf(context);
return Container();
},
),
),
);
expect(capturedValues.enabled, isFalse);
expect(capturedValues.forceFrames, isTrue);
});
testWidgets('TickerMode.valuesOf returns fallback when no ancestor', (WidgetTester tester) async {
late TickerModeData capturedValues;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
capturedValues = TickerMode.valuesOf(context);
return Container();
},
),
);
expect(capturedValues.enabled, isTrue);
expect(capturedValues.forceFrames, isFalse);
expect(capturedValues, equals(TickerModeData.fallback));
});
testWidgets('TickerMode.getValuesNotifier notifies listeners', (WidgetTester tester) async {
late ValueListenable<TickerModeData> notifier;
final List<TickerModeData> notifiedValues = <TickerModeData>[];
await tester.pumpWidget(
TickerMode(
enabled: true,
child: Builder(
builder: (BuildContext context) {
notifier = TickerMode.getValuesNotifier(context);
return Container();
},
),
),
);
notifier.addListener(() {
notifiedValues.add(notifier.value);
});
expect(notifier.value.enabled, isTrue);
expect(notifier.value.forceFrames, isFalse);
// Change forceFrames
await tester.pumpWidget(
TickerMode(
enabled: true,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
return Container();
},
),
),
);
expect(notifiedValues.length, 1);
expect(notifiedValues.last.enabled, isTrue);
expect(notifiedValues.last.forceFrames, isTrue);
});
test('TickerModeData equality works correctly', () {
const TickerModeData data1 = TickerModeData.fallback;
const TickerModeData data2 = TickerModeData.fallback;
const TickerModeData data3 = TickerModeData(enabled: false, forceFrames: false);
const TickerModeData data4 = TickerModeData(enabled: true, forceFrames: true);
expect(data1, equals(data2));
expect(data1, isNot(equals(data3)));
expect(data1, isNot(equals(data4)));
expect(data1.hashCode, equals(data2.hashCode));
expect(data1, equals(TickerModeData.fallback));
});
testWidgets('Deprecated TickerMode.of still works', (WidgetTester tester) async {
late bool capturedEnabled;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
// ignore: deprecated_member_use
capturedEnabled = TickerMode.of(context);
return Container();
},
),
),
);
expect(capturedEnabled, isFalse);
});
testWidgets('Deprecated TickerMode.getNotifier still works', (WidgetTester tester) async {
late ValueListenable<bool> notifier;
await tester.pumpWidget(
TickerMode(
enabled: false,
forceFrames: true,
child: Builder(
builder: (BuildContext context) {
// ignore: deprecated_member_use
notifier = TickerMode.getNotifier(context);
return Container();
},
),
),
);
expect(notifier.value, isFalse);
});
}
class _TickingWidget extends StatefulWidget {