slider.dart 26.9 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 60 61 62
/// 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].
///
63
/// See also:
64
///
65 66
///  * [SliderTheme] and [SliderThemeData] for information about controlling
///    the visual appearance of the slider.
67 68
///  * [Radio], for selecting among a set of explicit values.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
69
///  * <https://material.google.com/components/sliders.html>
70
class Slider extends StatefulWidget {
71 72 73 74 75 76 77 78 79
  /// Creates a material design slider.
  ///
  /// The slider 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.
  ///
  /// * [value] determines currently selected value for this slider.
  /// * [onChanged] is called when the user selects a new value for the slider.
80 81 82 83
  ///
  /// 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].
84
  const Slider({
Hixie's avatar
Hixie committed
85
    Key key,
86 87
    @required this.value,
    @required this.onChanged,
Hixie's avatar
Hixie committed
88 89
    this.min: 0.0,
    this.max: 1.0,
90 91
    this.divisions,
    this.label,
92
    this.activeColor,
93
    this.inactiveColor,
94 95 96
  }) : assert(value != null),
       assert(min != null),
       assert(max != null),
97
       assert(min <= max),
98 99 100
       assert(value >= min && value <= max),
       assert(divisions == null || divisions > 0),
       super(key: key);
101

102 103 104
  /// The currently selected value for this slider.
  ///
  /// The slider's thumb is drawn at a position that corresponds to this value.
105
  final double value;
106

107 108 109 110 111 112 113
  /// 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.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
  ///
  /// 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();
  ///     });
  ///   },
131
  /// )
132
  /// ```
133 134
  final ValueChanged<double> onChanged;

135
  /// The minimum value the user can select.
136
  ///
137 138 139
  /// 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
140
  final double min;
141 142 143

  /// The maximum value the user can select.
  ///
144 145 146
  /// 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
147
  final double max;
148

149 150 151 152 153 154 155 156 157
  /// 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.
  ///
158 159 160 161 162 163 164 165 166 167 168 169
  /// 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.
170 171
  final String label;

172
  /// The color to use for the portion of the slider rail that is active.
173
  ///
174 175
  /// The "active" side of the slider is the side between the thumb and the
  /// minimum value.
176
  ///
177 178 179 180 181
  /// 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;
182

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

195 196
  @override
  _SliderState createState() => new _SliderState();
197 198

  @override
199
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
200 201 202 203
    super.debugFillProperties(description);
    description.add(new DoubleProperty('value', value));
    description.add(new DoubleProperty('min', min));
    description.add(new DoubleProperty('max', max));
204
  }
205 206 207
}

class _SliderState extends State<Slider> with TickerProviderStateMixin {
208 209 210 211 212 213 214 215 216 217 218
  static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
  static const Duration positionAnimationDuration = const Duration(milliseconds: 75);

  AnimationController reactionController;
  AnimationController enableController;
  AnimationController positionController;

  @override
  void initState() {
    super.initState();
    reactionController = new AnimationController(
219 220 221
      duration: kRadialReactionDuration,
      vsync: this,
    );
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
    enableController = new AnimationController(
      duration: enableAnimationDuration,
      vsync: this,
    );
    positionController = new AnimationController(
      duration: positionAnimationDuration,
      vsync: this,
    );
  }

  @override
  void dispose() {
    reactionController.dispose();
    enableController.dispose();
    positionController.dispose();
    super.dispose();
238 239
  }

Hixie's avatar
Hixie committed
240
  void _handleChanged(double value) {
241
    assert(widget.onChanged != null);
242 243 244 245
    final double lerpValue = _lerp(value);
    if (lerpValue != widget.value) {
      widget.onChanged(lerpValue);
    }
Hixie's avatar
Hixie committed
246 247
  }

248 249 250 251 252 253
  // 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;
254 255
  }

256 257 258 259
  // 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);
260
    return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0;
261
  }
262

263
  @override
264
  Widget build(BuildContext context) {
265
    assert(debugCheckHasMaterial(context));
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283

    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),
      );
    }

284
    return new _SliderRenderObjectWidget(
285
      value: _unlerp(widget.value),
286 287
      divisions: widget.divisions,
      label: widget.label,
288
      sliderTheme: sliderTheme,
289
      textScaleFactor: MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
290
      onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
291
      state: this,
292 293 294 295 296
    );
  }
}

