toggleable.dart 16.5 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

// Radius of the radial reaction over time.
17
final Animatable<double> _kRadialReactionRadiusTween = Tween<double>(begin: 0.0, end: kRadialReactionRadius);
18

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

22 23 24 25 26
/// 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.
27
abstract class RenderToggleable extends RenderConstrainedBox {
28 29
  /// Creates a toggleable render object.
  ///
30 31
  /// The [activeColor], and [inactiveColor] arguments must not be
  /// null. The [value] can only be null if tristate is true.
32
  RenderToggleable({
33
    @required bool value,
34
    bool tristate = false,
35 36
    @required Color activeColor,
    @required Color inactiveColor,
37 38
    Color hoverColor,
    Color focusColor,
39
    ValueChanged<bool> onChanged,
40
    BoxConstraints additionalConstraints,
41
    @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),
Hixie's avatar
Hixie committed
55
       _onChanged = onChanged,
56 57
       _hasFocus = hasFocus,
       _hovering = hovering,
58
       _vsync = vsync,
59
       super(additionalConstraints: additionalConstraints) {
60
    _tap = TapGestureRecognizer()
61 62 63 64
      ..onTapDown = _handleTapDown
      ..onTap = _handleTap
      ..onTapUp = _handleTapUp
      ..onTapCancel = _handleTapCancel;
65
    _positionController = AnimationController(
Hixie's avatar
Hixie committed
66
      duration: _kToggleDuration,
67
      value: value == false ? 0.0 : 1.0,
68
      vsync: vsync,
69
    );
70
    _position = CurvedAnimation(
71
      parent: _positionController,
72
      curve: Curves.linear,
73 74
    )..addListener(markNeedsPaint)
     ..addStatusListener(_handlePositionStateChanged);
75
    _reactionController = AnimationController(
76 77 78
      duration: kRadialReactionDuration,
      vsync: vsync,
    );
79
    _reaction = CurvedAnimation(
80
      parent: _reactionController,
81
      curve: Curves.fastOutSlowIn,
Adam Barth's avatar
Adam Barth committed
82
    )..addListener(markNeedsPaint);
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    _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);
101 102
  }

103 104 105 106 107 108 109 110 111 112 113 114 115 116
  /// 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;
  AnimationController _positionController;

  /// The visual value of the control.
  ///
  /// When the control is inactive, the [value] is false and this animation has
117 118 119
  /// 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
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  /// 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;
  CurvedAnimation _position;

  /// 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;
  AnimationController _reactionController;
  Animation<double> _reaction;

137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 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
  /// 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;
  AnimationController _reactionFocusFadeController;
  Animation<double> _reactionFocusFade;

  /// 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;
  AnimationController _reactionHoverFadeController;
  Animation<double> _reactionHoverFade;

  /// 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();
  }

197 198 199 200 201 202 203 204 205 206 207 208
  /// 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);
  }

209 210 211 212 213
  /// 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.
214 215 216 217
  ///
  /// When the value changes, this object starts the [positionController] and
  /// [position] animations to animate the visual appearance of the control to
  /// the new value.
218 219
  bool get value => _value;
  bool _value;
220
  set value(bool value) {
221
    assert(tristate || value != null);
222 223 224
    if (value == _value)
      return;
    _value = value;
225
    markNeedsSemanticsUpdate();
226
    _position
227 228
      ..curve = Curves.easeIn
      ..reverseCurve = Curves.easeOut;
229 230 231 232 233 234 235 236 237 238 239
    if (tristate) {
      switch (_positionController.status) {
        case AnimationStatus.forward:
        case AnimationStatus.completed:
          _positionController.reverse();
          break;
        default:
          _positionController.forward();
      }
    } 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 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
  /// 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();
  }

  /// The color that should be used for the reaction when drawn.
  ///
  /// Used when the toggleable needs to change the reaction color/transparency
  /// that is displayed when the toggleable is toggled by a tap.
  ///
  /// Defaults to the [activeColor] at alpha [kRadialReactionAlpha].
  Color get reactionColor => _reactionColor;
  Color _reactionColor;
  set reactionColor(Color value) {
    assert(value != null);
    if (value == _reactionColor)
      return;
    _reactionColor = value;
    markNeedsPaint();
  }

