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

8
import 'package:flutter/foundation.dart';
9 10
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
11
import 'package:flutter/scheduler.dart' show timeDilation;
12 13
import 'package:flutter/widgets.dart';

14
import 'constants.dart';
15
import 'debug.dart';
16 17
import 'material.dart';
import 'slider_theme.dart';
18 19
import 'theme.dart';

20
/// A Material Design slider.
21
///
22 23 24
/// Used to select from a range of values.
///
/// A slider can be used to select from either a continuous or a discrete set of
25 26
/// 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
27
/// indicates the number of discrete intervals. For example, if [min] is 0.0 and
28
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the
29
/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0.
30
///
31 32 33 34 35 36 37 38 39 40 41 42
/// 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.
///
43 44 45
/// 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]).
///
46 47 48 49 50
/// 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.
51
///
52
/// By default, a slider will be as wide as possible, centered vertically. When
53
/// given unbounded constraints, it will attempt to make the rail 144 pixels
54
/// wide (with margins on each side) and will shrink-wrap vertically.
55
///
56 57
/// Requires one of its ancestors to be a [Material] widget.
///
58 59 60 61
/// 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.
///
62 63 64 65 66 67 68
/// 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].
///
69
/// See also:
70
///
71 72
///  * [SliderTheme] and [SliderThemeData] for information about controlling
///    the visual appearance of the slider.
73 74
///  * [Radio], for selecting among a set of explicit values.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
75
///  * <https://material.google.com/components/sliders.html>
76
///  * [MediaQuery], from which the text scale factor is obtained.
77
class Slider extends StatefulWidget {
78 79 80
  /// Creates a material design slider.
  ///
  /// The slider itself does not maintain any state. Instead, when the state of
81 82 83 84
  /// 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.
85 86 87
  ///
  /// * [value] determines currently selected value for this slider.
  /// * [onChanged] is called when the user selects a new value for the slider.
88 89 90 91
  ///
  /// 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].
92
  const Slider({
Hixie's avatar
Hixie committed
93
    Key key,
94 95
    @required this.value,
    @required this.onChanged,
Hixie's avatar
Hixie committed
96 97
    this.min: 0.0,
    this.max: 1.0,
98 99
    this.divisions,
    this.label,
100
    this.activeColor,
101
    this.inactiveColor,
102 103 104
  }) : assert(value != null),
       assert(min != null),
       assert(max != null),
105
       assert(min <= max),
106 107 108
       assert(value >= min && value <= max),
       assert(divisions == null || divisions > 0),
       super(key: key);
109

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

115 116 117 118 119 120 121
  /// 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.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
  ///
  /// 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();
  ///     });
  ///   },
139
  /// )
140
  /// ```
141 142
  final ValueChanged<double> onChanged;

143
  /// The minimum value the user can select.
144
  ///
145 146 147
  /// 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
148
  final double min;
149 150 151

  /// The maximum value the user can select.
  ///
152 153 154
  /// 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
155
  final double max;
156

157 158 159 160 161 162 163 164 165
  /// 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.
  ///
166 167 168 169 170 171 172 173 174 175 176 177
  /// 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.
178 179
  final String label;

180
  /// The color to use for the portion of the slider rail that is active.
181
  ///
182 183
  /// The "active" side of the slider is the side between the thumb and the
  /// minimum value.
184
  ///
185 186 187 188 189
  /// 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;
190

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

203 204
  @override
  _SliderState createState() => new _SliderState();
205 206

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

class _SliderState extends State<Slider> with TickerProviderStateMixin {
216
  static const Duration enableAnimationDuration = const Duration(milliseconds: 75);
217 218 219 220 221 222 223 224
  static const Duration valueIndicatorAnimationDuration = const Duration(milliseconds: 100);

  // Animation controller that is run when the overlay (a.k.a radial reaction)
  // is shown in response to user interaction.
  AnimationController overlayController;
  // Animation controller that is run when the value indicator is being shown
  // or hidden.
  AnimationController valueIndicatorController;
225
  // Animation controller that is run when enabling/disabling the slider.
226
  AnimationController enableController;
227 228
  // Animation controller that is run when transitioning between one value
  // and the next on a discrete slider.
229
  AnimationController positionController;
230
  Timer interactionTimer;
231 232 233 234

