slider.dart 27.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 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
8 9 10 11
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

12
import 'constants.dart';
13
import 'debug.dart';
14 15
import 'material.dart';
import 'slider_theme.dart';
16 17
import 'theme.dart';

18
/// A Material Design slider.
19
///
20 21 22
/// Used to select from a range of values.
///
/// A slider can be used to select from either a continuous or a discrete set of
23 24
/// values. The default is to use a continuous range of values from [min] to
/// [max]. To use discrete values, use a non-null value for [divisions], which
25
/// indicates the number of discrete intervals. For example, if [min] is 0.0 and
26
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the
27
/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0.
28
///
29 30 31 32 33 34 35 36 37 38 39 40
/// The terms for the parts of a slider are:
///
///  * The "thumb", which is a shape that slides horizontally when the user
///    drags it.
///  * The "rail", which is the line that the slider thumb slides along.
///  * The "value indicator", which is a shape that pops up when the user
///    is dragging the thumb to indicate the value being selected.
///  * The "active" side of the slider is the side between the thumb and the
///    minimum value.
///  * The "inactive" side of the slider is the side between the thumb and the
///    maximum value.
///
41 42 43
/// The slider will be disabled if [onChanged] is null or if the range given by
/// [min]..[max] is empty (i.e. if [min] is equal to [max]).
///
44 45 46 47 48
/// The slider widget itself does not maintain any state. Instead, when the state
/// of the slider changes, the widget calls the [onChanged] callback. Most
/// widgets that use a slider will listen for the [onChanged] callback and
/// rebuild the slider with a new [value] to update the visual appearance of the
/// slider.
49
///
50
/// By default, a slider will be as wide as possible, centered vertically. When
51
/// given unbounded constraints, it will attempt to make the rail 144 pixels
52
/// wide (with margins on each side) and will shrink-wrap vertically.
53
///
54 55
/// Requires one of its ancestors to be a [Material] widget.
///
56 57 58 59
/// Requires one of its ancestors to be a [MediaQuery] widget. Typically, these
/// are introduced by the [MaterialApp] or [WidgetsApp] widget at the top of
/// your application widget tree.
///
60 61 62 63 64 65 66
/// To determine how it should be displayed (e.g. colors, thumb shape, etc.),
/// a slider uses the [SliderThemeData] available from either a [SliderTheme]
/// widget or the [ThemeData.sliderTheme] a [Theme] widget above it in the
/// widget tree. You can also override some of the colors with the [activeColor]
/// and [inactiveColor] properties, although more fine-grained control of the
/// look is achieved using a [SliderThemeData].
///
67
/// See also:
68
///
69 70
///  * [SliderTheme] and [SliderThemeData] for information about controlling
///    the visual appearance of the slider.
71 72
///  * [Radio], for selecting among a set of explicit values.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
73
///  * <https://material.google.com/components/sliders.html>
74
///  * [MediaQuery], from which the text scale factor is obtained.
75
class Slider extends StatefulWidget {
76 77 78
  /// Creates a material design slider.
  ///
  /// The slider itself does not maintain any state. Instead, when the state of
79 80 81 82
  /// the slider changes, the widget calls the [onChanged] callback. Most
  /// widgets that use a slider will listen for the [onChanged] callback and
  /// rebuild the slider with a new [value] to update the visual appearance of
  /// the slider.
83 84 85
  ///
  /// * [value] determines currently selected value for this slider.
  /// * [onChanged] is called when the user selects a new value for the slider.
86 87 88 89
  ///
  /// You can override some of the colors with the [activeColor] and
  /// [inactiveColor] properties, although more fine-grained control of the
  /// appearance is achieved using a [SliderThemeData].
90
  const Slider({
Hixie's avatar
Hixie committed
91
    Key key,
92 93
    @required this.value,
    @required this.onChanged,
Hixie's avatar
Hixie committed
94 95
    this.min: 0.0,
    this.max: 1.0,
96 97
    this.divisions,
    this.label,
98
    this.activeColor,
99
    this.inactiveColor,
100 101 102
  }) : assert(value != null),
       assert(min != null),
       assert(max != null),
103
       assert(min <= max),
104 105 106
       assert(value >= min && value <= max),
       assert(divisions == null || divisions > 0),
       super(key: key);
107

108 109 110
  /// The currently selected value for this slider.
  ///
  /// The slider's thumb is drawn at a position that corresponds to this value.
111
  final double value;
112

113 114 115 116 117 118 119
  /// Called when the user selects a new value for the slider.
  ///
  /// The slider passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the slider with the new
  /// value.
  ///
  /// If null, the slider will be displayed as disabled.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
  ///
  /// 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 Slider(
  ///   value: _duelCommandment.toDouble(),
  ///   min: 1.0,
  ///   max: 10.0,
  ///   divisions: 10,
  ///   label: '$_duelCommandment',
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _duelCommandment = newValue.round();
  ///     });
  ///   },
137
  /// )
138
  /// ```
139 140
  final ValueChanged<double> onChanged;

141
  /// The minimum value the user can select.
142
  ///
143 144 145
  /// Defaults to 0.0. Must be less than or equal to [max].
  ///
  /// If the [max] is equal to the [min], then the slider is disabled.
Hixie's avatar
Hixie committed
146
  final double min;
147 148 149

