toggleable.dart 17.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/animation.dart';
6
import 'package:flutter/foundation.dart';
7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/scheduler.dart';
10

11 12
import 'constants.dart';

13
// Duration of the animation that moves the toggle from one state to another.
14
const Duration _kToggleDuration = Duration(milliseconds: 200);
15 16 17 18

// Duration of the fade animation for the reaction when focus and hover occur.
const Duration _kReactionFadeDuration = Duration(milliseconds: 50);

19 20 21 22 23
/// A base class for material style toggleable controls with toggle animations.
///
/// This class handles storing the current value, dispatching ValueChanged on a
/// tap gesture and driving a changed animation. Subclasses are responsible for
/// painting.
24
abstract class RenderToggleable extends RenderConstrainedBox {
25 26
  /// Creates a toggleable render object.
  ///
27 28
  /// The [activeColor], and [inactiveColor] arguments must not be
  /// null. The [value] can only be null if tristate is true.
29
  RenderToggleable({
30
    required bool? value,
31
    bool tristate = false,
32 33 34 35
    required Color activeColor,
    required Color inactiveColor,
    Color? hoverColor,
    Color? focusColor,
36 37
    Color? reactionColor,
    Color? inactiveReactionColor,
38
    required double splashRadius,
39 40 41
    ValueChanged<bool?>? onChanged,
    required BoxConstraints additionalConstraints,
    required TickerProvider vsync,
42 43
    bool hasFocus = false,
    bool hovering = false,
44 45
  }) : assert(tristate != null),
       assert(tristate || value != null),
46 47 48 49
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       _value = value,
50
       _tristate = tristate,
51 52
       _activeColor = activeColor,
       _inactiveColor = inactiveColor,
53 54
       _hoverColor = hoverColor ?? activeColor.withAlpha(kRadialReactionAlpha),
       _focusColor = focusColor ?? activeColor.withAlpha(kRadialReactionAlpha),
55 56
       _reactionColor = reactionColor ?? activeColor.withAlpha(kRadialReactionAlpha),
       _inactiveReactionColor = inactiveReactionColor ?? activeColor.withAlpha(kRadialReactionAlpha),
57
       _splashRadius = splashRadius,
Hixie's avatar
Hixie committed
58
       _onChanged = onChanged,
59 60
       _hasFocus = hasFocus,
       _hovering = hovering,
61
       _vsync = vsync,
62
       super(additionalConstraints: additionalConstraints) {
63
    _tap = TapGestureRecognizer()
64 65 66 67
      ..onTapDown = _handleTapDown
      ..onTap = _handleTap
      ..onTapUp = _handleTapUp
      ..onTapCancel = _handleTapCancel;
68
    _positionController = AnimationController(
Hixie's avatar
Hixie committed
69
      duration: _kToggleDuration,
70
      value: value == false ? 0.0 : 1.0,
71
      vsync: vsync,
72
    );
73
    _position = CurvedAnimation(
74
      parent: _positionController,
75
      curve: Curves.linear,
76
    )..addListener(markNeedsPaint);
77
    _reactionController = AnimationController(
78 79 80
      duration: kRadialReactionDuration,
      vsync: vsync,
    );
81
    _reaction = CurvedAnimation(
82
      parent: _reactionController,
83
      curve: Curves.fastOutSlowIn,
Adam Barth's avatar
Adam Barth committed
84
    )..addListener(markNeedsPaint);
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    _reactionHoverFadeController = AnimationController(
      duration: _kReactionFadeDuration,
      value: hovering || hasFocus ? 1.0 : 0.0,
      vsync: vsync,
    );
    _reactionHoverFade = CurvedAnimation(
      parent: _reactionHoverFadeController,
      curve: Curves.fastOutSlowIn,
    )..addListener(markNeedsPaint);
    _reactionFocusFadeController = AnimationController(
      duration: _kReactionFadeDuration,
      value: hovering || hasFocus ? 1.0 : 0.0,
      vsync: vsync,
    );
    _reactionFocusFade = CurvedAnimation(
      parent: _reactionFocusFadeController,
      curve: Curves.fastOutSlowIn,
    )..addListener(markNeedsPaint);
103 104
  }

105 106 107 108 109 110 111 112 113
  /// Used by subclasses to manipulate the visual value of the control.
  ///
  /// Some controls respond to user input by updating their visual value. For
  /// example, the thumb of a switch moves from one position to another when
  /// dragged. These controls manipulate this animation controller to update
  /// their [position] and eventually trigger an [onChanged] callback when the
  /// animation reaches either 0.0 or 1.0.
  @protected
  AnimationController get positionController => _positionController;
114
  late AnimationController _positionController;
115 116 117 118

