Do not rebuild when TickerMode changes (#93166)
This commit is contained in:
parent
f129fb0a92
commit
fe8e882a48
@ -1524,10 +1524,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
ScrollController? _internalScrollController;
|
||||
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
||||
|
||||
late final AnimationController _cursorBlinkOpacityController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _fadeDuration,
|
||||
)..addListener(_onCursorColorTick);
|
||||
AnimationController? _cursorBlinkOpacityController;
|
||||
|
||||
final LayerLink _toolbarLayerLink = LayerLink();
|
||||
final LayerLink _startHandleLayerLink = LayerLink();
|
||||
@ -1564,14 +1561,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// cursor position after the user has finished placing it.
|
||||
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
|
||||
|
||||
late final AnimationController _floatingCursorResetController = AnimationController(
|
||||
vsync: this,
|
||||
)..addListener(_onFloatingCursorResetTick);
|
||||
AnimationController? _floatingCursorResetController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.focusNode.hasFocus;
|
||||
|
||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||
|
||||
@override
|
||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
||||
@ -1698,6 +1693,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cursorBlinkOpacityController = AnimationController(
|
||||
vsync: this,
|
||||
duration: _fadeDuration,
|
||||
)..addListener(_onCursorColorTick);
|
||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||
widget.controller.addListener(_didChangeTextEditingValue);
|
||||
widget.focusNode.addListener(_handleFocusChanged);
|
||||
@ -1808,12 +1807,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_internalScrollController?.dispose();
|
||||
_currentAutofillScope?.unregister(autofillId);
|
||||
widget.controller.removeListener(_didChangeTextEditingValue);
|
||||
_floatingCursorResetController.dispose();
|
||||
_floatingCursorResetController?.dispose();
|
||||
_floatingCursorResetController = null;
|
||||
_closeInputConnectionIfNeeded();
|
||||
assert(!_hasInputConnection);
|
||||
_cursorTimer?.cancel();
|
||||
_cursorTimer = null;
|
||||
_cursorBlinkOpacityController.dispose();
|
||||
_cursorBlinkOpacityController?.dispose();
|
||||
_cursorBlinkOpacityController = null;
|
||||
_selectionOverlay?.dispose();
|
||||
_selectionOverlay = null;
|
||||
widget.focusNode.removeListener(_handleFocusChanged);
|
||||
@ -1952,10 +1953,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
@override
|
||||
void updateFloatingCursor(RawFloatingCursorPoint point) {
|
||||
_floatingCursorResetController ??= AnimationController(
|
||||
vsync: this,
|
||||
)..addListener(_onFloatingCursorResetTick);
|
||||
switch(point.state) {
|
||||
case FloatingCursorDragState.Start:
|
||||
if (_floatingCursorResetController.isAnimating) {
|
||||
_floatingCursorResetController.stop();
|
||||
if (_floatingCursorResetController!.isAnimating) {
|
||||
_floatingCursorResetController!.stop();
|
||||
_onFloatingCursorResetTick();
|
||||
}
|
||||
// We want to send in points that are centered around a (0,0) origin, so
|
||||
@ -1980,8 +1984,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
case FloatingCursorDragState.End:
|
||||
// We skip animation if no update has happened.
|
||||
if (_lastTextPosition != null && _lastBoundedOffset != null) {
|
||||
_floatingCursorResetController.value = 0.0;
|
||||
_floatingCursorResetController.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
|
||||
_floatingCursorResetController!.value = 0.0;
|
||||
_floatingCursorResetController!.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -1989,7 +1993,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
|
||||
void _onFloatingCursorResetTick() {
|
||||
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
||||
if (_floatingCursorResetController.isCompleted) {
|
||||
if (_floatingCursorResetController!.isCompleted) {
|
||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
||||
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
|
||||
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
|
||||
@ -1999,7 +2003,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_pointOffsetOrigin = null;
|
||||
_lastBoundedOffset = null;
|
||||
} else {
|
||||
final double lerpValue = _floatingCursorResetController.value;
|
||||
final double lerpValue = _floatingCursorResetController!.value;
|
||||
final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
|
||||
final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
|
||||
|
||||
@ -2528,14 +2532,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
|
||||
void _onCursorColorTick() {
|
||||
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
|
||||
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
|
||||
}
|
||||
|
||||
/// Whether the blinking cursor is actually visible at this precise moment
|
||||
/// (it's hidden half the time, since it blinks).
|
||||
@visibleForTesting
|
||||
bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
|
||||
bool get cursorCurrentlyVisible => _cursorBlinkOpacityController!.value > 0;
|
||||
|
||||
/// The cursor blink interval (the amount of time the cursor is in the "on"
|
||||
/// state or the "off" state). A complete cursor blink period is twice this
|
||||
@ -2561,9 +2565,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
//
|
||||
// These values and curves have been obtained through eyeballing, so are
|
||||
// likely not exactly the same as the values for native iOS.
|
||||
_cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
|
||||
_cursorBlinkOpacityController!.animateTo(targetOpacity, curve: Curves.easeOut);
|
||||
} else {
|
||||
_cursorBlinkOpacityController.value = targetOpacity;
|
||||
_cursorBlinkOpacityController!.value = targetOpacity;
|
||||
}
|
||||
|
||||
if (_obscureShowCharTicksPending > 0) {
|
||||
@ -2591,7 +2595,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
return;
|
||||
}
|
||||
_targetCursorVisibility = true;
|
||||
_cursorBlinkOpacityController.value = 1.0;
|
||||
_cursorBlinkOpacityController!.value = 1.0;
|
||||
if (EditableText.debugDeterministicCursor)
|
||||
return;
|
||||
if (widget.cursorOpacityAnimates) {
|
||||
@ -2606,14 +2610,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_cursorTimer?.cancel();
|
||||
_cursorTimer = null;
|
||||
_targetCursorVisibility = false;
|
||||
_cursorBlinkOpacityController.value = 0.0;
|
||||
_cursorBlinkOpacityController!.value = 0.0;
|
||||
if (EditableText.debugDeterministicCursor)
|
||||
return;
|
||||
if (resetCharTicks)
|
||||
_obscureShowCharTicksPending = 0;
|
||||
if (widget.cursorOpacityAnimates) {
|
||||
_cursorBlinkOpacityController.stop();
|
||||
_cursorBlinkOpacityController.value = 0.0;
|
||||
_cursorBlinkOpacityController!.stop();
|
||||
_cursorBlinkOpacityController!.value = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ export 'package:flutter/scheduler.dart' show TickerProvider;
|
||||
/// This only works if [AnimationController] objects are created using
|
||||
/// widget-aware ticker providers. For example, using a
|
||||
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
|
||||
class TickerMode extends StatelessWidget {
|
||||
class TickerMode extends StatefulWidget {
|
||||
/// Creates a widget that enables or disables tickers.
|
||||
///
|
||||
/// The [enabled] argument must not be null.
|
||||
@ -62,18 +62,85 @@ class TickerMode extends StatelessWidget {
|
||||
return widget?.enabled ?? true;
|
||||
}
|
||||
|
||||
/// Obtains a [ValueNotifier] from the [TickerMode] surrounding the `context`,
|
||||
/// which indicates whether tickers are enabled in the given subtree.
|
||||
///
|
||||
/// When that [TickerMode] enabled or disabled tickers, the notifier notifies
|
||||
/// its listeners.
|
||||
///
|
||||
/// While the [ValueNotifier] 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 changes from true to false or vice
|
||||
/// versa. This is preferable when the ticker mode does not impact what is
|
||||
/// currently rendered on screen, e.g. because it is ony 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 notifier,
|
||||
/// 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
|
||||
/// [ValueNotifier], whose [ValueNotifier.value] is always true.
|
||||
static ValueNotifier<bool> getNotifier(BuildContext context) {
|
||||
final _EffectiveTickerMode? widget = context.getElementForInheritedWidgetOfExactType<_EffectiveTickerMode>()?.widget as _EffectiveTickerMode?;
|
||||
return widget?.notifier ?? ValueNotifier<bool>(true);
|
||||
}
|
||||
|
||||
@override
|
||||
State<TickerMode> createState() => _TickerModeState();
|
||||
}
|
||||
|
||||
class _TickerModeState extends State<TickerMode> {
|
||||
bool _ancestorTicketMode = true;
|
||||
final ValueNotifier<bool> _effectiveMode = ValueNotifier<bool>(true);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_ancestorTicketMode = TickerMode.of(context);
|
||||
_updateEffectiveMode();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TickerMode oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateEffectiveMode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_effectiveMode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateEffectiveMode() {
|
||||
_effectiveMode.value = _ancestorTicketMode && widget.enabled;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _EffectiveTickerMode(
|
||||
enabled: enabled && TickerMode.of(context),
|
||||
child: child,
|
||||
enabled: _effectiveMode.value,
|
||||
notifier: _effectiveMode,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty('requested mode', value: enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
|
||||
properties.add(FlagProperty('requested mode', value: widget.enabled, ifTrue: 'enabled', ifFalse: 'disabled', showName: true));
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,11 +148,13 @@ class _EffectiveTickerMode extends InheritedWidget {
|
||||
const _EffectiveTickerMode({
|
||||
Key? key,
|
||||
required this.enabled,
|
||||
required this.notifier,
|
||||
required Widget child,
|
||||
}) : assert(enabled != null),
|
||||
super(key: key, child: child);
|
||||
super(key: key, child: child);
|
||||
|
||||
final bool enabled;
|
||||
final ValueNotifier<bool> notifier;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
|
||||
@ -127,10 +196,8 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
|
||||
]);
|
||||
}());
|
||||
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
|
||||
// We assume that this is called from initState, build, or some sort of
|
||||
// event handler, and that thus TickerMode.of(context) would return true. We
|
||||
// can't actually check that here because if we're in initState then we're
|
||||
// not allowed to do inheritance checks yet.
|
||||
_updateTickerModeNotifier();
|
||||
_updateTicker(); // Sets _ticker.mute correctly.
|
||||
return _ticker!;
|
||||
}
|
||||
|
||||
@ -154,14 +221,35 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
|
||||
_ticker!.describeForError('The offending ticker was'),
|
||||
]);
|
||||
}());
|
||||
_tickerModeNotifier?.removeListener(_updateTicker);
|
||||
_tickerModeNotifier = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ValueNotifier<bool>? _tickerModeNotifier;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
if (_ticker != null)
|
||||
_ticker!.muted = !TickerMode.of(context);
|
||||
super.didChangeDependencies();
|
||||
void activate() {
|
||||
super.activate();
|
||||
// We may have a new TickerMode ancestor.
|
||||
_updateTickerModeNotifier();
|
||||
_updateTicker();
|
||||
}
|
||||
|
||||
void _updateTicker() {
|
||||
if (_ticker != null) {
|
||||
_ticker!.muted = !_tickerModeNotifier!.value;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateTickerModeNotifier() {
|
||||
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
|
||||
if (newNotifier == _tickerModeNotifier) {
|
||||
return;
|
||||
}
|
||||
_tickerModeNotifier?.removeListener(_updateTicker);
|
||||
newNotifier.addListener(_updateTicker);
|
||||
_tickerModeNotifier = newNotifier;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -198,8 +286,14 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
||||
|
||||
@override
|
||||
Ticker createTicker(TickerCallback onTick) {
|
||||
if (_tickerModeNotifier == null) {
|
||||
// Setup TickerMode notifier before we vend the first ticker.
|
||||
_updateTickerModeNotifier();
|
||||
}
|
||||
assert(_tickerModeNotifier != null);
|
||||
_tickers ??= <_WidgetTicker>{};
|
||||
final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
|
||||
final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null)
|
||||
..muted = !_tickerModeNotifier!.value;
|
||||
_tickers!.add(result);
|
||||
return result;
|
||||
}
|
||||
@ -210,6 +304,35 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
||||
_tickers!.remove(ticker);
|
||||
}
|
||||
|
||||
ValueNotifier<bool>? _tickerModeNotifier;
|
||||
|
||||
@override
|
||||
void activate() {
|
||||
super.activate();
|
||||
// We may have a new TickerMode ancestor, get its Notifier.
|
||||
_updateTickerModeNotifier();
|
||||
_updateTickers();
|
||||
}
|
||||
|
||||
void _updateTickers() {
|
||||
if (_tickers != null) {
|
||||
final bool muted = !_tickerModeNotifier!.value;
|
||||
for (final Ticker ticker in _tickers!) {
|
||||
ticker.muted = muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateTickerModeNotifier() {
|
||||
final ValueNotifier<bool> newNotifier = TickerMode.getNotifier(context);
|
||||
if (newNotifier == _tickerModeNotifier) {
|
||||
return;
|
||||
}
|
||||
_tickerModeNotifier?.removeListener(_updateTickers);
|
||||
newNotifier.addListener(_updateTickers);
|
||||
_tickerModeNotifier = newNotifier;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
assert(() {
|
||||
@ -235,20 +358,11 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_tickerModeNotifier?.removeListener(_updateTickers);
|
||||
_tickerModeNotifier = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final bool muted = !TickerMode.of(context);
|
||||
if (_tickers != null) {
|
||||
for (final Ticker ticker in _tickers!) {
|
||||
ticker.muted = muted;
|
||||
}
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
|
@ -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/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -98,36 +99,194 @@ void main() {
|
||||
expect(outerTickCount, 0);
|
||||
expect(innerTickCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('Changing TickerMode does not rebuild widgets with SingleTickerProviderStateMixin', (WidgetTester tester) async {
|
||||
Widget widgetUnderTest({required bool tickerEnabled}) {
|
||||
return TickerMode(
|
||||
enabled: tickerEnabled,
|
||||
child: const _TickingWidget(),
|
||||
);
|
||||
}
|
||||
_TickingWidgetState state() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
|
||||
expect(state().ticker.isTicking, isTrue);
|
||||
expect(state().buildCount, 1);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
|
||||
expect(state().ticker.isTicking, isFalse);
|
||||
expect(state().buildCount, 1);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
|
||||
expect(state().ticker.isTicking, isTrue);
|
||||
expect(state().buildCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('Changing TickerMode does not rebuild widgets with TickerProviderStateMixin', (WidgetTester tester) async {
|
||||
Widget widgetUnderTest({required bool tickerEnabled}) {
|
||||
return TickerMode(
|
||||
enabled: tickerEnabled,
|
||||
child: const _MultiTickingWidget(),
|
||||
);
|
||||
}
|
||||
_MultiTickingWidgetState state() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
|
||||
expect(state().ticker.isTicking, isTrue);
|
||||
expect(state().buildCount, 1);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false));
|
||||
expect(state().ticker.isTicking, isFalse);
|
||||
expect(state().buildCount, 1);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true));
|
||||
expect(state().ticker.isTicking, isTrue);
|
||||
expect(state().buildCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('Moving widgets with SingleTickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
|
||||
final GlobalKey tickingWidgetKey = GlobalKey();
|
||||
Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
|
||||
return TickerMode(
|
||||
key: tickerModeKey,
|
||||
enabled: tickerEnabled,
|
||||
child: _TickingWidget(key: tickingWidgetKey),
|
||||
);
|
||||
}
|
||||
// Using different local keys to simulate changing TickerMode ancestors.
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
|
||||
final State tickerModeState = tester.state(find.byType(TickerMode));
|
||||
final _TickingWidgetState tickingState = tester.state<_TickingWidgetState>(find.byType(_TickingWidget));
|
||||
expect(tickingState.ticker.isTicking, isTrue);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
|
||||
expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
|
||||
expect(tickingState, same(tester.state<_TickingWidgetState>(find.byType(_TickingWidget))));
|
||||
expect(tickingState.ticker.isTicking, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Moving widgets with TickerProviderStateMixin to a new TickerMode ancestor works', (WidgetTester tester) async {
|
||||
final GlobalKey tickingWidgetKey = GlobalKey();
|
||||
Widget widgetUnderTest({required LocalKey tickerModeKey, required bool tickerEnabled}) {
|
||||
return TickerMode(
|
||||
key: tickerModeKey,
|
||||
enabled: tickerEnabled,
|
||||
child: _MultiTickingWidget(key: tickingWidgetKey),
|
||||
);
|
||||
}
|
||||
// Using different local keys to simulate changing TickerMode ancestors.
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: true, tickerModeKey: UniqueKey()));
|
||||
final State tickerModeState = tester.state(find.byType(TickerMode));
|
||||
final _MultiTickingWidgetState tickingState = tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget));
|
||||
expect(tickingState.ticker.isTicking, isTrue);
|
||||
|
||||
await tester.pumpWidget(widgetUnderTest(tickerEnabled: false, tickerModeKey: UniqueKey()));
|
||||
expect(tester.state(find.byType(TickerMode)), isNot(same(tickerModeState)));
|
||||
expect(tickingState, same(tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget))));
|
||||
expect(tickingState.ticker.isTicking, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Ticking widgets in old route do not rebuild when new route is pushed', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/foo' : (BuildContext context) => const Text('New route'),
|
||||
},
|
||||
home: Row(
|
||||
children: const <Widget>[
|
||||
_TickingWidget(),
|
||||
_MultiTickingWidget(),
|
||||
Text('Old route'),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
_MultiTickingWidgetState multiTickingState() => tester.state<_MultiTickingWidgetState>(find.byType(_MultiTickingWidget, skipOffstage: false));
|
||||
_TickingWidgetState tickingState() => tester.state<_TickingWidgetState>(find.byType(_TickingWidget, skipOffstage: false));
|
||||
|
||||
expect(find.text('Old route'), findsOneWidget);
|
||||
expect(find.text('New route'), findsNothing);
|
||||
|
||||
expect(multiTickingState().ticker.isTicking, isTrue);
|
||||
expect(multiTickingState().buildCount, 1);
|
||||
expect(tickingState().ticker.isTicking, isTrue);
|
||||
expect(tickingState().buildCount, 1);
|
||||
|
||||
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/foo');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Old route'), findsNothing);
|
||||
expect(find.text('New route'), findsOneWidget);
|
||||
|
||||
expect(multiTickingState().ticker.isTicking, isFalse);
|
||||
expect(multiTickingState().buildCount, 1);
|
||||
expect(tickingState().ticker.isTicking, isFalse);
|
||||
expect(tickingState().buildCount, 1);
|
||||
});
|
||||
}
|
||||
|
||||
class _TickingWidget extends StatefulWidget {
|
||||
const _TickingWidget({required this.onTick});
|
||||
const _TickingWidget({Key? key, this.onTick}) : super(key: key);
|
||||
|
||||
final VoidCallback onTick;
|
||||
final VoidCallback? onTick;
|
||||
|
||||
@override
|
||||
State<_TickingWidget> createState() => _TickingWidgetState();
|
||||
}
|
||||
|
||||
class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
|
||||
late Ticker _ticker;
|
||||
late Ticker ticker;
|
||||
int buildCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ticker = createTicker((Duration _) {
|
||||
widget.onTick();
|
||||
ticker = createTicker((Duration _) {
|
||||
widget.onTick?.call();
|
||||
})..start();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
buildCount += 1;
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
ticker.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _MultiTickingWidget extends StatefulWidget {
|
||||
const _MultiTickingWidget({Key? key, this.onTick}) : super(key: key);
|
||||
|
||||
final VoidCallback? onTick;
|
||||
|
||||
@override
|
||||
State<_MultiTickingWidget> createState() => _MultiTickingWidgetState();
|
||||
}
|
||||
|
||||
class _MultiTickingWidgetState extends State<_MultiTickingWidget> with TickerProviderStateMixin {
|
||||
late Ticker ticker;
|
||||
int buildCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ticker = createTicker((Duration _) {
|
||||
widget.onTick?.call();
|
||||
})..start();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
buildCount += 1;
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ticker.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user