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

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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
  /// 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
  /// the value 0.0. When the control is active, the value is [true] and this
  /// animation has the value 1.0. When the control is changing from inactive
  /// 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);
  }

113 114 115 116 117
  /// Whether this control is current "active" (checked, on, selected) or "inactive" (unchecked, off, not selected).
  ///
  /// When the value changes, this object starts the [positionController] and
  /// [position] animations to animate the visual appearance of the control to
  /// the new value.
118 119
  bool get value => _value;
  bool _value;
120
  set value(bool value) {
121
    assert(value != null);
122 123 124
    if (value == _value)
      return;
    _value = value;
Hixie's avatar
Hixie committed
125
    markNeedsSemanticsUpdate(onlyChanges: true, noGeometry: true);
126
    _position
127 128
      ..curve = Curves.easeIn
      ..reverseCurve = Curves.easeOut;
Adam Barth's avatar
Adam Barth committed
129 130 131 132
    if (value)
      _positionController.forward();
    else
      _positionController.reverse();
133 134
  }

135 136 137
  /// 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.
138 139
  Color get activeColor => _activeColor;
  Color _activeColor;
140
  set activeColor(Color value) {
141 142
    assert(value != null);
    if (value == _activeColor)
143
      return;
144 145 146 147
    _activeColor = value;
    markNeedsPaint();
  }

148 149 150
  /// 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.
151 152
  Color get inactiveColor => _inactiveColor;
  Color _inactiveColor;
153
  set inactiveColor(Color value) {
154 155 156 157
    assert(value != null);
    if (value == _inactiveColor)
      return;
    _inactiveColor = value;
158 159 160
    markNeedsPaint();
  }

161 162 163 164 165 166 167 168 169 170 171
  /// 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
172 173
  ValueChanged<bool> get onChanged => _onChanged;
  ValueChanged<bool> _onChanged;
174
  set onChanged(ValueChanged<bool> value) {
Hixie's avatar
Hixie committed
175 176 177 178 179 180 181 182 183
    if (value == _onChanged)
      return;
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
      markNeedsSemanticsUpdate(noGeometry: true);
    }
  }
184

185 186 187 188 189 190
  /// 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
191
  bool get isInteractive => onChanged != null;
192

193
  TapGestureRecognizer _tap;
Adam Barth's avatar
Adam Barth committed
194
  Point _downPosition;
195

196 197 198
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
199 200 201 202 203
    if (value)
      _positionController.forward();
    else
      _positionController.reverse();
    if (isInteractive) {
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
      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() {
221 222
    _positionController.stop();
    _reactionController.stop();
223 224 225
    super.detach();
  }

226
  void _handlePositionStateChanged(AnimationStatus status) {
227
    if (isInteractive) {
228
      if (status == AnimationStatus.completed && !_value)
229
        onChanged(true);
230
      else if (status == AnimationStatus.dismissed && _value)
231 232
        onChanged(false);
    }
233 234
  }

235
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
236
    if (isInteractive) {
237
      _downPosition = globalToLocal(details.globalPosition);
238
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
239
    }
240 241
  }

242
  void _handleTap() {
243
    if (isInteractive)
244
      onChanged(!_value);
245
  }
Adam Barth's avatar
Adam Barth committed
246

247
  void _handleTapUp(TapUpDetails details) {
Adam Barth's avatar
Adam Barth committed
248
    _downPosition = null;
249
    if (isInteractive)
250
      _reactionController.reverse();
251 252 253
  }

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
254
    _downPosition = null;
255
    if (isInteractive)
256
      _reactionController.reverse();
257 258
  }

259
  @override
Adam Barth's avatar
Adam Barth committed
260
  bool hitTestSelf(Point position) => true;
261

262
  @override
Ian Hickson's avatar
Ian Hickson committed
263
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
264
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
265
    if (event is PointerDownEvent && isInteractive)
266 267 268
      _tap.addPointer(event);
  }

269 270 271 272 273 274
  /// 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).
Adam Barth's avatar
Adam Barth committed
275
  void paintRadialReaction(Canvas canvas, Offset offset, Point origin) {
276
    if (!_reaction.isDismissed) {
277 278
      // TODO(abarth): We should have a different reaction color when position is zero.
      Paint reactionPaint = new Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
Adam Barth's avatar
Adam Barth committed
279 280 281
      Point center = Point.lerp(_downPosition ?? origin, origin, _reaction.value);
      double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
      canvas.drawCircle(center + offset, radius, reactionPaint);
282 283
    }
  }
Hixie's avatar
Hixie committed
284

285
  @override
286
  bool get isSemanticBoundary => isInteractive;
287 288

  @override
289
  SemanticsAnnotator get semanticsAnnotator => _annotate;
290 291 292 293 294 295

  void _annotate(SemanticsNode semantics) {
    semantics
      ..hasCheckedState = true
      ..isChecked = _value;
    if (isInteractive)
296
      semantics.addAction(SemanticsAction.tap);
Hixie's avatar
Hixie committed
297
  }
298 299

  @override
300 301
  void performAction(SemanticsAction action) {
    if (action == SemanticsAction.tap)
302 303
      _handleTap();
  }
304 305 306 307 308 309 310 311

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('value: ${value ? "checked" : "unchecked"}');
    if (!isInteractive)
      description.add('disabled');
  }
312
}