toggleable.dart 11.7 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
    if (tristate) {
      switch (_positionController.status) {
        case AnimationStatus.forward:
        case AnimationStatus.completed:
          _positionController.reverse();
          break;
        default:
          _positionController.forward();
      }
    } else {
      if (value == true)
148
        _positionController.forward();
149 150
      else
        _positionController.reverse();
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    }
  }

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

169 170 171
  /// 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.
172 173
  Color get activeColor => _activeColor;
  Color _activeColor;
174
  set activeColor(Color value) {
175 176
    assert(value != null);
    if (value == _activeColor)
177
      return;
178 179 180 181
    _activeColor = value;
    markNeedsPaint();
  }

182 183 184
  /// 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.
185 186
  Color get inactiveColor => _inactiveColor;
  Color _inactiveColor;
187
  set inactiveColor(Color value) {
188 189 190 191
    assert(value != null);
    if (value == _inactiveColor)
      return;
    _inactiveColor = value;
192 193 194
    markNeedsPaint();
  }

195 196 197 198 199 200 201 202 203 204 205
  /// 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
206 207
  ValueChanged<bool> get onChanged => _onChanged;
  ValueChanged<bool> _onChanged;
208
  set onChanged(ValueChanged<bool> value) {
Hixie's avatar
Hixie committed
209 210 211 212 213 214
    if (value == _onChanged)
      return;
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
      markNeedsPaint();
215
      markNeedsSemanticsUpdate();
Hixie's avatar
Hixie committed
216 217
    }
  }
218

219 220 221 222 223 224
  /// 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
225
  bool get isInteractive => onChanged != null;
226

227
  TapGestureRecognizer _tap;
228
  Offset _downPosition;
229

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

260 261 262
  // 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.
263
  void _handlePositionStateChanged(AnimationStatus status) {
264 265
    if (isInteractive && !tristate) {
      if (status == AnimationStatus.completed && _value == false) {
266
        onChanged(true);
267
      } else if (status == AnimationStatus.dismissed && _value != false) {
268
        onChanged(false);
269
      }
270
    }
271 272
  }

273
  void _handleTapDown(TapDownDetails details) {
Adam Barth's avatar
Adam Barth committed
274
    if (isInteractive) {
275
      _downPosition = globalToLocal(details.globalPosition);
276
      _reactionController.forward();
Adam Barth's avatar
Adam Barth committed
277
    }
278 279
  }

280
  void _handleTap() {
281 282 283 284 285 286 287 288 289 290 291 292 293
    if (!isInteractive)
      return;
    switch (value) {
      case false:
        onChanged(true);
        break;
      case true:
        onChanged(tristate ? null : false);
        break;
      default: // case null:
        onChanged(false);
        break;
    }
294
  }
Adam Barth's avatar
Adam Barth committed
295

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

  void _handleTapCancel() {
Adam Barth's avatar
Adam Barth committed
303
    _downPosition = null;
304
    if (isInteractive)
305
      _reactionController.reverse();
306 307
  }

308
  @override
309
  bool hitTestSelf(Offset position) => true;
310

311
  @override
Ian Hickson's avatar
Ian Hickson committed
312
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
313
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
314
    if (event is PointerDownEvent && isInteractive)
315 316 317
      _tap.addPointer(event);
  }

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

334
  @override
335 336
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
337

338
    config.isEnabled = isInteractive;
339
    if (isInteractive)
340
      config.onTap = _handleTap;
341
    config.isChecked = _value != false;
342
  }
343 344

  @override
345 346 347 348
  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));
349
  }
350
}