switch.dart 12.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/gestures.dart';
6
import 'package:flutter/rendering.dart';
7
import 'package:flutter/services.dart';
8
import 'package:flutter/widgets.dart';
9
import 'package:meta/meta.dart';
10

11
import 'colors.dart';
12
import 'constants.dart';
13
import 'debug.dart';
Adam Barth's avatar
Adam Barth committed
14
import 'shadows.dart';
15
import 'theme.dart';
16
import 'toggleable.dart';
17

18 19 20 21 22 23 24 25 26 27 28 29
/// A material design switch.
///
/// Used to toggle the on/off state of a single setting.
///
/// The switch itself does not maintain any state. Instead, when the state of
/// the switch changes, the widget calls the [onChanged] callback. Most widgets
/// that use a switch will listen for the [onChanged] callback and rebuild the
/// switch with a new [value] to update the visual appearance of the switch.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
30
///
31 32 33
///  * [CheckBox]
///  * [Radio]
///  * [Slider]
34
///  * <https://material.google.com/components/selection-controls.html#selection-controls-switch>
35
class Switch extends StatefulWidget {
36 37 38 39 40 41 42 43 44
  /// Creates a material design switch.
  ///
  /// The switch itself does not maintain any state. Instead, when the state of
  /// the switch changes, the widget calls the [onChanged] callback. Most widgets
  /// that use a switch will listen for the [onChanged] callback and rebuild the
  /// switch with a new [value] to update the visual appearance of the switch.
  ///
  /// * [value] determines this switch is on or off.
  /// * [onChanged] is called when the user toggles with switch on or off.
45 46
  Switch({
    Key key,
47 48
    @required this.value,
    @required this.onChanged,
49
    this.activeColor,
50 51
    this.activeThumbImage,
    this.inactiveThumbImage
52
  }) : super(key: key);
53

54
  /// Whether this switch is on or off.
55
  final bool value;
56

57 58 59 60 61 62 63
  /// Called when the user toggles with switch on or off.
  ///
  /// The switch passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the switch with the new
  /// value.
  ///
  /// If null, the switch will be displayed as disabled.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
  ///
  /// The callback provided to onChanged should update the state of the parent
  /// [StatefulWidget] using the [State.setState] method, so that the parent
  /// gets rebuilt; for example:
  ///
  /// ```dart
  /// new Switch(
  ///   value: _giveVerse,
  ///   onChanged: (bool newValue) {
  ///     setState(() {
  ///       _giveVerse = newValue;
  ///     });
  ///   },
  /// ),
  /// ```
79 80
  final ValueChanged<bool> onChanged;

81 82
  /// The color to use when this switch is on.
  ///
83
  /// Defaults to accent color of the current [Theme].
84
  final Color activeColor;
85

86 87
  /// An image to use on the thumb of this switch when the switch is on.
  final ImageProvider activeThumbImage;
88

89 90
  /// An image to use on the thumb of this switch when the switch is off.
  final ImageProvider inactiveThumbImage;
91

92 93 94 95 96 97 98 99 100 101 102 103 104
  @override
  _SwitchState createState() => new _SwitchState();

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('value: ${value ? "on" : "off"}');
    if (onChanged == null)
      description.add('disabled');
  }
}

class _SwitchState extends State<Switch> with TickerProviderStateMixin {
105
  @override
106
  Widget build(BuildContext context) {
107
    assert(debugCheckHasMaterial(context));
108
    final ThemeData themeData = Theme.of(context);
109
    final bool isDark = themeData.brightness == Brightness.dark;
110

111
    final Color activeThumbColor = config.activeColor ?? themeData.accentColor;
112
    final Color activeTrackColor = activeThumbColor.withAlpha(0x80);
113 114 115

    Color inactiveThumbColor;
    Color inactiveTrackColor;
116
    if (config.onChanged != null) {
117 118 119 120 121 122 123
      inactiveThumbColor = isDark ? Colors.grey[400] : Colors.grey[50];
      inactiveTrackColor = isDark ? Colors.white30 : Colors.black26;
    } else {
      inactiveThumbColor = isDark ? Colors.grey[800] : Colors.grey[400];
      inactiveTrackColor = isDark ? Colors.white10 : Colors.black12;
    }

124
    return new _SwitchRenderObjectWidget(
125
      value: config.value,
126 127
      activeColor: activeThumbColor,
      inactiveColor: inactiveThumbColor,
128 129
      activeThumbImage: config.activeThumbImage,
      inactiveThumbImage: config.inactiveThumbImage,
130 131
      activeTrackColor: activeTrackColor,
      inactiveTrackColor: inactiveTrackColor,
132
      configuration: createLocalImageConfiguration(context),
133 134
      onChanged: config.onChanged,
      vsync: this,
135 136 137 138
    );
  }
}