  /// The maximum value the user can select.
  ///
150 151 152
  /// Defaults to 1.0. Must be greater than or equal to [min].
  ///
  /// If the [max] is equal to the [min], then the slider is disabled.
Hixie's avatar
Hixie committed
153
  final double max;
154

155 156 157 158 159 160 161 162 163
  /// The number of discrete divisions.
  ///
  /// Typically used with [label] to show the current discrete value.
  ///
  /// If null, the slider is continuous.
  final int divisions;

  /// A label to show above the slider when the slider is active.
  ///
164 165 166 167 168 169 170 171 172 173 174 175
  /// It is used to display the value of a discrete slider, and it is displayed
  /// as part of the value indicator shape.
  ///
  /// The label is rendered using the active [ThemeData]'s
  /// [ThemeData.accentTextTheme.body2] text style.
  ///
  /// If null, then the value indicator will not be displayed.
  ///
  /// See also:
  ///
  ///  * [SliderComponentShape] for how to create a custom value indicator
  ///    shape.
176 177
  final String label;

178
  /// The color to use for the portion of the slider rail that is active.
179
  ///
180 181
  /// The "active" side of the slider is the side between the thumb and the
  /// minimum value.
182
  ///
183 184 185 186 187
  /// Defaults to [SliderTheme.activeRailColor] of the current [SliderTheme].
  ///
  /// Using a [SliderTheme] gives much more fine-grained control over the
  /// appearance of various components of the slider.
  final Color activeColor;
188

189
  /// The color for the inactive portion of the slider rail.
190
  ///
191 192
  /// The "inactive" side of the slider is the side between the thumb and the
  /// maximum value.
193
  ///
194 195
  /// Defaults to the [SliderTheme.inactiveRailColor] of the current
  /// [SliderTheme].
196
  ///
197 198 199
  /// Using a [SliderTheme] gives much more fine-grained control over the
  /// appearance of various components of the slider.
  final Color inactiveColor;
200

201 202
  @override
  _SliderState createState() => new _SliderState();
203 204

  @override
205
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
206 207 208 209
    super.debugFillProperties(description);
    description.add(new DoubleProperty('value', value));
    description.add(new DoubleProperty('min', min));
    description.add(new DoubleProperty('max', max));
210
  }
211 212 213
}

class _SliderState extends State<Slider> with TickerProviderStateMixin {
214 215 216
  static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
  static const Duration positionAnimationDuration = const Duration(milliseconds: 75);

217 218
  // Animation controller that is run when interactions occur (taps, drags,
  // etc.).
219
  AnimationController reactionController;
220
  // Animation controller that is run when enabling/disabling the slider.
221
  AnimationController enableController;
222 223
  // Animation controller that is run when transitioning between one value
  // and the next on a discrete slider.
224 225 226 227 228 229
  AnimationController positionController;

