1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | /// @docImport 'package:flutter/cupertino.dart'; |
6 | /// @docImport 'package:flutter/material.dart'; |
7 | library; |
8 | |
9 | import 'package:flutter/foundation.dart'; |
10 | import 'package:flutter/rendering.dart'; |
11 | |
12 | import 'actions.dart'; |
13 | import 'basic.dart'; |
14 | import 'focus_manager.dart'; |
15 | import 'framework.dart'; |
16 | import 'gesture_detector.dart'; |
17 | import 'ticker_provider.dart'; |
18 | import 'widget_state.dart'; |
19 | |
20 | // Duration of the animation that moves the toggle from one state to another. |
21 | const Duration _kToggleDuration = Duration(milliseconds: 200); |
22 | |
23 | // Duration of the fade animation for the reaction when focus and hover occur. |
24 | const Duration _kReactionFadeDuration = Duration(milliseconds: 50); |
25 | |
26 | /// A mixin for [StatefulWidget]s that implement toggleable |
27 | /// controls with toggle animations (e.g. [Switch]es, [CupertinoSwitch]es, |
28 | /// [Checkbox]es, [CupertinoCheckbox]es, [Radio]s, and [CupertinoRadio]s). |
29 | /// |
30 | /// The mixin implements the logic for toggling the control (e.g. when tapped) |
31 | /// and provides a series of animation controllers to transition the control |
32 | /// from one state to another. It does not have any opinion about the visual |
33 | /// representation of the toggleable widget. The visuals are defined by a |
34 | /// [CustomPainter] passed to the [buildToggleable]. [State] objects using this |
35 | /// mixin should call that method from their [build] method. |
36 | @optionalTypeArgs |
37 | mixin ToggleableStateMixin<S extends StatefulWidget> on TickerProviderStateMixin<S> { |
38 | /// Used by subclasses to manipulate the visual value of the control. |
39 | /// |
40 | /// Some controls respond to user input by updating their visual value. For |
41 | /// example, the thumb of a switch moves from one position to another when |
42 | /// dragged. These controls manipulate this animation controller to update |
43 | /// their [position] and eventually trigger an [onChanged] callback when the |
44 | /// animation reaches either 0.0 or 1.0. |
45 | AnimationController get positionController => _positionController; |
46 | late AnimationController _positionController; |
47 | |
48 | /// The visual value of the control. |
49 | /// |
50 | /// When the control is inactive, the [value] is false and this animation has |
51 | /// the value 0.0. When the control is active, the value is either true or |
52 | /// tristate is true and the value is null. When the control is active the |
53 | /// animation has a value of 1.0. When the control is changing from inactive |
54 | /// to active (or vice versa), [value] is the target value and this animation |
55 | /// gradually updates from 0.0 to 1.0 (or vice versa). |
56 | CurvedAnimation get position => _position; |
57 | late CurvedAnimation _position; |
58 | |
59 | /// Used by subclasses to control the radial reaction animation. |
60 | /// |
61 | /// Some controls have a radial ink reaction to user input. This animation |
62 | /// controller can be used to start or stop these ink reactions. |
63 | /// |
64 | /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] |
65 | /// may be used. |
66 | AnimationController get reactionController => _reactionController; |
67 | late AnimationController _reactionController; |
68 | |
69 | /// The visual value of the radial reaction animation. |
70 | /// |
71 | /// Some controls have a radial ink reaction to user input. This animation |
72 | /// controls the progress of these ink reactions. |
73 | /// |
74 | /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] |
75 | /// may be used. |
76 | CurvedAnimation get reaction => _reaction; |
77 | late CurvedAnimation _reaction; |
78 | |
79 | /// Controls the radial reaction's opacity animation for hover changes. |
80 | /// |
81 | /// Some controls have a radial ink reaction to pointer hover. This animation |
82 | /// controls these ink reaction fade-ins and |
83 | /// fade-outs. |
84 | /// |
85 | /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] |
86 | /// may be used. |
87 | CurvedAnimation get reactionHoverFade => _reactionHoverFade; |
88 | late CurvedAnimation _reactionHoverFade; |
89 | late AnimationController _reactionHoverFadeController; |
90 | |
91 | /// Controls the radial reaction's opacity animation for focus changes. |
92 | /// |
93 | /// Some controls have a radial ink reaction to focus. This animation |
94 | /// controls these ink reaction fade-ins and fade-outs. |
95 | /// |
96 | /// To paint the actual radial reaction, [ToggleablePainter.paintRadialReaction] |
97 | /// may be used. |
98 | CurvedAnimation get reactionFocusFade => _reactionFocusFade; |
99 | late CurvedAnimation _reactionFocusFade; |
100 | late AnimationController _reactionFocusFadeController; |
101 | |
102 | /// The amount of time a circular ink response should take to expand to its |
103 | /// full size if a radial reaction is drawn using |
104 | /// [ToggleablePainter.paintRadialReaction]. |
105 | Duration? get reactionAnimationDuration => _reactionAnimationDuration; |
106 | final Duration _reactionAnimationDuration = const Duration(milliseconds: 100); |
107 | |
108 | /// Whether [value] of this control can be changed by user interaction. |
109 | /// |
110 | /// The control is considered interactive if the [onChanged] callback is |
111 | /// non-null. If the callback is null, then the control is disabled, and |
112 | /// non-interactive. A disabled checkbox, for example, is displayed using a |
113 | /// grey color and its value cannot be changed. |
114 | bool get isInteractive => onChanged != null; |
115 | |
116 | late final Map<Type, Action<Intent>> _actionMap = <Type, Action<Intent>>{ |
117 | ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleTap), |
118 | }; |
119 | |
120 | /// Called when the control changes value. |
121 | /// |
122 | /// If the control is tapped, [onChanged] is called immediately with the new |
123 | /// value. |
124 | /// |
125 | /// The control is considered interactive (see [isInteractive]) if this |
126 | /// callback is non-null. If the callback is null, then the control is |
127 | /// disabled, and non-interactive. A disabled checkbox, for example, is |
128 | /// displayed using a grey color and its value cannot be changed. |
129 | ValueChanged<bool?>? get onChanged; |
130 | |
131 | /// False if this control is "inactive" (not checked, off, or unselected). |
132 | /// |
133 | /// If value is true then the control "active" (checked, on, or selected). If |
134 | /// tristate is true and value is null, then the control is considered to be |
135 | /// in its third or "indeterminate" state. |
136 | /// |
137 | /// When the value changes, this object starts the [positionController] and |
138 | /// [position] animations to animate the visual appearance of the control to |
139 | /// the new value. |
140 | bool? get value; |
141 | |
142 | /// If true, [value] can be true, false, or null, otherwise [value] must |
143 | /// be true or false. |
144 | /// |
145 | /// When [tristate] is true and [value] is null, then the control is |
146 | /// considered to be in its third or "indeterminate" state. |
147 | bool get tristate; |
148 | |
149 | @override |
150 | void initState() { |
151 | super.initState(); |
152 | _positionController = AnimationController( |
153 | duration: _kToggleDuration, |
154 | value: value == false ? 0.0 : 1.0, |
155 | vsync: this, |
156 | ); |
157 | _position = CurvedAnimation( |
158 | parent: _positionController, |
159 | curve: Curves.easeIn, |
160 | reverseCurve: Curves.easeOut, |
161 | ); |
162 | _reactionController = AnimationController( |
163 | duration: _reactionAnimationDuration, |
164 | vsync: this, |
165 | ); |
166 | _reaction = CurvedAnimation( |
167 | parent: _reactionController, |
168 | curve: Curves.fastOutSlowIn, |
169 | ); |
170 | _reactionHoverFadeController = AnimationController( |
171 | duration: _kReactionFadeDuration, |
172 | value: _hovering || _focused ? 1.0 : 0.0, |
173 | vsync: this, |
174 | ); |
175 | _reactionHoverFade = CurvedAnimation( |
176 | parent: _reactionHoverFadeController, |
177 | curve: Curves.fastOutSlowIn, |
178 | ); |
179 | _reactionFocusFadeController = AnimationController( |
180 | duration: _kReactionFadeDuration, |
181 | value: _hovering || _focused ? 1.0 : 0.0, |
182 | vsync: this, |
183 | ); |
184 | _reactionFocusFade = CurvedAnimation( |
185 | parent: _reactionFocusFadeController, |
186 | curve: Curves.fastOutSlowIn, |
187 | ); |
188 | } |
189 | |
190 | /// Runs the [position] animation to transition the Toggleable's appearance |
191 | /// to match [value]. |
192 | /// |
193 | /// This method must be called whenever [value] changes to ensure that the |
194 | /// visual representation of the Toggleable matches the current [value]. |
195 | void animateToValue() { |
196 | if (tristate) { |
197 | if (value == null) { |
198 | _positionController.value = 0.0; |
199 | } |
200 | if (value ?? true) { |
201 | _positionController.forward(); |
202 | } else { |
203 | _positionController.reverse(); |
204 | } |
205 | } else { |
206 | if (value ?? false) { |
207 | _positionController.forward(); |
208 | } else { |
209 | _positionController.reverse(); |
210 | } |
211 | } |
212 | } |
213 | |
214 | @override |
215 | void dispose() { |
216 | _positionController.dispose(); |
217 | _position.dispose(); |
218 | _reactionController.dispose(); |
219 | _reaction.dispose(); |
220 | _reactionHoverFadeController.dispose(); |
221 | _reactionHoverFade.dispose(); |
222 | _reactionFocusFadeController.dispose(); |
223 | _reactionFocusFade.dispose(); |
224 | super.dispose(); |
225 | } |
226 | |
227 | /// The most recent [Offset] at which a pointer touched the Toggleable. |
228 | /// |
229 | /// This is null if currently no pointer is touching the Toggleable or if |
230 | /// [isInteractive] is false. |
231 | Offset? get downPosition => _downPosition; |
232 | Offset? _downPosition; |
233 | |
234 | void _handleTapDown(TapDownDetails details) { |
235 | if (isInteractive) { |
236 | setState(() { |
237 | _downPosition = details.localPosition; |
238 | }); |
239 | _reactionController.forward(); |
240 | } |
241 | } |
242 | |
243 | void _handleTap([Intent? _]) { |
244 | if (!isInteractive) { |
245 | return; |
246 | } |
247 | switch (value) { |
248 | case false: |
249 | onChanged!(true); |
250 | case true: |
251 | onChanged!(tristate ? null : false); |
252 | case null: |
253 | onChanged!(false); |
254 | } |
255 | context.findRenderObject()!.sendSemanticsEvent(const TapSemanticEvent()); |
256 | } |
257 | |
258 | void _handleTapEnd([TapUpDetails? _]) { |
259 | if (_downPosition != null) { |
260 | setState(() { _downPosition = null; }); |
261 | } |
262 | _reactionController.reverse(); |
263 | } |
264 | |
265 | bool _focused = false; |
266 | void _handleFocusHighlightChanged(bool focused) { |
267 | if (focused != _focused) { |
268 | setState(() { _focused = focused; }); |
269 | if (focused) { |
270 | _reactionFocusFadeController.forward(); |
271 | } else { |
272 | _reactionFocusFadeController.reverse(); |
273 | } |
274 | } |
275 | } |
276 | |
277 | bool _hovering = false; |
278 | void _handleHoverChanged(bool hovering) { |
279 | if (hovering != _hovering) { |
280 | setState(() { _hovering = hovering; }); |
281 | if (hovering) { |
282 | _reactionHoverFadeController.forward(); |
283 | } else { |
284 | _reactionHoverFadeController.reverse(); |
285 | } |
286 | } |
287 | } |
288 | |
289 | /// Describes the current [WidgetState] of the Toggleable. |
290 | /// |
291 | /// The returned set will include: |
292 | /// |
293 | /// * [WidgetState.disabled], if [isInteractive] is false |
294 | /// * [WidgetState.hovered], if a pointer is hovering over the Toggleable |
295 | /// * [WidgetState.focused], if the Toggleable has input focus |
296 | /// * [WidgetState.selected], if [value] is true or null |
297 | Set<WidgetState> get states => <WidgetState>{ |
298 | if (!isInteractive) WidgetState.disabled, |
299 | if (_hovering) WidgetState.hovered, |
300 | if (_focused) WidgetState.focused, |
301 | if (value ?? true) WidgetState.selected, |
302 | }; |
303 | |
304 | /// Typically wraps a `painter` that draws the actual visuals of the |
305 | /// Toggleable with logic to toggle it. |
306 | /// |
307 | /// If drawing a radial ink reaction is desired (in Material Design for |
308 | /// example), consider providing a subclass of [ToggleablePainter] as a |
309 | /// `painter`, which implements logic to draw a radial ink reaction for this |
310 | /// control. The painter is usually configured with the [reaction], |
311 | /// [position], [reactionHoverFade], and [reactionFocusFade] animation |
312 | /// provided by this mixin. It is expected to draw the visuals of the |
313 | /// Toggleable based on the current value of these animations. The animations |
314 | /// are triggered by this mixin to transition the Toggleable from one state |
315 | /// to another. |
316 | /// |
317 | /// Material Toggleables must provide a [mouseCursor] which resolves to a |
318 | /// [MouseCursor] based on the current [WidgetState] of the Toggleable. |
319 | /// Cupertino Toggleables may not provide a [mouseCursor]. If no [mouseCursor] |
320 | /// is provided, [SystemMouseCursors.basic] will be used as the [mouseCursor] |
321 | /// across all [WidgetState]s. |
322 | /// |
323 | /// This method must be called from the [build] method of the [State] class |
324 | /// that uses this mixin. The returned [Widget] must be returned from the |
325 | /// build method - potentially after wrapping it in other widgets. |
326 | Widget buildToggleable({ |
327 | FocusNode? focusNode, |
328 | ValueChanged<bool>? onFocusChange, |
329 | bool autofocus = false, |
330 | WidgetStateProperty<MouseCursor>? mouseCursor, |
331 | required Size size, |
332 | required CustomPainter painter, |
333 | }) { |
334 | return FocusableActionDetector( |
335 | actions: _actionMap, |
336 | focusNode: focusNode, |
337 | autofocus: autofocus, |
338 | onFocusChange: onFocusChange, |
339 | enabled: isInteractive, |
340 | onShowFocusHighlight: _handleFocusHighlightChanged, |
341 | onShowHoverHighlight: _handleHoverChanged, |
342 | mouseCursor: mouseCursor?.resolve(states) ?? SystemMouseCursors.basic, |
343 | child: GestureDetector( |
344 | excludeFromSemantics: !isInteractive, |
345 | onTapDown: isInteractive ? _handleTapDown : null, |
346 | onTap: isInteractive ? _handleTap : null, |
347 | onTapUp: isInteractive ? _handleTapEnd : null, |
348 | onTapCancel: isInteractive ? _handleTapEnd : null, |
349 | child: Semantics( |
350 | enabled: isInteractive, |
351 | child: CustomPaint( |
352 | size: size, |
353 | painter: painter, |
354 | ), |
355 | ), |
356 | ), |
357 | ); |
358 | } |
359 | } |
360 | |
361 | /// A base class for a [CustomPainter] that may be passed to |
362 | /// [ToggleableStateMixin.buildToggleable] to draw the visual representation of |
363 | /// a Toggleable. |
364 | /// |
365 | /// Subclasses must implement the [paint] method to draw the actual visuals of |
366 | /// the Toggleable. |
367 | /// |
368 | /// If drawing a radial ink reaction is desired (in Material |
369 | /// Design for example), subclasses may call [paintRadialReaction] in their |
370 | /// [paint] method. |
371 | abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter { |
372 | /// The visual value of the control. |
373 | /// |
374 | /// Usually set to [ToggleableStateMixin.position]. |
375 | Animation<double> get position => _position!; |
376 | Animation<double>? _position; |
377 | set position(Animation<double> value) { |
378 | if (value == _position) { |
379 | return; |
380 | } |
381 | _position?.removeListener(notifyListeners); |
382 | value.addListener(notifyListeners); |
383 | _position = value; |
384 | notifyListeners(); |
385 | } |
386 | |
387 | /// The visual value of the radial reaction animation. |
388 | /// |
389 | /// Usually set to [ToggleableStateMixin.reaction]. |
390 | Animation<double> get reaction => _reaction!; |
391 | Animation<double>? _reaction; |
392 | set reaction(Animation<double> value) { |
393 | if (value == _reaction) { |
394 | return; |
395 | } |
396 | _reaction?.removeListener(notifyListeners); |
397 | value.addListener(notifyListeners); |
398 | _reaction = value; |
399 | notifyListeners(); |
400 | } |
401 | |
402 | /// Controls the radial reaction's opacity animation for focus changes. |
403 | /// |
404 | /// Usually set to [ToggleableStateMixin.reactionFocusFade]. |
405 | Animation<double> get reactionFocusFade => _reactionFocusFade!; |
406 | Animation<double>? _reactionFocusFade; |
407 | set reactionFocusFade(Animation<double> value) { |
408 | if (value == _reactionFocusFade) { |
409 | return; |
410 | } |
411 | _reactionFocusFade?.removeListener(notifyListeners); |
412 | value.addListener(notifyListeners); |
413 | _reactionFocusFade = value; |
414 | notifyListeners(); |
415 | } |
416 | |
417 | /// Controls the radial reaction's opacity animation for hover changes. |
418 | /// |
419 | /// Usually set to [ToggleableStateMixin.reactionHoverFade]. |
420 | Animation<double> get reactionHoverFade => _reactionHoverFade!; |
421 | Animation<double>? _reactionHoverFade; |
422 | set reactionHoverFade(Animation<double> value) { |
423 | if (value == _reactionHoverFade) { |
424 | return; |
425 | } |
426 | _reactionHoverFade?.removeListener(notifyListeners); |
427 | value.addListener(notifyListeners); |
428 | _reactionHoverFade = value; |
429 | notifyListeners(); |
430 | } |
431 | |
432 | /// The color that should be used in the active state (i.e., when |
433 | /// [ToggleableStateMixin.value] is true). |
434 | /// |
435 | /// For example, a checkbox should use this color when checked. |
436 | Color get activeColor => _activeColor!; |
437 | Color? _activeColor; |
438 | set activeColor(Color value) { |
439 | if (_activeColor == value) { |
440 | return; |
441 | } |
442 | _activeColor = value; |
443 | notifyListeners(); |
444 | } |
445 | |
446 | /// The color that should be used in the inactive state (i.e., when |
447 | /// [ToggleableStateMixin.value] is false). |
448 | /// |
449 | /// For example, a checkbox should use this color when unchecked. |
450 | Color get inactiveColor => _inactiveColor!; |
451 | Color? _inactiveColor; |
452 | set inactiveColor(Color value) { |
453 | if (_inactiveColor == value) { |
454 | return; |
455 | } |
456 | _inactiveColor = value; |
457 | notifyListeners(); |
458 | } |
459 | |
460 | /// The color that should be used for the reaction when the toggleable is |
461 | /// inactive. |
462 | /// |
463 | /// Used when the toggleable needs to change the reaction color/transparency |
464 | /// that is displayed when the toggleable is inactive and tapped. |
465 | Color get inactiveReactionColor => _inactiveReactionColor!; |
466 | Color? _inactiveReactionColor; |
467 | set inactiveReactionColor(Color value) { |
468 | if (value == _inactiveReactionColor) { |
469 | return; |
470 | } |
471 | _inactiveReactionColor = value; |
472 | notifyListeners(); |
473 | } |
474 | |
475 | /// The color that should be used for the reaction when the toggleable is |
476 | /// active. |
477 | /// |
478 | /// Used when the toggleable needs to change the reaction color/transparency |
479 | /// that is displayed when the toggleable is active and tapped. |
480 | Color get reactionColor => _reactionColor!; |
481 | Color? _reactionColor; |
482 | set reactionColor(Color value) { |
483 | if (value == _reactionColor) { |
484 | return; |
485 | } |
486 | _reactionColor = value; |
487 | notifyListeners(); |
488 | } |
489 | |
490 | /// The color that should be used for the reaction when [isHovered] is true. |
491 | /// |
492 | /// Used when the toggleable needs to change the reaction color/transparency, |
493 | /// when it is being hovered over. |
494 | Color get hoverColor => _hoverColor!; |
495 | Color? _hoverColor; |
496 | set hoverColor(Color value) { |
497 | if (value == _hoverColor) { |
498 | return; |
499 | } |
500 | _hoverColor = value; |
501 | notifyListeners(); |
502 | } |
503 | |
504 | /// The color that should be used for the reaction when [isFocused] is true. |
505 | /// |
506 | /// Used when the toggleable needs to change the reaction color/transparency, |
507 | /// when it has focus. |
508 | Color get focusColor => _focusColor!; |
509 | Color? _focusColor; |
510 | set focusColor(Color value) { |
511 | if (value == _focusColor) { |
512 | return; |
513 | } |
514 | _focusColor = value; |
515 | notifyListeners(); |
516 | } |
517 | |
518 | /// The splash radius for the radial reaction. |
519 | double get splashRadius => _splashRadius!; |
520 | double? _splashRadius; |
521 | set splashRadius(double value) { |
522 | if (value == _splashRadius) { |
523 | return; |
524 | } |
525 | _splashRadius = value; |
526 | notifyListeners(); |
527 | } |
528 | |
529 | /// The [Offset] within the Toggleable at which a pointer touched the Toggleable. |
530 | /// |
531 | /// This is null if currently no pointer is touching the Toggleable. |
532 | /// |
533 | /// Usually set to [ToggleableStateMixin.downPosition]. |
534 | Offset? get downPosition => _downPosition; |
535 | Offset? _downPosition; |
536 | set downPosition(Offset? value) { |
537 | if (value == _downPosition) { |
538 | return; |
539 | } |
540 | _downPosition = value; |
541 | notifyListeners(); |
542 | } |
543 | |
544 | /// True if this toggleable has the input focus. |
545 | bool get isFocused => _isFocused!; |
546 | bool? _isFocused; |
547 | set isFocused(bool? value) { |
548 | if (value == _isFocused) { |
549 | return; |
550 | } |
551 | _isFocused = value; |
552 | notifyListeners(); |
553 | } |
554 | |
555 | /// True if this toggleable is being hovered over by a pointer. |
556 | bool get isHovered => _isHovered!; |
557 | bool? _isHovered; |
558 | set isHovered(bool? value) { |
559 | if (value == _isHovered) { |
560 | return; |
561 | } |
562 | _isHovered = value; |
563 | notifyListeners(); |
564 | } |
565 | |
566 | /// Determines whether the toggleable shows as active. |
567 | bool get isActive => _isActive!; |
568 | bool? _isActive; |
569 | set isActive(bool? value) { |
570 | if (value == _isActive) { |
571 | return; |
572 | } |
573 | _isActive = value; |
574 | notifyListeners(); |
575 | } |
576 | |
577 | /// Used by subclasses to paint the radial ink reaction for this control. |
578 | /// |
579 | /// The reaction is painted on the given canvas at the given offset. The |
580 | /// origin is the center point of the reaction (usually distinct from the |
581 | /// [downPosition] at which the user interacted with the control). |
582 | void paintRadialReaction({ |
583 | required Canvas canvas, |
584 | Offset offset = Offset.zero, |
585 | required Offset origin, |
586 | }) { |
587 | if (!reaction.isDismissed || !reactionFocusFade.isDismissed || !reactionHoverFade.isDismissed) { |
588 | final Paint reactionPaint = Paint() |
589 | ..color = Color.lerp( |
590 | Color.lerp( |
591 | Color.lerp(inactiveReactionColor, reactionColor, position.value), |
592 | hoverColor, |
593 | reactionHoverFade.value, |
594 | ), |
595 | focusColor, |
596 | reactionFocusFade.value, |
597 | )!; |
598 | final Animatable<double> radialReactionRadiusTween = Tween<double>( |
599 | begin: 0.0, |
600 | end: splashRadius, |
601 | ); |
602 | final double reactionRadius = isFocused || isHovered |
603 | ? splashRadius |
604 | : radialReactionRadiusTween.evaluate(reaction); |
605 | if (reactionRadius > 0.0) { |
606 | canvas.drawCircle(origin + offset, reactionRadius, reactionPaint); |
607 | } |
608 | } |
609 | } |
610 | |
611 | @override |
612 | void dispose() { |
613 | _position?.removeListener(notifyListeners); |
614 | _reaction?.removeListener(notifyListeners); |
615 | _reactionFocusFade?.removeListener(notifyListeners); |
616 | _reactionHoverFade?.removeListener(notifyListeners); |
617 | super.dispose(); |
618 | } |
619 | |
620 | @override |
621 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; |
622 | |
623 | @override |
624 | bool? hitTest(Offset position) => null; |
625 | |
626 | @override |
627 | SemanticsBuilderCallback? get semanticsBuilder => null; |
628 | |
629 | @override |
630 | bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false; |
631 | |
632 | @override |
633 | String toString() => describeIdentity(this); |
634 | } |
635 | |