335 336 337 338 339 340 341 342 343 344 345
  /// Called when the control changes value.
  ///
  /// If the control is tapped, [onChanged] is called immediately with the new
  /// value. If the control changes value due to an animation (see
  /// [positionController]), the callback is called when the animation
  /// completes.
  ///
  /// 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.
Hixie's avatar
Hixie committed
346 347
  ValueChanged<bool> get onChanged => _onChanged;
  ValueChanged<bool> _onChanged;
348
  set onChanged(ValueChanged<bool> value) {
Hixie's avatar
Hixie committed
349 350 351 352 353 354
    if (value == _onChanged)
      return;
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
355
      markNeedsSemanticsUpdate();
Hixie's avatar
Hixie committed
356 357
    }
  }
358

359 360 361 362 363 364
  /// 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
365
  bool get isInteractive => onChanged != null;
366

367
  TapGestureRecognizer _tap;
368
  Offset _downPosition;
369

370 371 372
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
373
    if (value == false)
374
      _positionController.reverse();
375 376
    else
      _positionController.forward();
377
    if (isInteractive) {
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
      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() {
395 396
    _positionController.stop();
    _reactionController.stop();
397 398 399
    super.detach();
  }

400 401 402
  // Handle the case where the _positionController's value changes because
  // the user dragged the toggleable: we may reach 0.0 or 1.0 without
  // seeing a tap. The Switch does this.
403
  void _handlePositionStateChanged(AnimationStatus status) {
404 405
    if (isInteractive && !tristate) {
      if (status == AnimationStatus.completed && _value == false) {
406
        onChanged(true);
407
      } else if (status == AnimationStatus.dismissed && _value != false) {
408
        onChanged(false);
409
      }
410
    }
411 412
  }

413
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
414
    if (isInteractive) {
415
      _downPosition = globalToLocal(details.globalPosition);
416
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
417
    }
418 419
  }

420
  void _handleTap() {
421 422 423 424 425 426 427 428 429 430 431 432 433
    if (!isInteractive)
      return;
    switch (value) {
      case false:
        onChanged(true);
        break;
      case true:
        onChanged(tristate ? null : false);
        break;
      default: // case null:
        onChanged(false);
        break;
    }
434
    sendSemanticsEvent(const TapSemanticEvent());
435
  }
Adam Barth's avatar
Adam Barth committed
436

437
  void _handleTapUp(TapUpDetails details) {
Adam Barth's avatar
Adam Barth committed
438
    _downPosition = null;
439
    if (isInteractive)
440
      _reactionController.reverse();
441 442 443
  }

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
444
    _downPosition = null;
445
    if (isInteractive)
446
      _reactionController.reverse();
447 448
  }

449
  @override
450
  bool hitTestSelf(Offset position) => true;
451

452
  @override
Ian Hickson's avatar
Ian Hickson committed
453
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
454
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
455
    if (event is PointerDownEvent && isInteractive)
456 457 458
      _tap.addPointer(event);
  }

459 460 461 462 463 464
  /// 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).
465
  void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
466 467 468 469 470 471 472
    if (!_reaction.isDismissed || !_reactionFocusFade.isDismissed || !_reactionHoverFade.isDismissed) {
      final Paint reactionPaint = Paint()
        ..color = Color.lerp(
          Color.lerp(activeColor.withAlpha(kRadialReactionAlpha), hoverColor, _reactionHoverFade.value),
          focusColor,
          _reactionFocusFade.value,
        );
473
      final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
474 475 476
      final double reactionRadius = hasFocus || hovering
          ? kRadialReactionRadius
          : _kRadialReactionRadiusTween.evaluate(_reaction);
477 478 479
      if (reactionRadius > 0.0) {
        canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
      }
480 481
    }
  }
Hixie's avatar
Hixie committed
482

483
  @override
484 485
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
486

487
    config.isEnabled = isInteractive;
488
    if (isInteractive)
489
      config.onTap = _handleTap;
490
  }
491 492

  @override
493 494
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
495 496
    properties.add(FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    properties.add(FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
497
  }
498
}