  @override
  void initState() {
    super.initState();
    reactionController = new AnimationController(
230 231 232
      duration: kRadialReactionDuration,
      vsync: this,
    );
233 234 235 236 237 238 239 240
    enableController = new AnimationController(
      duration: enableAnimationDuration,
      vsync: this,
    );
    positionController = new AnimationController(
      duration: positionAnimationDuration,
      vsync: this,
    );
241 242
    enableController.value = widget.onChanged != null ? 1.0 : 0.0;
    positionController.value = widget.value;
243 244 245 246 247 248 249 250
  }

  @override
  void dispose() {
    reactionController.dispose();
    enableController.dispose();
    positionController.dispose();
    super.dispose();
251 252
  }

Hixie's avatar
Hixie committed
253
  void _handleChanged(double value) {
254
    assert(widget.onChanged != null);
255 256 257 258
    final double lerpValue = _lerp(value);
    if (lerpValue != widget.value) {
      widget.onChanged(lerpValue);
    }
Hixie's avatar
Hixie committed
259 260
  }

261 262 263 264 265 266
  // Returns a number between min and max, proportional to value, which must
  // be between 0.0 and 1.0.
  double _lerp(double value) {
    assert(value >= 0.0);
    assert(value <= 1.0);
    return value * (widget.max - widget.min) + widget.min;
267 268
  }

269 270 271 272
  // Returns a number between 0.0 and 1.0, given a value between min and max.
  double _unlerp(double value) {
    assert(value <= widget.max);
    assert(value >= widget.min);
273
    return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0;
274
  }
275

276
  @override
277
  Widget build(BuildContext context) {
278
    assert(debugCheckHasMaterial(context));
279
    assert(debugCheckHasMediaQuery(context));
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

    SliderThemeData sliderTheme = SliderTheme.of(context);

    // If the widget has active or inactive colors specified, then we plug them
    // in to the slider theme as best we can. If the developer wants more
    // control than that, then they need to use a SliderTheme.
    if (widget.activeColor != null || widget.inactiveColor != null) {
      sliderTheme = sliderTheme.copyWith(
        activeRailColor: widget.activeColor,
        inactiveRailColor: widget.inactiveColor,
        activeTickMarkColor: widget.inactiveColor,
        inactiveTickMarkColor: widget.activeColor,
        thumbColor: widget.activeColor,
        valueIndicatorColor: widget.activeColor,
        overlayColor: widget.activeColor?.withAlpha(0x29),
      );
    }

298
    return new _SliderRenderObjectWidget(
299
      value: _unlerp(widget.value),
300 301
      divisions: widget.divisions,
      label: widget.label,
302
      sliderTheme: sliderTheme,
303
      mediaQueryData: MediaQuery.of(context),
304
      onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
305
      state: this,
306 307 308 309 310
    );
  }
}

class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
311
  const _SliderRenderObjectWidget({
312 313 314 315
    Key key,
    this.value,
    this.divisions,
    this.label,
316
    this.sliderTheme,
317
    this.mediaQueryData,
318
    this.onChanged,
319
    this.state,
320
  }) : super(key: key);
321 322

  final double value;
323 324
  final int divisions;
  final String label;
325
  final SliderThemeData sliderTheme;
326
  final MediaQueryData mediaQueryData;
327
  final ValueChanged<double> onChanged;
328
  final _SliderState state;
329

330
  @override
331 332 333 334 335
  _RenderSlider createRenderObject(BuildContext context) {
    return new _RenderSlider(
      value: value,
      divisions: divisions,
      label: label,
336 337
      sliderTheme: sliderTheme,
      theme: Theme.of(context),
338
      mediaQueryData: mediaQueryData,
339
      onChanged: onChanged,
340
      state: state,
341
      textDirection: Directionality.of(context),
342 343
    );
  }
344

345
  @override
346
  void updateRenderObject(BuildContext context, _RenderSlider renderObject) {
347 348
    renderObject
      ..value = value
349 350
      ..divisions = divisions
      ..label = label
351 352
      ..sliderTheme = sliderTheme
      ..theme = Theme.of(context)
353
      ..mediaQueryData = mediaQueryData
354 355
      ..onChanged = onChanged
      ..textDirection = Directionality.of(context);
356 357
    // Ticker provider cannot change since there's a 1:1 relationship between
    // the _SliderRenderObjectWidget object and the _SliderState object.
358 359 360
  }
}

