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? _internalScrollController;
|
||||||
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
||||||
|
|
||||||
late final AnimationController _cursorBlinkOpacityController = AnimationController(
|
AnimationController? _cursorBlinkOpacityController;
|
||||||
vsync: this,
|
|
||||||
duration: _fadeDuration,
|
|
||||||
)..addListener(_onCursorColorTick);
|
|
||||||
|
|
||||||
final LayerLink _toolbarLayerLink = LayerLink();
|
final LayerLink _toolbarLayerLink = LayerLink();
|
||||||
final LayerLink _startHandleLayerLink = 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.
|
// cursor position after the user has finished placing it.
|
||||||
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
|
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
|
||||||
|
|
||||||
late final AnimationController _floatingCursorResetController = AnimationController(
|
AnimationController? _floatingCursorResetController;
|
||||||
vsync: this,
|
|
||||||
)..addListener(_onFloatingCursorResetTick);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => widget.focusNode.hasFocus;
|
bool get wantKeepAlive => widget.focusNode.hasFocus;
|
||||||
|
|
||||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
|
||||||
@ -1698,6 +1693,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_cursorBlinkOpacityController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: _fadeDuration,
|
||||||
|
)..addListener(_onCursorColorTick);
|
||||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
@ -1808,12 +1807,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_internalScrollController?.dispose();
|
_internalScrollController?.dispose();
|
||||||
_currentAutofillScope?.unregister(autofillId);
|
_currentAutofillScope?.unregister(autofillId);
|
||||||
widget.controller.removeListener(_didChangeTextEditingValue);
|
widget.controller.removeListener(_didChangeTextEditingValue);
|
||||||
_floatingCursorResetController.dispose();
|
_floatingCursorResetController?.dispose();
|
||||||
|
_floatingCursorResetController = null;
|
||||||
_closeInputConnectionIfNeeded();
|
_closeInputConnectionIfNeeded();
|
||||||
assert(!_hasInputConnection);
|
assert(!_hasInputConnection);
|
||||||
_cursorTimer?.cancel();
|
_cursorTimer?.cancel();
|
||||||
_cursorTimer = null;
|
_cursorTimer = null;
|
||||||
_cursorBlinkOpacityController.dispose();
|
_cursorBlinkOpacityController?.dispose();
|
||||||
|
_cursorBlinkOpacityController = null;
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay?.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
widget.focusNode.removeListener(_handleFocusChanged);
|
widget.focusNode.removeListener(_handleFocusChanged);
|
||||||
@ -1952,10 +1953,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void updateFloatingCursor(RawFloatingCursorPoint point) {
|
void updateFloatingCursor(RawFloatingCursorPoint point) {
|
||||||
|
_floatingCursorResetController ??= AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
)..addListener(_onFloatingCursorResetTick);
|
||||||
switch(point.state) {
|
switch(point.state) {
|
||||||
case FloatingCursorDragState.Start:
|
case FloatingCursorDragState.Start:
|
||||||
if (_floatingCursorResetController.isAnimating) {
|
if (_floatingCursorResetController!.isAnimating) {
|
||||||
_floatingCursorResetController.stop();
|
_floatingCursorResetController!.stop();
|
||||||
_onFloatingCursorResetTick();
|
_onFloatingCursorResetTick();
|
||||||
}
|
}
|
||||||
// We want to send in points that are centered around a (0,0) origin, so
|
// 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:
|
case FloatingCursorDragState.End:
|
||||||
// We skip animation if no update has happened.
|
// We skip animation if no update has happened.
|
||||||
if (_lastTextPosition != null && _lastBoundedOffset != null) {
|
if (_lastTextPosition != null && _lastBoundedOffset != null) {
|
||||||
_floatingCursorResetController.value = 0.0;
|
_floatingCursorResetController!.value = 0.0;
|
||||||
_floatingCursorResetController.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
|
_floatingCursorResetController!.animateTo(1.0, duration: _floatingCursorResetTime, curve: Curves.decelerate);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1989,7 +1993,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
void _onFloatingCursorResetTick() {
|
void _onFloatingCursorResetTick() {
|
||||||
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
||||||
if (_floatingCursorResetController.isCompleted) {
|
if (_floatingCursorResetController!.isCompleted) {
|
||||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
||||||
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
|
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.
|
// 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;
|
_pointOffsetOrigin = null;
|
||||||
_lastBoundedOffset = null;
|
_lastBoundedOffset = null;
|
||||||
} else {
|
} else {
|
||||||
final double lerpValue = _floatingCursorResetController.value;
|
final double lerpValue = _floatingCursorResetController!.value;
|
||||||
final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
|
final double lerpX = ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
|
||||||
final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
|
final double lerpY = ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
|
||||||
|
|
||||||
@ -2528,14 +2532,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onCursorColorTick() {
|
void _onCursorColorTick() {
|
||||||
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
||||||
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
|
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the blinking cursor is actually visible at this precise moment
|
/// Whether the blinking cursor is actually visible at this precise moment
|
||||||
/// (it's hidden half the time, since it blinks).
|
/// (it's hidden half the time, since it blinks).
|
||||||
@visibleForTesting
|
@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"
|
/// 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
|
/// 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
|
// These values and curves have been obtained through eyeballing, so are
|
||||||
// likely not exactly the same as the values for native iOS.
|
// likely not exactly the same as the values for native iOS.
|
||||||
_cursorBlinkOpacityController.animateTo(targetOpacity, curve: Curves.easeOut);
|
_cursorBlinkOpacityController!.animateTo(targetOpacity, curve: Curves.easeOut);
|
||||||
} else {
|
} else {
|
||||||
_cursorBlinkOpacityController.value = targetOpacity;
|
_cursorBlinkOpacityController!.value = targetOpacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_obscureShowCharTicksPending > 0) {
|
if (_obscureShowCharTicksPending > 0) {
|
||||||
@ -2591,7 +2595,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_targetCursorVisibility = true;
|
_targetCursorVisibility = true;
|
||||||
_cursorBlinkOpacityController.value = 1.0;
|
_cursorBlinkOpacityController!.value = 1.0;
|
||||||
if (EditableText.debugDeterministicCursor)
|
if (EditableText.debugDeterministicCursor)
|
||||||
return;
|
return;
|
||||||
if (widget.cursorOpacityAnimates) {
|
if (widget.cursorOpacityAnimates) {
|
||||||
@ -2606,14 +2610,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_cursorTimer?.cancel();
|
_cursorTimer?.cancel();
|
||||||
_cursorTimer = null;
|
_cursorTimer = null;
|
||||||
_targetCursorVisibility = false;
|
_targetCursorVisibility = false;
|
||||||
_cursorBlinkOpacityController.value = 0.0;
|
_cursorBlinkOpacityController!.value = 0.0;
|
||||||
if (EditableText.debugDeterministicCursor)
|
if (EditableText.debugDeterministicCursor)
|
||||||
return;
|
return;
|
||||||
if (resetCharTicks)
|
if (resetCharTicks)
|
||||||
_obscureShowCharTicksPending = 0;
|
_obscureShowCharTicksPending = 0;
|
||||||
if (widget.cursorOpacityAnimates) {
|
if (widget.cursorOpacityAnimates) {
|
||||||
_cursorBlinkOpacityController.stop();
|
_cursorBlinkOpacityController!.stop();
|
||||||
_cursorBlinkOpacityController.value = 0.0;
|
_cursorBlinkOpacityController!.value = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export 'package:flutter/scheduler.dart' show TickerProvider;
|
|||||||
/// This only works if [AnimationController] objects are created using
|
/// This only works if [AnimationController] objects are created using
|
||||||
/// widget-aware ticker providers. For example, using a
|
/// widget-aware ticker providers. For example, using a
|
||||||
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
|
/// [TickerProviderStateMixin] or a [SingleTickerProviderStateMixin].
|
||||||
class TickerMode extends StatelessWidget {
|
class TickerMode extends StatefulWidget {
|
||||||
/// Creates a widget that enables or disables tickers.
|
/// Creates a widget that enables or disables tickers.
|
||||||
///
|
///
|
||||||
/// The [enabled] argument must not be null.
|
/// The [enabled] argument must not be null.
|
||||||
@ -62,18 +62,85 @@ class TickerMode extends StatelessWidget {
|
|||||||
return widget?.enabled ?? true;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _EffectiveTickerMode(
|
return _EffectiveTickerMode(
|
||||||
enabled: enabled && TickerMode.of(context),
|
enabled: _effectiveMode.value,
|
||||||
child: child,
|
notifier: _effectiveMode,
|
||||||
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(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({
|
const _EffectiveTickerMode({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
|
required this.notifier,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) : assert(enabled != null),
|
}) : assert(enabled != null),
|
||||||
super(key: key, child: child);
|
super(key: key, child: child);
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
final ValueNotifier<bool> notifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(_EffectiveTickerMode oldWidget) => enabled != oldWidget.enabled;
|
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);
|
_ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by ${describeIdentity(this)}' : null);
|
||||||
// We assume that this is called from initState, build, or some sort of
|
_updateTickerModeNotifier();
|
||||||
// event handler, and that thus TickerMode.of(context) would return true. We
|
_updateTicker(); // Sets _ticker.mute correctly.
|
||||||
// can't actually check that here because if we're in initState then we're
|
|
||||||
// not allowed to do inheritance checks yet.
|
|
||||||
return _ticker!;
|
return _ticker!;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,14 +221,35 @@ mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> imple
|
|||||||
_ticker!.describeForError('The offending ticker was'),
|
_ticker!.describeForError('The offending ticker was'),
|
||||||
]);
|
]);
|
||||||
}());
|
}());
|
||||||
|
_tickerModeNotifier?.removeListener(_updateTicker);
|
||||||
|
_tickerModeNotifier = null;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValueNotifier<bool>? _tickerModeNotifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void activate() {
|
||||||
if (_ticker != null)
|
super.activate();
|
||||||
_ticker!.muted = !TickerMode.of(context);
|
// We may have a new TickerMode ancestor.
|
||||||
super.didChangeDependencies();
|
_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
|
@override
|
||||||
@ -198,8 +286,14 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Ticker createTicker(TickerCallback onTick) {
|
Ticker createTicker(TickerCallback onTick) {
|
||||||
|
if (_tickerModeNotifier == null) {
|
||||||
|
// Setup TickerMode notifier before we vend the first ticker.
|
||||||
|
_updateTickerModeNotifier();
|
||||||
|
}
|
||||||
|
assert(_tickerModeNotifier != null);
|
||||||
_tickers ??= <_WidgetTicker>{};
|
_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);
|
_tickers!.add(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -210,6 +304,35 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
|||||||
_tickers!.remove(ticker);
|
_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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -235,20 +358,11 @@ mixin TickerProviderStateMixin<T extends StatefulWidget> on State<T> implements
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
|
_tickerModeNotifier?.removeListener(_updateTickers);
|
||||||
|
_tickerModeNotifier = null;
|
||||||
super.dispose();
|
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
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -98,36 +99,194 @@ void main() {
|
|||||||
expect(outerTickCount, 0);
|
expect(outerTickCount, 0);
|
||||||
expect(innerTickCount, 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 {
|
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
|
@override
|
||||||
State<_TickingWidget> createState() => _TickingWidgetState();
|
State<_TickingWidget> createState() => _TickingWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
|
class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
|
||||||
late Ticker _ticker;
|
late Ticker ticker;
|
||||||
|
int buildCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_ticker = createTicker((Duration _) {
|
ticker = createTicker((Duration _) {
|
||||||
widget.onTick();
|
widget.onTick?.call();
|
||||||
})..start();
|
})..start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
buildCount += 1;
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
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();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user