  @override
  void initState() {
    super.initState();
235
    overlayController = new AnimationController(
236 237 238
      duration: kRadialReactionDuration,
      vsync: this,
    );
239 240 241 242
    valueIndicatorController = new AnimationController(
      duration: valueIndicatorAnimationDuration,
      vsync: this,
    );
243 244 245 246 247
    enableController = new AnimationController(
      duration: enableAnimationDuration,
      vsync: this,
    );
    positionController = new AnimationController(
248
      duration: Duration.zero,
249 250
      vsync: this,
    );
251
    enableController.value = widget.onChanged != null ? 1.0 : 0.0;
252
    positionController.value = _unlerp(widget.value);
253 254 255 256
  }

  @override
  void dispose() {
257
    interactionTimer?.cancel();
258 259
    overlayController.dispose();
    valueIndicatorController.dispose();
260 261 262
    enableController.dispose();
    positionController.dispose();
    super.dispose();
263 264
  }

Hixie's avatar
Hixie committed
265
  void _handleChanged(double value) {
266
    assert(widget.onChanged != null);
267 268 269 270
    final double lerpValue = _lerp(value);
    if (lerpValue != widget.value) {
      widget.onChanged(lerpValue);
    }
Hixie's avatar
Hixie committed
271 272
  }

273 274 275 276 277 278
  // 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;
279 280
  }

281 282 283 284
  // 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);
285
    return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0;
286
  }
287

288
  @override
289
  Widget build(BuildContext context) {
290
    assert(debugCheckHasMaterial(context));
291
    assert(debugCheckHasMediaQuery(context));
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

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

310
    return new _SliderRenderObjectWidget(
311
      value: _unlerp(widget.value),
312 313
      divisions: widget.divisions,
      label: widget.label,
314
      sliderTheme: sliderTheme,
315
      mediaQueryData: MediaQuery.of(context),
316
      onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
317
      state: this,
318 319 320 321 322
    );
  }
}

class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
323
  const _SliderRenderObjectWidget({
324 325 326 327
    Key key,
    this.value,
    this.divisions,
    this.label,
328
    this.sliderTheme,
329
    this.mediaQueryData,
330
    this.onChanged,
331
    this.state,
332
  }) : super(key: key);
333 334

  final double value;
335 336
  final int divisions;
  final String label;
337
  final SliderThemeData sliderTheme;
338
  final MediaQueryData mediaQueryData;
339
  final ValueChanged<double> onChanged;
340
  final _SliderState state;
341

342
  @override
343 344 345 346 347
  _RenderSlider createRenderObject(BuildContext context) {
    return new _RenderSlider(
      value: value,
      divisions: divisions,
      label: label,
348 349
      sliderTheme: sliderTheme,
      theme: Theme.of(context),
350
      mediaQueryData: mediaQueryData,
351
      onChanged: onChanged,
352
      state: state,
353
      textDirection: Directionality.of(context),
354 355
    );
  }
356

357
  @override
358
  void updateRenderObject(BuildContext context, _RenderSlider renderObject) {
359 360
    renderObject
      ..value = value
361 362
      ..divisions = divisions
      ..label = label
363 364
      ..sliderTheme = sliderTheme
      ..theme = Theme.of(context)
365
      ..mediaQueryData = mediaQueryData
366 367
      ..onChanged = onChanged
      ..textDirection = Directionality.of(context);
368 369
    // Ticker provider cannot change since there's a 1:1 relationship between
    // the _SliderRenderObjectWidget object and the _SliderState object.
370 371 372
  }
}

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

423 424 425 426 427 428 429 430 431
  static const Duration _positionAnimationDuration = const Duration(milliseconds: 75);
  static const double _overlayRadius = 16.0;
  static const double _overlayDiameter = _overlayRadius * 2.0;
  static const double _railHeight = 2.0;
  static const double _preferredRailWidth = 144.0;
  static const double _preferredTotalWidth = _preferredRailWidth + _overlayDiameter;
  static const Duration _minimumInteractionTime = const Duration(milliseconds: 500);
  static const double _adjustmentUnit = 0.1; // Matches iOS implementation of material slider.
  static final Tween<double> _overlayRadiusTween = new Tween<double>(begin: 0.0, end: _overlayRadius);
432 433

  _SliderState _state;