361 362 363 364 365
const double _overlayRadius = 16.0;
const double _overlayDiameter = _overlayRadius * 2.0;
const double _railHeight = 2.0;
const double _preferredRailWidth = 144.0;
const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
366

367 368
const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
369

370
class _RenderSlider extends RenderBox {
371
  _RenderSlider({
372
    @required double value,
373 374
    int divisions,
    String label,
375 376
    SliderThemeData sliderTheme,
    ThemeData theme,
377
    MediaQueryData mediaQueryData,
378
    ValueChanged<double> onChanged,
379
    @required _SliderState state,
380
    @required TextDirection textDirection,
381
  }) : assert(value != null && value >= 0.0 && value <= 1.0),
382
       assert(state != null),
383
       assert(textDirection != null),
Ian Hickson's avatar
Ian Hickson committed
384
       _label = label,
385
       _value = value,
386
       _divisions = divisions,
387 388
       _sliderTheme = sliderTheme,
       _theme = theme,
389
       _mediaQueryData = mediaQueryData,
390
       _onChanged = onChanged,
391
       _state = state,
392
       _textDirection = textDirection {
Ian Hickson's avatar
Ian Hickson committed
393
    _updateLabelPainter();
394
    final GestureArenaTeam team = new GestureArenaTeam();
395
    _drag = new HorizontalDragGestureRecognizer()
396
      ..team = team
397 398
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
399 400
      ..onEnd = _handleDragEnd
      ..onCancel = _endInteraction;
401 402
    _tap = new TapGestureRecognizer()
      ..team = team
403
      ..onTapDown = _handleTapDown
404 405
      ..onTapUp = _handleTapUp
      ..onTapCancel = _endInteraction;
406 407 408 409 410 411 412 413
    _reaction = new CurvedAnimation(
      parent: _state.reactionController,
      curve: Curves.fastOutSlowIn,
    );
    _enableAnimation = new CurvedAnimation(
      parent: _state.enableController,
      curve: Curves.easeInOut,
    );
414 415 416 417
  }

  double get value => _value;
  double _value;
418 419 420

  _SliderState _state;

421
  set value(double newValue) {
422
    assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
423 424
    final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
    if (convertedValue == _value) {
425
      return;
426 427 428 429 430 431 432
    }
    _value = convertedValue;
    if (isDiscrete) {
      _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
    } else {
      _state.positionController.value = convertedValue;
    }
433 434 435 436
  }

  int get divisions => _divisions;
  int _divisions;
437
  set divisions(int value) {
438
    if (value == _divisions) {
439
      return;
440
    }
441
    _divisions = value;
442 443 444 445 446
    markNeedsPaint();
  }

  String get label => _label;
  String _label;
447
  set label(String value) {
448
    if (value == _label) {
449
      return;
450
    }
451
    _label = value;
Ian Hickson's avatar
Ian Hickson committed
452
    _updateLabelPainter();
453 454
  }

455 456 457 458
  SliderThemeData get sliderTheme => _sliderTheme;
  SliderThemeData _sliderTheme;
  set sliderTheme(SliderThemeData value) {
    if (value == _sliderTheme) {
459
      return;
460 461
    }
    _sliderTheme = value;
462 463 464
    markNeedsPaint();
  }

465 466 467 468
  ThemeData get theme => _theme;
  ThemeData _theme;
  set theme(ThemeData value) {
    if (value == _theme) {
469
      return;
470 471
    }
    _theme = value;
472 473 474
    markNeedsPaint();
  }

475 476 477 478
  MediaQueryData get mediaQueryData => _mediaQueryData;
  MediaQueryData _mediaQueryData;
  set mediaQueryData(MediaQueryData value) {
    if (value == _mediaQueryData) {
479
      return;
480
    }
481 482 483
    _mediaQueryData = value;
    // Media query data includes the textScaleFactor, so we need to update the
    // label painter.
484 485 486
    _updateLabelPainter();
  }

487 488 489
  ValueChanged<double> get onChanged => _onChanged;
  ValueChanged<double> _onChanged;
  set onChanged(ValueChanged<double> value) {
490
    if (value == _onChanged) {
491
      return;
492
    }
493 494 495
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
496 497 498 499 500
      if (isInteractive) {
        _state.enableController.forward();
      } else {
        _state.enableController.reverse();
      }
501
      markNeedsPaint();
502
      markNeedsSemanticsUpdate();
503 504
    }
  }
