toggleable.dart 11.5 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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
const Duration _kToggleDuration = const Duration(milliseconds: 200);
Adam Barth's avatar
Adam Barth committed
14
final Tween<double> _kRadialReactionRadiusTween = new Tween<double>(begin: 0.0, end: kRadialReactionRadius);
15

16 17 18 19 20
/// 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.
21
abstract class RenderToggleable extends RenderConstrainedBox {
22 23
  /// Creates a toggleable render object.
  ///
24 25
  /// The [activeColor], and [inactiveColor] arguments must not be
  /// null. The [value] can only be null if tristate is true.
26
  RenderToggleable({
27
    @required bool value,
28
    bool tristate: false,
29
    Size size,
30 31
    @required Color activeColor,
    @required Color inactiveColor,
32 33
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
34 35
  }) : assert(tristate != null),
       assert(tristate || value != null),
36 37 38 39
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       _value = value,
40
       _tristate = tristate,
41 42
       _activeColor = activeColor,
       _inactiveColor = inactiveColor,
Hixie's avatar
Hixie committed
43
       _onChanged = onChanged,
44
       _vsync = vsync,
45
       super(additionalConstraints: new BoxConstraints.tight(size)) {
46
    _tap = new TapGestureRecognizer()
47 48 49 50
      ..onTapDown = _handleTapDown
      ..onTap = _handleTap
      ..onTapUp = _handleTapUp
      ..onTapCancel = _handleTapCancel;
51
    _positionController = new AnimationController(
Hixie's avatar
Hixie committed
52
      duration: _kToggleDuration,
53
      value: value == false ? 0.0 : 1.0,
54
      vsync: vsync,
55 56
    );
    _position = new CurvedAnimation(
57
      parent: _positionController,
58
      curve: Curves.linear,
59 60
    )..addListener(markNeedsPaint)
     ..addStatusListener(_handlePositionStateChanged);
61 62 63 64
    _reactionController = new AnimationController(
      duration: kRadialReactionDuration,
      vsync: vsync,
    );
Adam Barth's avatar
Adam Barth committed
65
    _reaction = new CurvedAnimation(
66
      parent: _reactionController,
67
      curve: Curves.fastOutSlowIn,
Adam Barth's avatar
Adam Barth committed
68
    )..addListener(markNeedsPaint);
69 70
  }

71 72 73 74 75 76 77 78 79 80 81 82 83 84
  /// 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
85 86 87
  /// 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
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
  /// 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;

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

117 118 119 120 121
  /// 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.
122 123 124 125
  ///
  /// When the value changes, this object starts the [positionController] and
  /// [position] animations to animate the visual appearance of the control to
  /// the new value.
126 127
  bool get value => _value;
  bool _value;
128
  set value(bool value) {
129
    assert(tristate || value != null);
130 131 132
    if (value == _value)
      return;
    _value = value;
133
    markNeedsSemanticsUpdate();
134
    _position
135 136
      ..curve = Curves.easeIn
      ..reverseCurve = Curves.easeOut;
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
    switch (_positionController.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        _positionController.reverse();
        break;
      default:
        _positionController.forward();
    }
  }

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

162 163 164
  /// 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.
165 166
  Color get activeColor => _activeColor;
  Color _activeColor;
167
  set activeColor(Color value) {
168 169
    assert(value != null);
    if (value == _activeColor)
170
      return;
171 172 173 174
    _activeColor = value;
    markNeedsPaint();
  }

175 176 177
  /// 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.
178 179
  Color get inactiveColor => _inactiveColor;
  Color _inactiveColor;
180
  set inactiveColor(Color value) {
181 182 183 184
    assert(value != null);
    if (value == _inactiveColor)
      return;
    _inactiveColor = value;
185 186 187
    markNeedsPaint();
  }

188 189 190 191 192 193 194 195 196 197 198
  /// 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
199 200
  ValueChanged<bool> get onChanged => _onChanged;
  ValueChanged<bool> _onChanged;
201
  set onChanged(ValueChanged<bool> value) {
Hixie's avatar
Hixie committed
202 203 204 205 206 207
    if (value == _onChanged)
      return;
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
208
      markNeedsSemanticsUpdate();
Hixie's avatar
Hixie committed
209 210
    }
  }
211

212 213 214 215 216 217
  /// 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
218
  bool get isInteractive => onChanged != null;
219

220
  TapGestureRecognizer _tap;
221
  Offset _downPosition;
222

223 224 225
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
226
    if (value == false)
227
      _positionController.reverse();
228 229
    else
      _positionController.forward();
230
    if (isInteractive) {
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
      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() {
248 249
    _positionController.stop();
    _reactionController.stop();
250 251 252
    super.detach();
  }

253 254 255
  // 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.
256
  void _handlePositionStateChanged(AnimationStatus status) {
257 258
    if (isInteractive && !tristate) {
      if (status == AnimationStatus.completed && _value == false) {
259
        onChanged(true);
260 261
      }
      else if (status == AnimationStatus.dismissed && _value != false) {
262
        onChanged(false);
263
      }
264
    }
265 266
  }

267
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
268
    if (isInteractive) {
269
      _downPosition = globalToLocal(details.globalPosition);
270
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
271
    }
272 273
  }

274
  void _handleTap() {
275 276 277 278 279 280 281 282 283 284 285 286 287
    if (!isInteractive)
      return;
    switch (value) {
      case false:
        onChanged(true);
        break;
      case true:
        onChanged(tristate ? null : false);
        break;
      default: // case null:
        onChanged(false);
        break;
    }
288
  }
Adam Barth's avatar
Adam Barth committed
289

290
  void _handleTapUp(TapUpDetails details) {
Adam Barth's avatar
Adam Barth committed
291
    _downPosition = null;
292
    if (isInteractive)
293
      _reactionController.reverse();
294 295 296
  }

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
297
    _downPosition = null;
298
    if (isInteractive)
299
      _reactionController.reverse();
300 301
  }

302
  @override
303
  bool hitTestSelf(Offset position) => true;
304

305
  @override
Ian Hickson's avatar
Ian Hickson committed
306
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
307
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
308
    if (event is PointerDownEvent && isInteractive)
309 310 311
      _tap.addPointer(event);
  }

312 313 314 315 316 317
  /// 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).
318
  void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
319
    if (!_reaction.isDismissed) {
320
      // TODO(abarth): We should have a different reaction color when position is zero.
321
      final Paint reactionPaint = new Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
322
      final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
323
      final double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
Adam Barth's avatar
Adam Barth committed
324
      canvas.drawCircle(center + offset, radius, reactionPaint);
325 326
    }
  }
Hixie's avatar
Hixie committed
327

328
  @override
329 330
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
331

332
    config.isEnabled = isInteractive;
333
    if (isInteractive)
334
      config.onTap = _handleTap;
335
    config.isChecked = _value != false;
336
  }
337 338

  @override
339 340 341 342
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    properties.add(new FlagProperty('isInteractive', value: isInteractive, ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
343
  }
344
}