434 435 436 437 438 439 440 441 442 443 444 445 446 447
  Animation<double> _overlayAnimation;
  Animation<double> _valueIndicatorAnimation;
  Animation<double> _enableAnimation;
  final TextPainter _labelPainter = new TextPainter();
  HorizontalDragGestureRecognizer _drag;
  TapGestureRecognizer _tap;
  bool _active = false;
  double _currentDragValue = 0.0;

  double get _railLength => size.width - _overlayDiameter;

  bool get isInteractive => onChanged != null;

  bool get isDiscrete => divisions != null && divisions > 0;
448

449 450
  double get value => _value;
  double _value;
451
  set value(double newValue) {
452
    assert(newValue != null && newValue >= 0.0 && newValue <= 1.0);
453 454
    final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
    if (convertedValue == _value) {
455
      return;
456 457 458
    }
    _value = convertedValue;
    if (isDiscrete) {
459 460 461 462 463 464 465 466
      // Reset the duration to match the distance that we're traveling, so that
      // whatever the distance, we still do it in _positionAnimationDuration,
      // and if we get re-targeted in the middle, it still takes that long to
      // get to the new location.
      final double distance = (_value - _state.positionController.value).abs();
      _state.positionController.duration = distance != 0.0
        ? _positionAnimationDuration * (1.0 / distance)
        : 0.0;
467 468 469 470
      _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
    } else {
      _state.positionController.value = convertedValue;
    }
471 472 473 474
  }

  int get divisions => _divisions;
  int _divisions;
475
  set divisions(int value) {
476
    if (value == _divisions) {
477
      return;
478
    }
479
    _divisions = value;
480 481 482 483 484
    markNeedsPaint();
  }

  String get label => _label;
  String _label;
485
  set label(String value) {
486
    if (value == _label) {
487
      return;
488
    }
489
    _label = value;
Ian Hickson's avatar
Ian Hickson committed
490
    _updateLabelPainter();
491 492
  }

493 494 495 496
  SliderThemeData get sliderTheme => _sliderTheme;
  SliderThemeData _sliderTheme;
  set sliderTheme(SliderThemeData value) {
    if (value == _sliderTheme) {
497
      return;
498 499
    }
    _sliderTheme = value;
500 501 502
    markNeedsPaint();
  }

503 504 505 506
  ThemeData get theme => _theme;
  ThemeData _theme;
  set theme(ThemeData value) {
    if (value == _theme) {
507
      return;
508 509
    }
    _theme = value;
510 511 512
    markNeedsPaint();
  }

513 514 515 516
  MediaQueryData get mediaQueryData => _mediaQueryData;
  MediaQueryData _mediaQueryData;
  set mediaQueryData(MediaQueryData value) {
    if (value == _mediaQueryData) {
517
      return;
518
    }
519 520 521
    _mediaQueryData = value;
    // Media query data includes the textScaleFactor, so we need to update the
    // label painter.
522 523 524
    _updateLabelPainter();
  }

525 526 527
  ValueChanged<double> get onChanged => _onChanged;
  ValueChanged<double> _onChanged;
  set onChanged(ValueChanged<double> value) {
528
    if (value == _onChanged) {
529
      return;
530
    }
531 532 533
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
534 535 536 537 538
      if (isInteractive) {
        _state.enableController.forward();
      } else {
        _state.enableController.reverse();
      }
539
      markNeedsPaint();
540
      markNeedsSemanticsUpdate();
541 542
    }
  }
543

544 545 546 547
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    assert(value != null);
548
    if (value == _textDirection) {
549
      return;
550
    }
551
    _textDirection = value;
Ian Hickson's avatar
Ian Hickson committed
552 553 554
    _updateLabelPainter();
  }

555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
  bool get showValueIndicator {
    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;
    }
    return showValueIndicator;
  }