139 140 141 142
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
  _SwitchRenderObjectWidget({
    Key key,
    this.value,
143 144
    this.activeColor,
    this.inactiveColor,
145 146
    this.activeThumbImage,
    this.inactiveThumbImage,
147 148
    this.activeTrackColor,
    this.inactiveTrackColor,
149
    this.configuration,
150 151
    this.onChanged,
    this.vsync,
152
  }) : super(key: key);
153 154

  final bool value;
155 156
  final Color activeColor;
  final Color inactiveColor;
157 158
  final ImageProvider activeThumbImage;
  final ImageProvider inactiveThumbImage;
159 160
  final Color activeTrackColor;
  final Color inactiveTrackColor;
161
  final ImageConfiguration configuration;
Hixie's avatar
Hixie committed
162
  final ValueChanged<bool> onChanged;
163
  final TickerProvider vsync;
164

165
  @override
166
  _RenderSwitch createRenderObject(BuildContext context) => new _RenderSwitch(
167
    value: value,
168 169
    activeColor: activeColor,
    inactiveColor: inactiveColor,
170 171
    activeThumbImage: activeThumbImage,
    inactiveThumbImage: inactiveThumbImage,
172 173
    activeTrackColor: activeTrackColor,
    inactiveTrackColor: inactiveTrackColor,
174
    configuration: configuration,
175 176
    onChanged: onChanged,
    vsync: vsync,
177
  );
178

179
  @override
180
  void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
181 182 183 184
    renderObject
      ..value = value
      ..activeColor = activeColor
      ..inactiveColor = inactiveColor
185 186
      ..activeThumbImage = activeThumbImage
      ..inactiveThumbImage = inactiveThumbImage
187 188
      ..activeTrackColor = activeTrackColor
      ..inactiveTrackColor = inactiveTrackColor
189
      ..configuration = configuration
190 191
      ..onChanged = onChanged
      ..vsync = vsync;
192 193 194
  }
}

195
const double _kTrackHeight = 14.0;
196
const double _kTrackWidth = 33.0;
197 198 199 200 201
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius;

202
class _RenderSwitch extends RenderToggleable {
203 204
  _RenderSwitch({
    bool value,
205 206
    Color activeColor,
    Color inactiveColor,
207 208
    ImageProvider activeThumbImage,
    ImageProvider inactiveThumbImage,
209 210
    Color activeTrackColor,
    Color inactiveTrackColor,
211
    ImageConfiguration configuration,
212 213
    ValueChanged<bool> onChanged,
    @required TickerProvider vsync,
214 215
  }) : _activeThumbImage = activeThumbImage,
       _inactiveThumbImage = inactiveThumbImage,
216 217
       _activeTrackColor = activeTrackColor,
       _inactiveTrackColor = inactiveTrackColor,
218
       _configuration = configuration,
219 220 221 222 223
       super(
         value: value,
         activeColor: activeColor,
         inactiveColor: inactiveColor,
         onChanged: onChanged,
224 225
         size: const Size(_kSwitchWidth, _kSwitchHeight),
         vsync: vsync,
226
       ) {
227
    _drag = new HorizontalDragGestureRecognizer()
228 229 230
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
      ..onEnd = _handleDragEnd;
231 232
  }

233 234 235 236
  ImageProvider get activeThumbImage => _activeThumbImage;
  ImageProvider _activeThumbImage;
  set activeThumbImage(ImageProvider value) {
    if (value == _activeThumbImage)
237
      return;
238
    _activeThumbImage = value;
239 240 241
    markNeedsPaint();
  }

242 243 244 245
  ImageProvider get inactiveThumbImage => _inactiveThumbImage;
  ImageProvider _inactiveThumbImage;
  set inactiveThumbImage(ImageProvider value) {
    if (value == _inactiveThumbImage)
246
      return;
247
    _inactiveThumbImage = value;
248 249 250
    markNeedsPaint();
  }

251 252
  Color get activeTrackColor => _activeTrackColor;
  Color _activeTrackColor;
253
  set activeTrackColor(Color value) {
254 255 256 257 258 259 260 261 262
    assert(value != null);
    if (value == _activeTrackColor)
      return;
    _activeTrackColor = value;
    markNeedsPaint();
  }