505

506 507 508 509
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
510
    if (value == _textDirection) {
511
      return;
512
    }
513
    _textDirection = value;
Ian Hickson's avatar
Ian Hickson committed
514 515 516 517 518 519
    _updateLabelPainter();
  }

  void _updateLabelPainter() {
    if (label != null) {
      _labelPainter
520
        ..text = new TextSpan(style: _theme.accentTextTheme.body2, text: label)
Ian Hickson's avatar
Ian Hickson committed
521
        ..textDirection = textDirection
522
        ..textScaleFactor = _mediaQueryData.textScaleFactor
Ian Hickson's avatar
Ian Hickson committed
523 524 525 526 527 528 529 530
        ..layout();
    } else {
      _labelPainter.text = null;
    }
    // Changing the textDirection can result in the layout changing, because the
    // bidi algorithm might line up the glyphs differently which can result in
    // different ligatures, different shapes, etc. So we always markNeedsLayout.
    markNeedsLayout();
531 532
  }

533
  double get _railLength => size.width - _overlayDiameter;
534

535
  Animation<double> _reaction;
536
  Animation<double> _enableAnimation;
537
  final TextPainter _labelPainter = new TextPainter();
538
  HorizontalDragGestureRecognizer _drag;
539
  TapGestureRecognizer _tap;
540 541 542
  bool _active = false;
  double _currentDragValue = 0.0;

543 544
  bool get isInteractive => onChanged != null;

545 546
  bool get isDiscrete => divisions != null && divisions > 0;

547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _reaction.addListener(markNeedsPaint);
    _enableAnimation.addListener(markNeedsPaint);
    _state.positionController.addListener(markNeedsPaint);
  }

  @override
  void detach() {
    _reaction.removeListener(markNeedsPaint);
    _enableAnimation.removeListener(markNeedsPaint);
    _state.positionController.removeListener(markNeedsPaint);
    super.detach();
  }

563 564 565 566 567 568 569 570 571 572
  double _getValueFromVisualPosition(double visualPosition) {
    switch (textDirection) {
      case TextDirection.rtl:
        return 1.0 - visualPosition;
      case TextDirection.ltr:
        return visualPosition;
    }
    return null;
  }

573
  double _getValueFromGlobalPosition(Offset globalPosition) {
574
    final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
575
    return _getValueFromVisualPosition(visualPosition);
576 577
  }

578 579
  double _discretize(double value) {
    double result = value.clamp(0.0, 1.0);
580
    if (isDiscrete) {
581
      result = (result * divisions).round() / divisions;
582
    }
583 584
    return result;
  }
585

586
  void _startInteraction(Offset globalPosition) {
587
    if (isInteractive) {
588
      _active = true;
589
      _currentDragValue = _getValueFromGlobalPosition(globalPosition);
590
      onChanged(_discretize(_currentDragValue));
591 592 593 594 595 596 597 598 599
      _state.reactionController.forward();
    }
  }

  void _endInteraction() {
    if (_active) {
      _active = false;
      _currentDragValue = 0.0;
      _state.reactionController.reverse();
600 601 602
    }
  }

603 604
  void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);

605
  void _handleDragUpdate(DragUpdateDetails details) {
606
    if (isInteractive) {
607
      final double valueDelta = details.primaryDelta / _railLength;
608 609 610 611 612 613 614 615
      switch (textDirection) {
        case TextDirection.rtl:
          _currentDragValue -= valueDelta;
          break;
        case TextDirection.ltr:
          _currentDragValue += valueDelta;
          break;
      }
616
      onChanged(_discretize(_currentDragValue));
617 618 619
    }
  }

620
  void _handleDragEnd(DragEndDetails details) => _endInteraction();
621

622 623 624
  void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);

  void _handleTapUp(TapUpDetails details) => _endInteraction();
625

626
  @override
627
  bool hitTestSelf(Offset position) => true;