  /// The visual value of the control.
  ///
  /// When the control is inactive, the [value] is false and this animation has
119 120 121
  /// the value 0.0. When the control is active, the value either true or tristate
  /// is true and the value is null. When the control is active the animation
  /// has a value of 1.0. When the control is changing from inactive
122 123 124
  /// to active (or vice versa), [value] is the target value and this animation
  /// gradually updates from 0.0 to 1.0 (or vice versa).
  CurvedAnimation get position => _position;
125
  late CurvedAnimation _position;
126 127 128 129 130 131 132 133 134 135

  /// Used by subclasses to control the radial reaction animation.
  ///
  /// Some controls have a radial ink reaction to user input. This animation
  /// controller can be used to start or stop these ink reactions.
  ///
  /// Subclasses should call [paintRadialReaction] to actually paint the radial
  /// reaction.
  @protected
  AnimationController get reactionController => _reactionController;
136 137
  late AnimationController _reactionController;
  late Animation<double> _reaction;
138

139 140 141 142 143 144 145 146 147 148 149
  /// Used by subclasses to control the radial reaction's opacity animation for
  /// [hasFocus] changes.
  ///
  /// Some controls have a radial ink reaction to focus. This animation
  /// controller can be used to start or stop these ink reaction fade-ins and
  /// fade-outs.
  ///
  /// Subclasses should call [paintRadialReaction] to actually paint the radial
  /// reaction.
  @protected
  AnimationController get reactionFocusFadeController => _reactionFocusFadeController;
150 151
  late AnimationController _reactionFocusFadeController;
  late Animation<double> _reactionFocusFade;
152 153 154 155 156 157 158 159 160 161 162 163

  /// Used by subclasses to control the radial reaction's opacity animation for
  /// [hovering] changes.
  ///
  /// Some controls have a radial ink reaction to pointer hover. This animation
  /// controller can be used to start or stop these ink reaction fade-ins and
  /// fade-outs.
  ///
  /// Subclasses should call [paintRadialReaction] to actually paint the radial
  /// reaction.
  @protected
  AnimationController get reactionHoverFadeController => _reactionHoverFadeController;
164 165
  late AnimationController _reactionHoverFadeController;
  late Animation<double> _reactionHoverFade;
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198

  /// True if this toggleable has the input focus.
  bool get hasFocus => _hasFocus;
  bool _hasFocus;
  set hasFocus(bool value) {
    assert(value != null);
    if (value == _hasFocus)
      return;
    _hasFocus = value;
    if (_hasFocus) {
      _reactionFocusFadeController.forward();
    } else {
      _reactionFocusFadeController.reverse();
    }
    markNeedsPaint();
  }

  /// True if this toggleable is being hovered over by a pointer.
  bool get hovering => _hovering;
  bool _hovering;
  set hovering(bool value) {
    assert(value != null);
    if (value == _hovering)
      return;
    _hovering = value;
    if (_hovering) {
      _reactionHoverFadeController.forward();
    } else {
      _reactionHoverFadeController.reverse();
    }
    markNeedsPaint();
  }

199 200 201 202 203 204 205 206 207 208 209 210
  /// The [TickerProvider] for the [AnimationController]s that run the animations.
  TickerProvider get vsync => _vsync;
  TickerProvider _vsync;
  set vsync(TickerProvider value) {
    assert(value != null);
    if (value == _vsync)
      return;
    _vsync = value;
    positionController.resync(vsync);
    reactionController.resync(vsync);
  }

211 212 213 214 215
  /// False if this control is "inactive" (not checked, off, or unselected).
  ///
  /// If value is true then the control "active" (checked, on, or selected). If
  /// tristate is true and value is null, then the control is considered to be
  /// in its third or "indeterminate" state.
216 217 218 219
  ///
  /// When the value changes, this object starts the [positionController] and
  /// [position] animations to animate the visual appearance of the control to
  /// the new value.
220 221 222
  bool? get value => _value;
  bool? _value;
  set value(bool? value) {
223
    assert(tristate || value != null);
224 225 226
    if (value == _value)
      return;
    _value = value;
227
    markNeedsSemanticsUpdate();
228
    _position
229 230
      ..curve = Curves.easeIn
      ..reverseCurve = Curves.easeOut;
231
    if (tristate) {
232 233 234 235 236 237
      if (value == null)
        _positionController.value = 0.0;
      if (value != false)
        _positionController.forward();
      else
        _positionController.reverse();
238 239
    } else {
      if (value == true)
240
        _positionController.forward();
241 242
      else
        _positionController.reverse();
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
    }
  }

