mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
1c09ce6285
commit
4ef262721c
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -348,6 +348,9 @@ class FakeTicker implements Ticker {
|
||||
@override
|
||||
bool muted = false;
|
||||
|
||||
@override
|
||||
bool forceFrames = false;
|
||||
|
||||
@override
|
||||
void absorbTicker(Ticker originalTicker) {}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user