628

629
  @override
Ian Hickson's avatar
Ian Hickson committed
630
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
631
    assert(debugHandleEvent(event, entry));
632 633
    if (event is PointerDownEvent && isInteractive) {
      // We need to add the drag first so that it has priority.
634
      _drag.addPointer(event);
635 636
      _tap.addPointer(event);
    }
637 638
  }

639 640
  @override
  double computeMinIntrinsicWidth(double height) {
641 642 643 644
    return math.max(
      _overlayDiameter,
      _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width,
    );
645 646 647 648 649 650
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    // This doesn't quite match the definition of computeMaxIntrinsicWidth,
    // but it seems within the spirit...
651
    return _preferredTotalWidth;
652 653 654
  }

  @override
655
  double computeMinIntrinsicHeight(double width) => _overlayDiameter;
656 657

  @override
658
  double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
659 660 661 662 663 664 665

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = new Size(
666 667
      constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
      constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter,
668 669 670
    );
  }

671
  void _paintTickMarks(
672 673 674 675 676 677
    Canvas canvas,
    Rect railLeft,
    Rect railRight,
    Paint leftPaint,
    Paint rightPaint,
  ) {
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
    if (isDiscrete) {
      // The ticks are tiny circles that are the same height as the rail.
      const double tickRadius = _railHeight / 2.0;
      final double railWidth = railRight.right - railLeft.left;
      final double dx = (railWidth - _railHeight) / divisions;
      // If the ticks would be too dense, don't bother painting them.
      if (dx >= 3.0 * _railHeight) {
        for (int i = 0; i <= divisions; i += 1) {
          final double left = railLeft.left + i * dx;
          final Offset center = new Offset(left + tickRadius, railLeft.top + tickRadius);
          if (railLeft.contains(center)) {
            canvas.drawCircle(center, tickRadius, leftPaint);
          } else if (railRight.contains(center)) {
            canvas.drawCircle(center, tickRadius, rightPaint);
          }
        }
      }
    }
  }

  void _paintOverlay(Canvas canvas, Offset center) {
    if (!_reaction.isDismissed) {
      // TODO(gspencer) : We don't really follow the spec here for overlays.
      // The spec says to use 16% opacity for drawing over light material,
      // and 32% for colored material, but we don't really have a way to
      // know what the underlying color is, so there's no easy way to
      // implement this. Choosing the "light" version for now.
      final Paint reactionPaint = new Paint()..color = _sliderTheme.overlayColor;
      final double radius = _overlayRadiusTween.evaluate(_reaction);
      canvas.drawCircle(center, radius, reactionPaint);
    }
  }

711
  @override
712 713 714
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

715 716
    final double railLength = size.width - 2 * _overlayRadius;
    final double value = _state.positionController.value;
717 718 719 720 721 722 723 724 725
    final ColorTween activeRailEnableColor = new ColorTween(begin: _sliderTheme.disabledActiveRailColor, end: _sliderTheme.activeRailColor);
    final ColorTween inactiveRailEnableColor = new ColorTween(begin: _sliderTheme.disabledInactiveRailColor, end: _sliderTheme.inactiveRailColor);
    final ColorTween activeTickMarkEnableColor = new ColorTween(begin: _sliderTheme.disabledActiveTickMarkColor, end: _sliderTheme.activeTickMarkColor);
    final ColorTween inactiveTickMarkEnableColor = new ColorTween(begin: _sliderTheme.disabledInactiveTickMarkColor, end: _sliderTheme.inactiveTickMarkColor);

    final Paint activeRailPaint = new Paint()..color = activeRailEnableColor.evaluate(_enableAnimation);
    final Paint inactiveRailPaint = new Paint()..color = inactiveRailEnableColor.evaluate(_enableAnimation);
    final Paint activeTickMarkPaint = new Paint()..color = activeTickMarkEnableColor.evaluate(_enableAnimation);
    final Paint inactiveTickMarkPaint = new Paint()..color = inactiveTickMarkEnableColor.evaluate(_enableAnimation);
726 727

    double visualPosition;
728 729 730 731
    Paint leftRailPaint;
    Paint rightRailPaint;
    Paint leftTickMarkPaint;
    Paint rightTickMarkPaint;