Ian Hickson's avatar
Ian Hickson committed
574 575 576
  void _updateLabelPainter() {
    if (label != null) {
      _labelPainter
577 578 579 580
        ..text = new TextSpan(
          style: _sliderTheme.valueIndicatorTextStyle,
          text: label,
        )
Ian Hickson's avatar
Ian Hickson committed
581
        ..textDirection = textDirection
582
        ..textScaleFactor = _mediaQueryData.textScaleFactor
Ian Hickson's avatar
Ian Hickson committed
583 584 585 586 587 588 589 590
        ..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();
591 592
  }

593 594 595
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
596 597
    _overlayAnimation.addListener(markNeedsPaint);
    _valueIndicatorAnimation.addListener(markNeedsPaint);
598 599 600 601 602 603
    _enableAnimation.addListener(markNeedsPaint);
    _state.positionController.addListener(markNeedsPaint);
  }

  @override
  void detach() {
604 605
    _overlayAnimation.removeListener(markNeedsPaint);
    _valueIndicatorAnimation.removeListener(markNeedsPaint);
606 607 608 609 610
    _enableAnimation.removeListener(markNeedsPaint);
    _state.positionController.removeListener(markNeedsPaint);
    super.detach();
  }

611 612 613 614 615 616 617 618 619 620
  double _getValueFromVisualPosition(double visualPosition) {
    switch (textDirection) {
      case TextDirection.rtl:
        return 1.0 - visualPosition;
      case TextDirection.ltr:
        return visualPosition;
    }
    return null;
  }

621
  double _getValueFromGlobalPosition(Offset globalPosition) {
622
    final double visualPosition = (globalToLocal(globalPosition).dx - _overlayRadius) / _railLength;
623
    return _getValueFromVisualPosition(visualPosition);
624 625
  }

626 627
  double _discretize(double value) {
    double result = value.clamp(0.0, 1.0);
628
    if (isDiscrete) {
629
      result = (result * divisions).round() / divisions;
630
    }
631 632
    return result;
  }
633

634
  void _startInteraction(Offset globalPosition) {
635
    if (isInteractive) {
636
      _active = true;
637
      _currentDragValue = _getValueFromGlobalPosition(globalPosition);
638
      onChanged(_discretize(_currentDragValue));
639 640 641
      _state.overlayController.forward();
      if (showValueIndicator) {
        _state.valueIndicatorController.forward();
642
        _state.interactionTimer?.cancel();
643
        _state.interactionTimer = new Timer(_minimumInteractionTime * timeDilation, () {
644 645 646
          _state.interactionTimer = null;
          if (!_active &&
              _state.valueIndicatorController.status == AnimationStatus.completed) {
647 648 649 650
            _state.valueIndicatorController.reverse();
          }
        });
      }
651 652 653 654
    }
  }

  void _endInteraction() {
655
    if (_active && _state.mounted) {
656 657
      _active = false;
      _currentDragValue = 0.0;
658
      _state.overlayController.reverse();
659
      if (showValueIndicator && _state.interactionTimer == null) {
660 661
        _state.valueIndicatorController.reverse();
      }
662 663 664
    }
  }

665 666
  void _handleDragStart(DragStartDetails details) => _startInteraction(details.globalPosition);

667
  void _handleDragUpdate(DragUpdateDetails details) {
668
    if (isInteractive) {
669
      final double valueDelta = details.primaryDelta / _railLength;
670 671 672 673 674 675 676 677
      switch (textDirection) {
        case TextDirection.rtl:
          _currentDragValue -= valueDelta;
          break;
        case TextDirection.ltr:
          _currentDragValue += valueDelta;
          break;
      }
678
      onChanged(_discretize(_currentDragValue));
679 680 681
    }
  }

682
  void _handleDragEnd(DragEndDetails details) => _endInteraction();
683

684 685 686
  void _handleTapDown(TapDownDetails details) => _startInteraction(details.globalPosition);

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

688
  @override
689
  bool hitTestSelf(Offset position) => true;
690

691
  @override
Ian Hickson's avatar
Ian Hickson committed
692
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
693
    assert(debugHandleEvent(event, entry));
694 695
    if (event is PointerDownEvent && isInteractive) {
      // We need to add the drag first so that it has priority.
696
      _drag.addPointer(event);
697 698
      _tap.addPointer(event);
    }
699 700
  }

701 702
  @override
  double computeMinIntrinsicWidth(double height) {
703 704 705 706
    return math.max(
      _overlayDiameter,
      _sliderTheme.thumbShape.getPreferredSize(isInteractive, isDiscrete).width,
    );
707 708 709 710 711 712
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    // This doesn't quite match the definition of computeMaxIntrinsicWidth,
    // but it seems within the spirit...
713
    return _preferredTotalWidth;
714 715 716
  }

  @override