class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
297
  const _SliderRenderObjectWidget({
298 299 300 301
    Key key,
    this.value,
    this.divisions,
    this.label,
302
    this.sliderTheme,
303
    this.textScaleFactor,
304
    this.onChanged,
305
    this.state,
306
  }) : super(key: key);
307 308

  final double value;
309 310
  final int divisions;
  final String label;
311
  final SliderThemeData sliderTheme;
312
  final double textScaleFactor;
313
  final ValueChanged<double> onChanged;
314
  final _SliderState state;
315

316
  @override
317 318 319 320 321
  _RenderSlider createRenderObject(BuildContext context) {
    return new _RenderSlider(
      value: value,
      divisions: divisions,
      label: label,
322 323
      sliderTheme: sliderTheme,
      theme: Theme.of(context),
324
      textScaleFactor: textScaleFactor,
325
      onChanged: onChanged,
326
      state: state,
327
      textDirection: Directionality.of(context),
328 329
    );
  }
330

331
  @override
332
  void updateRenderObject(BuildContext context, _RenderSlider renderObject) {
333 334
    renderObject
      ..value = value
335 336
      ..divisions = divisions
      ..label = label
337 338
      ..sliderTheme = sliderTheme
      ..theme = Theme.of(context)
339
      ..textScaleFactor = textScaleFactor
340 341
      ..onChanged = onChanged
      ..textDirection = Directionality.of(context);
342 343
    // Ticker provider cannot change since there's a 1:1 relationship between
    // the _SliderRenderObjectWidget object and the _SliderState object.
344 345 346
  }
}

347 348 349 350 351
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;
352

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

356
class _RenderSlider extends RenderBox {
357
  _RenderSlider({
358
    @required double value,
359 360
    int divisions,
    String label,
361 362
    SliderThemeData sliderTheme,
    ThemeData theme,
363
    double textScaleFactor,
364
    ValueChanged<double> onChanged,
365
    @required _SliderState state,
366
    @required TextDirection textDirection,
367
  }) : assert(value != null && value >= 0.0 && value <= 1.0),
368
       assert(state != null),
369
       assert(textDirection != null),
Ian Hickson's avatar
Ian Hickson committed
370
       _label = label,
371
       _value = value,
372
       _divisions = divisions,
373 374
       _sliderTheme = sliderTheme,
       _theme = theme,
375
       _textScaleFactor = textScaleFactor,
376
       _onChanged = onChanged,
377
       _state = state,
378
       _textDirection = textDirection {
Ian Hickson's avatar
Ian Hickson committed
379
    _updateLabelPainter();
380
    final GestureArenaTeam team = new GestureArenaTeam();
381
    _drag = new HorizontalDragGestureRecognizer()
382
      ..team = team
383 384
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
385 386
      ..onEnd = _handleDragEnd
      ..onCancel = _endInteraction;
387 388
    _tap = new TapGestureRecognizer()
      ..team = team
389
      ..onTapDown = _handleTapDown
390 391
      ..onTapUp = _handleTapUp
      ..onTapCancel = _endInteraction;
392 393 394 395 396 397
    _reaction = new CurvedAnimation(parent: state.reactionController, curve: Curves.fastOutSlowIn)
      ..addListener(markNeedsPaint);
    state.enableController.value = isInteractive ? 1.0 : 0.0;
    _enableAnimation = new CurvedAnimation(parent: state.enableController, curve: Curves.easeInOut)
      ..addListener(markNeedsPaint);
    state.positionController.value = _value;
398 399 400 401
  }

  double get value => _value;
  double _value;
402 403 404

  _SliderState _state;

405
  set value(double newValue) {
406
    assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
407 408
    final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
    if (convertedValue == _value) {
409
      return;
410 411 412 413 414 415 416
    }
    _value = convertedValue;
    if (isDiscrete) {
      _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
    } else {
      _state.positionController.value = convertedValue;
    }
417 418 419 420
  }

  int get divisions => _divisions;
  int _divisions;
421

422
  set divisions(int value) {
423
    if (value == _divisions) {
424
      return;
425
    }
426
    _divisions = value;
427 428 429 430 431
    markNeedsPaint();
  }