  /// If true, [value] can be true, false, or null, otherwise [value] must
  /// be true or false.
  ///
  /// When [tristate] is true and [value] is null, then the control is
  /// considered to be in its third or "indeterminate" state.
  bool get tristate => _tristate;
  bool _tristate;
  set tristate(bool value) {
    assert(tristate != null);
    if (value == _tristate)
      return;
    _tristate = value;
    markNeedsSemanticsUpdate();
259 260
  }

261 262 263
  /// The color that should be used in the active state (i.e., when [value] is true).
  ///
  /// For example, a checkbox should use this color when checked.
264 265
  Color get activeColor => _activeColor;
  Color _activeColor;
266
  set activeColor(Color value) {
267 268
    assert(value != null);
    if (value == _activeColor)
269
      return;
270 271 272 273
    _activeColor = value;
    markNeedsPaint();
  }

274 275 276
  /// The color that should be used in the inactive state (i.e., when [value] is false).
  ///
  /// For example, a checkbox should use this color when unchecked.
277 278
  Color get inactiveColor => _inactiveColor;
  Color _inactiveColor;
279
  set inactiveColor(Color value) {
280 281 282 283
    assert(value != null);
    if (value == _inactiveColor)
      return;
    _inactiveColor = value;
284 285 286
    markNeedsPaint();
  }

287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
  /// The color that should be used for the reaction when [hovering] is true.
  ///
  /// Used when the toggleable needs to change the reaction color/transparency,
  /// when it is being hovered over.
  ///
  /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
  Color get hoverColor => _hoverColor;
  Color _hoverColor;
  set hoverColor(Color value) {
    assert(value != null);
    if (value == _hoverColor)
      return;
    _hoverColor = value;
    markNeedsPaint();
  }

  /// The color that should be used for the reaction when [hasFocus] is true.
  ///
  /// Used when the toggleable needs to change the reaction color/transparency,
  /// when it has focus.
  ///
  /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
  Color get focusColor => _focusColor;
  Color _focusColor;
  set focusColor(Color value) {
    assert(value != null);
    if (value == _focusColor)
      return;
    _focusColor = value;
    markNeedsPaint();
  }

319 320
  /// The color that should be used for the reaction when the toggleable is
  /// active.
321 322
  ///
  /// Used when the toggleable needs to change the reaction color/transparency
323
  /// that is displayed when the toggleable is active and tapped.
324 325
  ///
  /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
326 327 328
  Color? get reactionColor => _reactionColor;
  Color? _reactionColor;
  set reactionColor(Color? value) {
329 330 331 332 333 334 335
    assert(value != null);
    if (value == _reactionColor)
      return;
    _reactionColor = value;
    markNeedsPaint();
  }

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
  /// The color that should be used for the reaction when the toggleable is
  /// inactive.
  ///
  /// Used when the toggleable needs to change the reaction color/transparency
  /// that is displayed when the toggleable is inactive and tapped.
  ///
  /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
  Color? get inactiveReactionColor => _inactiveReactionColor;
  Color? _inactiveReactionColor;
  set inactiveReactionColor(Color? value) {
    assert(value != null);
    if (value == _inactiveReactionColor)
      return;
    _inactiveReactionColor = value;
    markNeedsPaint();
  }

353 354 355 356 357 358 359 360 361 362
  /// The splash radius for the radial reaction.
  double get splashRadius => _splashRadius;
  double _splashRadius;
  set splashRadius(double value) {
    if (value == _splashRadius)
      return;
    _splashRadius = value;
    markNeedsPaint();
  }

363 364 365
  /// Called when the control changes value.
  ///
  /// If the control is tapped, [onChanged] is called immediately with the new
366
  /// value.
367 368 369 370 371
  ///
  /// The control is considered interactive (see [isInteractive]) if this
  /// callback is non-null. If the callback is null, then the control is
  /// disabled, and non-interactive. A disabled checkbox, for example, is
  /// displayed using a grey color and its value cannot be changed.
372 373 374
  ValueChanged<bool?>? get onChanged => _onChanged;
  ValueChanged<bool?>? _onChanged;
  set onChanged(ValueChanged<bool?>? value) {
Hixie's avatar
Hixie committed
375 376 377 378 379 380
    if (value == _onChanged)
      return;
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
381
      markNeedsSemanticsUpdate();
Hixie's avatar
Hixie committed
382 383
    }
  }
