1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
// 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'constants.dart';
import 'debug.dart';
import 'shadows.dart';
import 'theme.dart';
import 'toggleable.dart';
/// 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:
///
/// * [SwitchListTile], which combines this widget with a [ListTile] so that
/// you can give the switch a label.
/// * [Checkbox], another widget with similar semantics.
/// * [Radio], for selecting among a set of explicit values.
/// * [Slider], for selecting a value in a range.
/// * <https://material.google.com/components/selection-controls.html#selection-controls-switch>
class Switch extends StatefulWidget {
/// 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.
///
/// The following arguments are required:
///
/// * [value] determines whether this switch is on or off.
/// * [onChanged] is called when the user toggles the switch on or off.
const Switch({
Key key,
@required this.value,
@required this.onChanged,
this.activeColor,
this.activeTrackColor,
this.inactiveThumbColor,
this.inactiveTrackColor,
this.activeThumbImage,
this.inactiveThumbImage
}) : super(key: key);
/// Whether this switch is on or off.
///
/// This property must not be null.
final bool value;
/// Called when the user toggles the 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.
///
/// 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;
/// });
/// },
/// )
/// ```
final ValueChanged<bool> onChanged;
/// The color to use when this switch is on.
///
/// Defaults to accent color of the current [Theme].
final Color activeColor;
/// The color to use on the track when this switch is on.
///
/// Defaults to accent color of the current [Theme] with the opacity set at 50%.
final Color activeTrackColor;
/// The color to use on the thumb when this switch is off.
///
/// Defaults to the colors described in the Material design specification.
final Color inactiveThumbColor;
/// The color to use on the track when this switch is off.
///
/// Defaults to the colors described in the Material design specification.
final Color inactiveTrackColor;
/// An image to use on the thumb of this switch when the switch is on.
final ImageProvider activeThumbImage;
/// An image to use on the thumb of this switch when the switch is off.
final ImageProvider inactiveThumbImage;
@override
_SwitchState createState() => new _SwitchState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
description.add(new ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
}
}
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
final ThemeData themeData = Theme.of(context);
final bool isDark = themeData.brightness == Brightness.dark;
final Color activeThumbColor = widget.activeColor ?? themeData.accentColor;
final Color activeTrackColor = widget.activeTrackColor ?? activeThumbColor.withAlpha(0x80);
Color inactiveThumbColor;
Color inactiveTrackColor;
if (widget.onChanged != null) {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade400 : Colors.grey.shade50);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white30 : Colors.black26);
} else {
inactiveThumbColor = widget.inactiveThumbColor ?? (isDark ? Colors.grey.shade800 : Colors.grey.shade400);
inactiveTrackColor = widget.inactiveTrackColor ?? (isDark ? Colors.white10 : Colors.black12);
}
return new _SwitchRenderObjectWidget(
value: widget.value,
activeColor: activeThumbColor,
inactiveColor: inactiveThumbColor,
activeThumbImage: widget.activeThumbImage,
inactiveThumbImage: widget.inactiveThumbImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: createLocalImageConfiguration(context),
onChanged: widget.onChanged,
vsync: this,
);
}
}
class _SwitchRenderObjectWidget extends LeafRenderObjectWidget {
const _SwitchRenderObjectWidget({
Key key,
this.value,
this.activeColor,
this.inactiveColor,
this.activeThumbImage,
this.inactiveThumbImage,
this.activeTrackColor,
this.inactiveTrackColor,
this.configuration,
this.onChanged,
this.vsync,
}) : super(key: key);
final bool value;
final Color activeColor;
final Color inactiveColor;
final ImageProvider activeThumbImage;
final ImageProvider inactiveThumbImage;
final Color activeTrackColor;
final Color inactiveTrackColor;
final ImageConfiguration configuration;
final ValueChanged<bool> onChanged;
final TickerProvider vsync;
@override
_RenderSwitch createRenderObject(BuildContext context) {
return new _RenderSwitch(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
activeThumbImage: activeThumbImage,
inactiveThumbImage: inactiveThumbImage,
activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor,
configuration: configuration,
onChanged: onChanged,
textDirection: Directionality.of(context),
vsync: vsync,
);
}
@override
void updateRenderObject(BuildContext context, _RenderSwitch renderObject) {
renderObject
..value = value
..activeColor = activeColor
..inactiveColor = inactiveColor
..activeThumbImage = activeThumbImage
..inactiveThumbImage = inactiveThumbImage
..activeTrackColor = activeTrackColor
..inactiveTrackColor = inactiveTrackColor
..configuration = configuration
..onChanged = onChanged
..textDirection = Directionality.of(context)
..vsync = vsync;
}
}
const double _kTrackHeight = 14.0;
const double _kTrackWidth = 33.0;
const double _kTrackRadius = _kTrackHeight / 2.0;
const double _kThumbRadius = 10.0;
const double _kSwitchWidth = _kTrackWidth - 2 * _kTrackRadius + 2 * kRadialReactionRadius;
const double _kSwitchHeight = 2 * kRadialReactionRadius;
class _RenderSwitch extends RenderToggleable {
_RenderSwitch({
bool value,
Color activeColor,
Color inactiveColor,
ImageProvider activeThumbImage,
ImageProvider inactiveThumbImage,
Color activeTrackColor,
Color inactiveTrackColor,
ImageConfiguration configuration,
@required TextDirection textDirection,
ValueChanged<bool> onChanged,
@required TickerProvider vsync,
}) : assert(textDirection != null),
_activeThumbImage = activeThumbImage,
_inactiveThumbImage = inactiveThumbImage,
_activeTrackColor = activeTrackColor,
_inactiveTrackColor = inactiveTrackColor,
_configuration = configuration,
_textDirection = textDirection,
super(
value: value,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
size: const Size(_kSwitchWidth, _kSwitchHeight),
vsync: vsync,
) {
_drag = new HorizontalDragGestureRecognizer()
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
}
ImageProvider get activeThumbImage => _activeThumbImage;
ImageProvider _activeThumbImage;
set activeThumbImage(ImageProvider value) {
if (value == _activeThumbImage)
return;
_activeThumbImage = value;
markNeedsPaint();
}
ImageProvider get inactiveThumbImage => _inactiveThumbImage;
ImageProvider _inactiveThumbImage;
set inactiveThumbImage(ImageProvider value) {
if (value == _inactiveThumbImage)
return;
_inactiveThumbImage = value;
markNeedsPaint();
}
Color get activeTrackColor => _activeTrackColor;
Color _activeTrackColor;
set activeTrackColor(Color value) {
assert(value != null);
if (value == _activeTrackColor)
return;
_activeTrackColor = value;
markNeedsPaint();
}
Color get inactiveTrackColor => _inactiveTrackColor;
Color _inactiveTrackColor;
set inactiveTrackColor(Color value) {
assert(value != null);
if (value == _inactiveTrackColor)
return;
_inactiveTrackColor = value;
markNeedsPaint();
}
ImageConfiguration get configuration => _configuration;
ImageConfiguration _configuration;
set configuration(ImageConfiguration value) {
assert(value != null);
if (value == _configuration)
return;
_configuration = value;
markNeedsPaint();
}
TextDirection get textDirection => _textDirection;
TextDirection _textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textDirection == value)
return;
_textDirection = value;
markNeedsPaint();
}
@override
void detach() {
_cachedThumbPainter?.dispose();
_cachedThumbPainter = null;
super.detach();
}
double get _trackInnerLength => size.width - 2.0 * kRadialReactionRadius;
HorizontalDragGestureRecognizer _drag;
void _handleDragStart(DragStartDetails details) {
if (isInteractive)
reactionController.forward();
}
void _handleDragUpdate(DragUpdateDetails details) {
if (isInteractive) {
position
..curve = null
..reverseCurve = null;
final double delta = details.primaryDelta / _trackInnerLength;
switch (textDirection) {
case TextDirection.rtl:
positionController.value -= delta;
break;
case TextDirection.ltr:
positionController.value += delta;
break;
}
}
}
void _handleDragEnd(DragEndDetails details) {
if (position.value >= 0.5)
positionController.forward();
else
positionController.reverse();
reactionController.reverse();
}
@override
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent && onChanged != null)
_drag.addPointer(event);
super.handleEvent(event, entry);
}
Color _cachedThumbColor;
ImageProvider _cachedThumbImage;
BoxPainter _cachedThumbPainter;
BoxDecoration _createDefaultThumbDecoration(Color color, ImageProvider image) {
return new BoxDecoration(
color: color,
image: image == null ? null : new DecorationImage(image: image),
shape: BoxShape.circle,
boxShadow: kElevationToShadow[1]
);
}
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();
}
@override
void paint(PaintingContext context, Offset offset) {
final Canvas canvas = context.canvas;
final bool isActive = onChanged != null;
final double currentValue = position.value;
double visualPosition;
switch (textDirection) {
case TextDirection.rtl:
visualPosition = 1.0 - currentValue;
break;
case TextDirection.ltr:
visualPosition = currentValue;
break;
}
final Color trackColor = isActive ? Color.lerp(inactiveTrackColor, activeTrackColor, currentValue) : inactiveTrackColor;
// Paint the track
final Paint paint = new Paint()
..color = trackColor;
final double trackHorizontalPadding = kRadialReactionRadius - _kTrackRadius;
final Rect trackRect = new Rect.fromLTWH(
offset.dx + trackHorizontalPadding,
offset.dy + (size.height - _kTrackHeight) / 2.0,
size.width - 2.0 * trackHorizontalPadding,
_kTrackHeight
);
final RRect trackRRect = new RRect.fromRectAndRadius(trackRect, const Radius.circular(_kTrackRadius));
canvas.drawRRect(trackRRect, paint);
final Offset thumbPosition = new Offset(
kRadialReactionRadius + visualPosition * _trackInnerLength,
size.height / 2.0
);
paintRadialReaction(canvas, offset, thumbPosition);
try {
_isPainting = true;
BoxPainter thumbPainter;
final Color thumbColor = isActive ? Color.lerp(inactiveColor, activeColor, currentValue) : inactiveColor;
final ImageProvider thumbImage = isActive ? (currentValue < 0.5 ? inactiveThumbImage : activeThumbImage) : inactiveThumbImage;
if (_cachedThumbPainter == null || thumbColor != _cachedThumbColor || thumbImage != _cachedThumbImage) {
_cachedThumbColor = thumbColor;
_cachedThumbImage = thumbImage;
_cachedThumbPainter = _createDefaultThumbDecoration(thumbColor, thumbImage).createBoxPainter(_handleDecorationChanged);
}
thumbPainter = _cachedThumbPainter;
// The thumb contracts slightly during the animation
final double inset = 1.0 - (currentValue - 0.5).abs() * 2.0;
final double radius = _kThumbRadius - inset;
thumbPainter.paint(
canvas,
thumbPosition + offset - new Offset(radius, radius),
configuration.copyWith(size: new Size.fromRadius(radius))
);
} finally {
_isPainting = false;
}
}
}