  String get label => _label;
  String _label;
432

433
  set label(String value) {
434
    if (value == _label) {
435
      return;
436
    }
437
    _label = value;
Ian Hickson's avatar
Ian Hickson committed
438
    _updateLabelPainter();
439 440
  }

441 442
  SliderThemeData get sliderTheme => _sliderTheme;
  SliderThemeData _sliderTheme;
443

444 445
  set sliderTheme(SliderThemeData value) {
    if (value == _sliderTheme) {
446
      return;
447 448
    }
    _sliderTheme = value;
449 450 451
    markNeedsPaint();
  }

452 453
  ThemeData get theme => _theme;
  ThemeData _theme;
454

455 456
  set theme(ThemeData value) {
    if (value == _theme) {
457
      return;
458 459
    }
    _theme = value;
460 461 462
    markNeedsPaint();
  }

463 464
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
465

466
  set textScaleFactor(double value) {
467
    if (value == _textScaleFactor) {
468
      return;
469
    }
470 471 472 473
    _textScaleFactor = value;
    _updateLabelPainter();
  }

474 475
  ValueChanged<double> get onChanged => _onChanged;
  ValueChanged<double> _onChanged;
476

477
  set onChanged(ValueChanged<double> value) {
478
    if (value == _onChanged) {
479
      return;
480
    }
481 482 483
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
484 485 486 487 488
      if (isInteractive) {
        _state.enableController.forward();
      } else {
        _state.enableController.reverse();
      }
489
      markNeedsPaint();
490
      markNeedsSemanticsUpdate();
491 492
    }
  }
493

494 495
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
496

497 498
  set textDirection(TextDirection value) {
    assert(value != null);
499
    if (value == _textDirection) {
500
      return;
501
    }
502
    _textDirection = value;
Ian Hickson's avatar
Ian Hickson committed
503 504 505 506 507
    _updateLabelPainter();
  }

  void _updateLabelPainter() {
    if (label != null) {
508 509 510
      // We have to account for the text scale factor in the supplied theme.
      final TextStyle style = _theme.accentTextTheme.body2
          .copyWith(fontSize: _theme.accentTextTheme.body2.fontSize * _textScaleFactor);
Ian Hickson's avatar
Ian Hickson committed
511
      _labelPainter
512
        ..text = new TextSpan(style: style, text: label)
Ian Hickson's avatar
Ian Hickson committed
513 514 515 516 517 518 519 520 521
        ..textDirection = textDirection
        ..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();
522 523
  }

524
  double get _railLength => size.width - _overlayDiameter;
525

526
  Animation<double> _reaction;
527
  Animation<double> _enableAnimation;
528
  final TextPainter _labelPainter = new TextPainter();
529
  HorizontalDragGestureRecognizer _drag;
530
  TapGestureRecognizer _tap;
531 532 533
  bool _active = false;
  double _currentDragValue = 0.0;

534 535
  bool get isInteractive => onChanged != null;

536 537
  bool get isDiscrete => divisions != null && divisions > 0;

538 539 540 541 542 543 544 545 546 547
  double _getValueFromVisualPosition(double visualPosition) {
    switch (textDirection) {
      case TextDirection.rtl:
        return 1.0 - visualPosition;
      case TextDirection.ltr:
        return visualPosition;
    }
    return null;
  }

548
  double _getValueFromGlobalPosition(Offset globalPosition) {
549
    final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
550
    return _getValueFromVisualPosition(visualPosition);
551 552
  }

553 554
  double _discretize(double value) {
    double result = value.clamp(0.0, 1.0);
555
    if (isDiscrete) {
556
      result = (result * divisions).round() / divisions;
557
    }
558 559
    return result;
  }
560

561
  void _startInteraction(Offset globalPosition) {
562
    if (isInteractive) {
563
      _active = true;
564
      _currentDragValue = _getValueFromGlobalPosition(globalPosition);
565
      onChanged(_discretize(_currentDragValue));
566 567 568 569 570 571 572 573 574
      _state.reactionController.forward();
    }
  }

  void _endInteraction() {
    if (_active) {
      _active = false;
      _currentDragValue = 0.0;
      _state.reactionController.reverse();
575 576 577
    }
  }

578 579
  void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);