732 733 734
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - value;
735 736 737 738
        leftRailPaint = inactiveRailPaint;
        rightRailPaint = activeRailPaint;
        leftTickMarkPaint = inactiveTickMarkPaint;
        rightTickMarkPaint = activeTickMarkPaint;
739 740 741
        break;
      case TextDirection.ltr:
        visualPosition = value;
742 743 744 745
        leftRailPaint = activeRailPaint;
        rightRailPaint = inactiveRailPaint;
        leftTickMarkPaint = activeTickMarkPaint;
        rightTickMarkPaint = inactiveTickMarkPaint;
746 747
        break;
    }
748

749 750 751 752 753 754 755 756 757
    const double railRadius = _railHeight / 2.0;
    const double thumbGap = 2.0;

    final double railVerticalCenter = offset.dy + (size.height) / 2.0;
    final double railLeft = offset.dx + _overlayRadius;
    final double railTop = railVerticalCenter - railRadius;
    final double railBottom = railVerticalCenter + railRadius;
    final double railRight = railLeft + railLength;
    final double railActive = railLeft + railLength * visualPosition;
758 759 760
    final double thumbRadius = _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width / 2.0;
    final double railActiveLeft = math.max(0.0, railActive - thumbRadius - thumbGap * (1.0 - _enableAnimation.value));
    final double railActiveRight = math.min(railActive + thumbRadius + thumbGap * (1.0 - _enableAnimation.value), railRight);
761 762 763 764 765 766 767 768 769 770 771
    final Rect railLeftRect = new Rect.fromLTRB(railLeft, railTop, railActiveLeft, railBottom);
    final Rect railRightRect = new Rect.fromLTRB(railActiveRight, railTop, railRight, railBottom);

    final Offset thumbCenter = new Offset(railActive, railVerticalCenter);

    // Paint the rail.
    if (visualPosition > 0.0) {
      canvas.drawRect(railLeftRect, leftRailPaint);
    }
    if (visualPosition < 1.0) {
      canvas.drawRect(railRightRect, rightRailPaint);
772
    }
773

774
    _paintOverlay(canvas, thumbCenter);
775

776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
    _paintTickMarks(
      canvas,
      railLeftRect,
      railRightRect,
      leftTickMarkPaint,
      rightTickMarkPaint,
    );

    if (isInteractive && _reaction.status != AnimationStatus.dismissed && label != null) {
      bool showValueIndicator;
      switch (_sliderTheme.showValueIndicator) {
        case ShowValueIndicator.onlyForDiscrete:
          showValueIndicator = isDiscrete;
          break;
        case ShowValueIndicator.onlyForContinuous:
          showValueIndicator = !isDiscrete;
          break;
        case ShowValueIndicator.always:
          showValueIndicator = true;
          break;
        case ShowValueIndicator.never:
          showValueIndicator = false;
          break;
      }
      if (showValueIndicator) {
        _sliderTheme.valueIndicatorShape.paint(
802
          this,
803 804 805 806 807 808 809 810 811
          context,
          isDiscrete,
          thumbCenter,
          _reaction,
          _enableAnimation,
          _labelPainter,
          _sliderTheme,
          _textDirection,
          value,
812 813
        );
      }
814
    }
Adam Barth's avatar
Adam Barth committed
815

816
    _sliderTheme.thumbShape.paint(
817
      this,
818 819 820 821 822 823 824 825 826 827
      context,
      isDiscrete,
      thumbCenter,
      _reaction,
      _enableAnimation,
      label != null ? _labelPainter : null,
      _sliderTheme,
      _textDirection,
      value,
    );
828
  }
829 830

  @override
831 832
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
833

834 835
    config.isSemanticBoundary = isInteractive;
    if (isInteractive) {
836 837
      config.onIncrease = _increaseAction;
      config.onDecrease = _decreaseAction;
838 839 840
    }
  }

841
  double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
842

843
  void _increaseAction() {
844
    if (isInteractive) {
845
      onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
846
    }
847 848
  }

849
  void _decreaseAction() {
850
    if (isInteractive) {
851
      onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
852
    }
853
  }
854
}