  Color get inactiveTrackColor => _inactiveTrackColor;
  Color _inactiveTrackColor;
263
  set inactiveTrackColor(Color value) {
264 265 266 267 268 269 270
    assert(value != null);
    if (value == _inactiveTrackColor)
      return;
    _inactiveTrackColor = value;
    markNeedsPaint();
  }

271 272 273 274 275 276 277 278
  ImageConfiguration get configuration => _configuration;
  ImageConfiguration _configuration;
  set configuration (ImageConfiguration value) {
    assert(value != null);
    if (value == _configuration)
      return;
    _configuration = value;
    markNeedsPaint();
279 280 281 282
  }

  @override
  void detach() {
283 284
    _cachedThumbPainter?.dispose();
    _cachedThumbPainter = null;
285 286 287
    super.detach();
  }

288
  double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
289

290 291
  HorizontalDragGestureRecognizer _drag;

292
  void _handleDragStart(DragStartDetails details) {
293
    if (onChanged != null)
294
      reactionController.forward();
295 296
  }

297
  void _handleDragUpdate(DragUpdateDetails details) {
298
    if (onChanged != null) {
299
      position
300 301
        ..curve = null
        ..reverseCurve = null;
302
      positionController.value += details.primaryDelta / _trackInnerLength;
303 304 305
    }
  }

306
  void _handleDragEnd(DragEndDetails details) {
307 308
    if (position.value >= 0.5)
      positionController.forward();
309
    else
310 311
      positionController.reverse();
    reactionController.reverse();
312 313
  }

314
  @override
Ian Hickson's avatar
Ian Hickson committed
315
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
316
    assert(debugHandleEvent(event, entry));
Ian Hickson's avatar
Ian Hickson committed
317
    if (event is PointerDownEvent && onChanged != null)
318 319
      _drag.addPointer(event);
    super.handleEvent(event, entry);
320 321
  }

322
  Color _cachedThumbColor;
323
  ImageProvider _cachedThumbImage;
324 325
  BoxPainter _cachedThumbPainter;

326
  BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) {
327 328
    return new BoxDecoration(
      backgroundColor: color,
329
      backgroundImage: image == null ? null : new BackgroundImage(image: image),
330
      shape: BoxShape.circle,
331
      boxShadow: kElevationToShadow[1]
332 333
    );
  }
334

335 336 337 338 339 340 341 342 343 344 345
  bool _isPainting = false;

  void _handleDecorationChanged() {
    // If the image decoration is available synchronously, we'll get called here
    // during paint. There's no reason to mark ourselves as needing paint if we
    // are already in the middle of painting. (In fact, doing so would trigger
    // an assert).
    if (!_isPainting)
      markNeedsPaint();
  }

346
  @override
347
  void paint(PaintingContext context, Offset offset) {
Adam Barth's avatar
Adam Barth committed
348
    final Canvas canvas = context.canvas;
349

350
    final bool isActive = onChanged != null;
351
    final double currentPosition = position.value;
352

353
    final Color trackColor = isActive ? Color.lerp(inactiveTrackColor, activeTrackColor, currentPosition) : inactiveTrackColor;
354

355
    // Paint the track
356
    final Paint paint = new Paint()
357
      ..color = trackColor;
358 359
    final double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
    final Rect trackRect = new Rect.fromLTWH(
360 361 362 363 364
      offset.dx + trackHorizontalPadding,
      offset.dy + (size.height - _kTrackHeight) / 2.0,
      size.width - 2.0 * trackHorizontalPadding,
      _kTrackHeight
    );
365
    final RRect trackRRect = new RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
366 367
    canvas.drawRRect(trackRRect, paint);

368
    final Point thumbPosition = new Point(
Adam Barth's avatar
Adam Barth committed
369 370
      kRadialReactionRadius + currentPosition * _trackInnerLength,
      size.height / 2.0
371
    );
372

Adam Barth's avatar
Adam Barth committed
373
    paintRadialReaction(canvas, offset, thumbPosition);
374

375 376 377
    try {
      _isPainting = true;
      BoxPainter thumbPainter;
378
      final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentPosition) : inactiveColor;
379 380
      final ImageProvider thumbImage = isActive ? (currentPosition < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage;
      if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) {
381
        _cachedThumbColor = thumbColor;
382 383
        _cachedThumbImage = thumbImage;
        _cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged);
384 385
      }
      thumbPainter = _cachedThumbPainter;
386

387 388 389 390 391 392 393 394 395 396 397
      // The thumb contracts slightly during the animation
      final double inset = 1.0 - (currentPosition - 0.5).abs() * 2.0;
      final double radius = _kThumbRadius - inset;
      thumbPainter.paint(
        canvas,
        thumbPosition.toOffset() + offset - new Offset(radius, radius),
        configuration.copyWith(size: new Size.fromRadius(radius))
      );
    } finally {
      _isPainting = false;
    }
398 399
  }
}