717
  double computeMinIntrinsicHeight(double width) => _overlayDiameter;
718 719

  @override
720
  double computeMaxIntrinsicHeight(double width) => _overlayDiameter;
721 722 723 724 725 726 727

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = new Size(
728 729
      constraints.hasBoundedWidth ? constraints.maxWidth : _preferredTotalWidth,
      constraints.hasBoundedHeight ? constraints.maxHeight : _overlayDiameter,
730 731 732
    );
  }

733
  void _paintTickMarks(
734 735 736 737 738 739
    Canvas canvas,
    Rect railLeft,
    Rect railRight,
    Paint leftPaint,
    Paint rightPaint,
  ) {
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
    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) {
761
    if (!_overlayAnimation.isDismissed) {
762 763 764 765 766
      // 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.
767 768 769
      final Paint overlayPaint = new Paint()..color = _sliderTheme.overlayColor;
      final double radius = _overlayRadiusTween.evaluate(_overlayAnimation);
      canvas.drawCircle(center, radius, overlayPaint);
770 771 772
    }
  }

773
  @override
774 775 776
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

777 778
    final double railLength = size.width - 2 * _overlayRadius;
    final double value = _state.positionController.value;
779 780 781 782 783 784 785 786 787
    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);
788 789

    double visualPosition;
790 791 792 793
    Paint leftRailPaint;
    Paint rightRailPaint;
    Paint leftTickMarkPaint;
    Paint rightTickMarkPaint;
794 795 796
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - value;
797 798 799 800
        leftRailPaint = inactiveRailPaint;
        rightRailPaint = activeRailPaint;
        leftTickMarkPaint = inactiveTickMarkPaint;
        rightTickMarkPaint = activeTickMarkPaint;
801 802 803
        break;
      case TextDirection.ltr:
        visualPosition = value;
804 805 806 807
        leftRailPaint = activeRailPaint;
        rightRailPaint = inactiveRailPaint;
        leftTickMarkPaint = activeTickMarkPaint;
        rightTickMarkPaint = inactiveTickMarkPaint;
808 809
        break;
    }
810

811 812 813 814 815 816 817 818 819
    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;
820 821 822
    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);
823 824 825 826 827 828 829 830 831 832 833
    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);
834
    }
835

836
    _paintOverlay(canvas, thumbCenter);
837

838 839 840 841 842 843 844 845
    _paintTickMarks(
      canvas,
      railLeftRect,
      railRightRect,
      leftTickMarkPaint,
      rightTickMarkPaint,
    );

846 847
    if (isInteractive && label != null &&
        _valueIndicatorAnimation.status != AnimationStatus.dismissed) {
848 849 850 851
      if (showValueIndicator) {
        _sliderTheme.valueIndicatorShape.paint(
          context,
          thumbCenter,
852 853 854 855 856 857 858 859
          activationAnimation: _valueIndicatorAnimation,
          enableAnimation: _enableAnimation,
          isDiscrete: isDiscrete,
          labelPainter: _labelPainter,
          parentBox: this,
          sliderTheme: _sliderTheme,
          textDirection: _textDirection,
          value: _value,
860 861
        );
      }
862
    }
Adam Barth's avatar
Adam Barth committed
863

864 865 866
    _sliderTheme.thumbShape.paint(
      context,
      thumbCenter,
867 868 869 870 871 872 873 874
      activationAnimation: _valueIndicatorAnimation,
      enableAnimation: _enableAnimation,
      isDiscrete: isDiscrete,
      labelPainter: _labelPainter,
      parentBox: this,
      sliderTheme: _sliderTheme,
      textDirection: _textDirection,
      value: _value,
875
    );
876
  }
877 878

  @override
879 880
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
881

882 883
    config.isSemanticBoundary = isInteractive;
    if (isInteractive) {
884 885
      config.onIncrease = _increaseAction;
      config.onDecrease = _decreaseAction;
886 887 888
    }
  }

889
  double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _adjustmentUnit;
890

891
  void _increaseAction() {
892
    if (isInteractive) {
893
      onChanged((value + _semanticActionUnit).clamp(0.0, 1.0));
894
    }
895 896
  }

897
  void _decreaseAction() {
898
    if (isInteractive) {
899
      onChanged((value - _semanticActionUnit).clamp(0.0, 1.0));
900
    }
901
  }
902
}