384

385 386 387 388 389 390
  /// Whether [value] of this control can be changed by user interaction.
  ///
  /// The control is considered interactive if the [onChanged] callback is
  /// non-null. If the callback is null, then the control is disabled, and
  /// non-interactive. A disabled checkbox, for example, is displayed using a
  /// grey color and its value cannot be changed.
Hixie's avatar
Hixie committed
391
  bool get isInteractive => onChanged != null;
392

393 394
  late TapGestureRecognizer _tap;
  Offset? _downPosition;
395

396 397 398
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
399
    if (value == false)
400
      _positionController.reverse();
401 402
    else
      _positionController.forward();
403
    if (isInteractive) {
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
      switch (_reactionController.status) {
        case AnimationStatus.forward:
          _reactionController.forward();
          break;
        case AnimationStatus.reverse:
          _reactionController.reverse();
          break;
        case AnimationStatus.dismissed:
        case AnimationStatus.completed:
          // nothing to do
          break;
      }
    }
  }

  @override
  void detach() {
421 422
    _positionController.stop();
    _reactionController.stop();
423 424
    _reactionHoverFadeController.stop();
    _reactionFocusFadeController.stop();
425 426 427
    super.detach();
  }

428
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
429
    if (isInteractive) {
430
      _downPosition = globalToLocal(details.globalPosition);
431
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
432
    }
433 434
  }

435
  void _handleTap() {
436 437 438 439
    if (!isInteractive)
      return;
    switch (value) {
      case false:
440
        onChanged!(true);
441 442
        break;
      case true:
443
        onChanged!(tristate ? null : false);
444
        break;
445 446
      case null:
        onChanged!(false);
447 448
        break;
    }
449
    sendSemanticsEvent(const TapSemanticEvent());
450
  }
Adam Barth's avatar
Adam Barth committed
451

452
  void _handleTapUp(TapUpDetails details) {
Adam Barth's avatar
Adam Barth committed
453
    _downPosition = null;
454
    if (isInteractive)
455
      _reactionController.reverse();
456 457 458
  }

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
459
    _downPosition = null;
460
    if (isInteractive)
461
      _reactionController.reverse();
462 463
  }

464
  @override
465
  bool hitTestSelf(Offset position) => true;
466

467
  @override
Ian Hickson's avatar
Ian Hickson committed
468
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
469
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
470
    if (event is PointerDownEvent && isInteractive)
471 472 473
      _tap.addPointer(event);
  }

474 475 476 477 478 479
  /// Used by subclasses to paint the radial ink reaction for this control.
  ///
  /// The reaction is painted on the given canvas at the given offset. The
  /// origin is the center point of the reaction (usually distinct from the
  /// point at which the user interacted with the control, which is handled
  /// automatically).
480
  void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
481 482 483
    if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
      final Paint reactionPaint = Paint()
        ..color = Color.lerp(
484 485 486 487 488
          Color.lerp(
            Color.lerp(inactiveReactionColor, reactionColor, _position.value),
            hoverColor,
            _reactionHoverFade.value,
          ),
489 490
          focusColor,
          _reactionFocusFade.value,
491 492
        )!;
      final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value)!;
493 494 495 496
      final Animatable<double> radialReactionRadiusTween = Tween<double>(
        begin: 0.0,
        end: splashRadius,
      );
497
      final double reactionRadius = hasFocus || hovering
498 499
          ? splashRadius
          : radialReactionRadiusTween.evaluate(_reaction);
500 501 502
      if (reactionRadius > 0.0) {
        canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
      }
503 504
    }
  }
Hixie's avatar
Hixie committed
505

506
  @override
507 508
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
509

510
    config.isEnabled = isInteractive;
511
    if (isInteractive)
512
      config.onTap = _handleTap;
513
  }
514 515

  @override
516 517
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
518 519
    properties.add(FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    properties.add(FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
520
  }
521
}