toggleable.dart 15.9 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
    )..addListener(markNeedsPaint);
74
    _reactionController = AnimationController(
75 76 77
      duration: kRadialReactionDuration,
      vsync: vsync,
    );
78
    _reaction = CurvedAnimation(
79
      parent: _reactionController,
80
      curve: Curves.fastOutSlowIn,
Adam Barth's avatar
Adam Barth committed
81
    )..addListener(markNeedsPaint);
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
    _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);
100 101
  }

102 103 104 105 106 107 108 109 110 111 112 113 114 115
  /// 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
116 117 118
  /// 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
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  /// 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;

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

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

208 209 210 211 212
  /// 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.
213 214 215 216
  ///
  /// When the value changes, this object starts the [positionController] and
  /// [position] animations to animate the visual appearance of the control to
  /// the new value.
217 218
  bool get value => _value;
  bool _value;
219
  set value(bool value) {
220
    assert(tristate || value != null);
221 222 223
    if (value == _value)
      return;
    _value = value;
224
    markNeedsSemanticsUpdate();
225
    _position
226 227
      ..curve = Curves.easeIn
      ..reverseCurve = Curves.easeOut;
228 229 230 231 232 233 234 235 236 237 238
    if (tristate) {
      switch (_positionController.status) {
        case AnimationStatus.forward:
        case AnimationStatus.completed:
          _positionController.reverse();
          break;
        default:
          _positionController.forward();
      }
    } else {
      if (value == true)
239
        _positionController.forward();
240 241
      else
        _positionController.reverse();
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
    }
  }

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

260 261 262
  /// 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.
263 264
  Color get activeColor => _activeColor;
  Color _activeColor;
265
  set activeColor(Color value) {
266 267
    assert(value != null);
    if (value == _activeColor)
268
      return;
269 270 271 272
    _activeColor = value;
    markNeedsPaint();
  }

273 274 275
  /// 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.
276 277
  Color get inactiveColor => _inactiveColor;
  Color _inactiveColor;
278
  set inactiveColor(Color value) {
279 280 281 282
    assert(value != null);
    if (value == _inactiveColor)
      return;
    _inactiveColor = value;
283 284 285
    markNeedsPaint();
  }

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

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

356 357 358 359 360 361
  /// 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
362
  bool get isInteractive => onChanged != null;
363

364
  TapGestureRecognizer _tap;
365
  Offset _downPosition;
366

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

399
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
400
    if (isInteractive) {
401
      _downPosition = globalToLocal(details.globalPosition);
402
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
403
    }
404 405
  }

406
  void _handleTap() {
407 408 409 410 411 412 413 414 415 416 417 418 419
    if (!isInteractive)
      return;
    switch (value) {
      case false:
        onChanged(true);
        break;
      case true:
        onChanged(tristate ? null : false);
        break;
      default: // case null:
        onChanged(false);
        break;
    }
420
    sendSemanticsEvent(const TapSemanticEvent());
421
  }
Adam Barth's avatar
Adam Barth committed
422

423
  void _handleTapUp(TapUpDetails details) {
Adam Barth's avatar
Adam Barth committed
424
    _downPosition = null;
425
    if (isInteractive)
426
      _reactionController.reverse();
427 428 429
  }

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
430
    _downPosition = null;
431
    if (isInteractive)
432
      _reactionController.reverse();
433 434
  }

435
  @override
436
  bool hitTestSelf(Offset position) => true;
437

438
  @override
Ian Hickson's avatar
Ian Hickson committed
439
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
440
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
441
    if (event is PointerDownEvent && isInteractive)
442 443 444
      _tap.addPointer(event);
  }

445 446 447 448 449 450
  /// 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).
451
  void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
452 453 454 455 456 457 458
    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,
        );
459
      final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
460 461 462
      final double reactionRadius = hasFocus || hovering
          ? kRadialReactionRadius
          : _kRadialReactionRadiusTween.evaluate(_reaction);
463 464 465
      if (reactionRadius > 0.0) {
        canvas.drawCircle(center + offset, reactionRadius, reactionPaint);
      }
466 467
    }
  }
Hixie's avatar
Hixie committed
468

469
  @override
470 471
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
472

473
    config.isEnabled = isInteractive;
474
    if (isInteractive)
475
      config.onTap = _handleTap;
476
  }
477 478

  @override
479 480
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
481 482
    properties.add(FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    properties.add(FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
483
  }
484
}