580
  void _handleDragUpdate(DragUpdateDetails details) {
581
    if (isInteractive) {
582
      final double valueDelta = details.primaryDelta / _railLength;
583 584 585 586 587 588 589 590
      switch (textDirection) {
        case TextDirection.rtl:
          _currentDragValue -= valueDelta;
          break;
        case TextDirection.ltr:
          _currentDragValue += valueDelta;
          break;
      }
591
      onChanged(_discretize(_currentDragValue));
592
      markNeedsPaint();
593 594 595
    }
  }

596
  void _handleDragEnd(DragEndDetails details) => _endInteraction();
597

598 599 600
  void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);

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

602
  @override
603
  bool hitTestSelf(Offset position) => true;
604

605
  @override
Ian Hickson's avatar
Ian Hickson committed
606
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
607
    assert(debugHandleEvent(event, entry));
608 609
    if (event is PointerDownEvent && isInteractive) {
      // We need to add the drag first so that it has priority.
610
      _drag.addPointer(event);
611 612
      _tap.addPointer(event);
    }
613 614
  }

615 616
  @override
  double computeMinIntrinsicWidth(double height) {
617 618
    return math.max(_overlayDiameter,
        _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width);
619 620 621 622 623 624
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    // This doesn't quite match the definition of computeMaxIntrinsicWidth,
    // but it seems within the spirit...
625
    return _preferredTotalWidth;
626 627 628
  }

  @override
629
  double computeMinIntrinsicHeight(double width) => _overlayDiameter;
630 631

  @override
632
  double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
633 634 635 636 637 638 639

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = new Size(
640 641
      constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
      constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter,
642 643 644
    );
  }

645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
  void _paintTickMarks(
      Canvas canvas, Rect railLeft, Rect railRight, Paint leftPaint, Paint rightPaint) {
    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);
    }
  }

680
  @override
681 682 683
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
    final double railLength = size.width - 2 * _overlayRadius;
    final double value = _state.positionController.value;
    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);
703 704

    double visualPosition;
705 706 707 708
    Paint leftRailPaint;
    Paint rightRailPaint;
    Paint leftTickMarkPaint;
    Paint rightTickMarkPaint;
709 710 711
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - value;
712 713 714 715
        leftRailPaint = inactiveRailPaint;
        rightRailPaint = activeRailPaint;
        leftTickMarkPaint = inactiveTickMarkPaint;
        rightTickMarkPaint = activeTickMarkPaint;
716 717 718
        break;
      case TextDirection.ltr:
        visualPosition = value;
719 720 721 722
        leftRailPaint = activeRailPaint;
        rightRailPaint = inactiveRailPaint;
        leftTickMarkPaint = activeTickMarkPaint;
        rightTickMarkPaint = inactiveTickMarkPaint;
723 724
        break;
    }
725

726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
    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;
    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);
    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);
752
    }
753

754
    _paintOverlay(canvas, thumbCenter);
755

756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
    _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(
          context,
          isDiscrete,
          thumbCenter,
          _reaction,
          _enableAnimation,
          _labelPainter,
          _sliderTheme,
          _textDirection,
          _textScaleFactor,
          value,
792 793
        );
      }
794
    }
Adam Barth's avatar
Adam Barth committed
795

796 797 798 799 800 801 802 803 804 805 806 807
    _sliderTheme.thumbShape.paint(
      context,
      isDiscrete,
      thumbCenter,
      _reaction,
      _enableAnimation,
      label != null ? _labelPainter : null,
      _sliderTheme,
      _textDirection,
      _textScaleFactor,
      value,
    );
808
  }
809 810

  @override
811 812
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
813

814 815
    config.isSemanticBoundary = isInteractive;
    if (isInteractive) {
816 817
      config.onIncrease = _increaseAction;
      config.onDecrease = _decreaseAction;
818 819 820
    }
  }

821
  double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
822

823
  void _increaseAction() {
824
    if (isInteractive) {
825
      onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
826
    }
827 828
  }

829
  void _decreaseAction() {
830
    if (isInteractive) {
831
      onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
832
    }
833
  }
834
}