toggleable.dart 10.4 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
  RenderToggleable({
27
    @required bool value,
28
    Size size,
29 30
    @required Color activeColor,
    @required Color inactiveColor,
31 32
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
33 34 35 36 37
  }) : assert(value != null),
       assert(activeColor != null),
       assert(inactiveColor != null),
       assert(vsync != null),
       _value = value,
38 39
       _activeColor = activeColor,
       _inactiveColor = inactiveColor,
Hixie's avatar
Hixie committed
40
       _onChanged = onChanged,
41
       _vsync = vsync,
42
       super(additionalConstraints: new BoxConstraints.tight(size)) {
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
  /// 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
82
  /// the value 0.0. When the control is active, the value is true and this
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
  /// 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;
125
    markNeedsSemanticsUpdate(onlyLocalUpdates: 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;
194
  Offset _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
260
  bool hitTestSelf(Offset 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).
275
  void paintRadialReaction(Canvas canvas, Offset offset, Offset origin) {
276
    if (!_reaction.isDismissed) {
277
      // TODO(abarth): We should have a different reaction color when position is zero.
278
      final Paint reactionPaint = new Paint()..color = activeColor.withAlpha(kRadialReactionAlpha);
279
      final Offset center = Offset.lerp(_downPosition ?? origin, origin, _reaction.value);
280
      final double radius = _kRadialReactionRadiusTween.evaluate(_reaction);
Adam Barth's avatar
Adam Barth committed
281
      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

  @override
306
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
307 308 309
    super.debugFillProperties(description);
    description.add(new FlagProperty('value', value: value, ifTrue: 'checked', ifFalse: 'unchecked', showName: true));
    description.add(new FlagProperty('isInteractive', value: isInteractive,  ifTrue: 'enabled', ifFalse: 'disabled', defaultValue: true));
310
  }
311
}