slider_theme.dart 62.3 KB
Newer Older
1 2 3 4 5
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;
6
import 'dart:ui' show Path, lerpDouble;
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

import 'theme.dart';
import 'theme_data.dart';

/// Applies a slider theme to descendant [Slider] widgets.
///
/// A slider theme describes the colors and shape choices of the slider
/// components.
///
/// Descendant widgets obtain the current theme's [SliderThemeData] object using
/// [SliderTheme.of]. When a widget uses [SliderTheme.of], it is automatically
/// rebuilt if the theme later changes.
///
24 25 26 27 28
/// The slider is as big as the largest of
/// the [SliderComponentShape.getPreferredSize] of the thumb shape,
/// the [SliderComponentShape.getPreferredSize] of the overlay shape,
/// and the [SliderTickMarkShape.getPreferredSize] of the tick mark shape
///
29 30 31 32 33
/// See also:
///
///  * [SliderThemeData], which describes the actual configuration of a slider
///    theme.
///  * [SliderComponentShape], which can be used to create custom shapes for
34 35 36 37 38
///    the slider thumb, overlay, and value indicator.
///  * [SliderTrackShape], which can be used to create custom shapes for the
///    slider track.
///  * [SliderTickMarkShape], which can be used to create custom shapes for the
///    slider tick marks.
39 40 41 42 43 44 45 46
class SliderTheme extends InheritedWidget {
  /// Applies the given theme [data] to [child].
  ///
  /// The [data] and [child] arguments must not be null.
  const SliderTheme({
    Key key,
    @required this.data,
    @required Widget child,
47 48 49
  }) : assert(child != null),
       assert(data != null),
       super(key: key, child: child);
50 51 52 53 54 55 56 57 58 59

  /// Specifies the color and shape values for descendant slider widgets.
  final SliderThemeData data;

  /// Returns the data from the closest [SliderTheme] instance that encloses
  /// the given context.
  ///
  /// Defaults to the ambient [ThemeData.sliderTheme] if there is no
  /// [SliderTheme] in the given build context.
  ///
60
  /// {@tool sample}
61 62
  ///
  /// ```dart
63 64
  /// class Launch extends StatefulWidget {
  ///   @override
65
  ///   State createState() => LaunchState();
66
  /// }
67
  ///
68 69
  /// class LaunchState extends State<Launch> {
  ///   double _rocketThrust;
70
  ///
71 72
  ///   @override
  ///   Widget build(BuildContext context) {
73
  ///     return SliderTheme(
74
  ///       data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff804040)),
75
  ///       child: Slider(
76 77 78 79 80
  ///         onChanged: (double value) { setState(() { _rocketThrust = value; }); },
  ///         value: _rocketThrust,
  ///       ),
  ///     );
  ///   }
81 82
  /// }
  /// ```
83
  /// {@end-tool}
84 85 86 87 88 89 90 91 92 93 94
  ///
  /// See also:
  ///
  ///  * [SliderThemeData], which describes the actual configuration of a slider
  ///    theme.
  static SliderThemeData of(BuildContext context) {
    final SliderTheme inheritedTheme = context.inheritFromWidgetOfExactType(SliderTheme);
    return inheritedTheme != null ? inheritedTheme.data : Theme.of(context).sliderTheme;
  }

  @override
95
  bool updateShouldNotify(SliderTheme oldWidget) => data != oldWidget.data;
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
}

/// Describes the conditions under which the value indicator on a [Slider]
/// will be shown. Used with [SliderThemeData.showValueIndicator].
///
/// See also:
///
///  * [Slider], a Material Design slider widget.
///  * [SliderThemeData], which describes the actual configuration of a slider
///    theme.
enum ShowValueIndicator {
  /// The value indicator will only be shown for discrete sliders (sliders
  /// where [Slider.divisions] is non-null).
  onlyForDiscrete,

  /// The value indicator will only be shown for continuous sliders (sliders
  /// where [Slider.divisions] is null).
  onlyForContinuous,

  /// The value indicator will be shown for all types of sliders.
  always,

  /// The value indicator will never be shown.
  never,
}

/// Holds the color, shape, and typography values for a material design slider
/// theme.
///
/// Use this class to configure a [SliderTheme] widget, or to set the
/// [ThemeData.sliderTheme] for a [Theme] widget.
///
/// To obtain the current ambient slider theme, use [SliderTheme.of].
///
/// The parts of a slider are:
///
///  * The "thumb", which is a shape that slides horizontally when the user
///    drags it.
134
///  * The "track", which is the line that the slider thumb slides along.
135 136 137 138 139 140 141 142
///  * The "tick marks", which are regularly spaced marks that are drawn when
///    using discrete divisions.
///  * The "value indicator", which appears when the user is dragging the thumb
///    to indicate the value being selected.
///  * The "overlay", which appears around the thumb, and is shown when the
///    thumb is pressed, focused, or hovered. It is painted underneath the
///    thumb, so it must extend beyond the bounds of the thumb itself to
///    actually be visible.
143 144 145 146 147 148 149
///  * 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.
///  * The [Slider] is disabled when it is not accepting user input. See
///    [Slider] for details on when this happens.
///
150 151 152 153 154 155
/// The thumb, track, tick marks, value indicator, and overlay can be customized
/// by creating subclasses of [SliderTrackShape],
/// [SliderComponentShape], and/or [SliderTickMarkShape]. See
/// [RoundSliderThumbShape], [RectangularSliderTrackShape],
/// [RoundSliderTickMarkShape], [PaddleSliderValueIndicatorShape], and
/// [RoundSliderOverlayShape] for examples.
156
///
157 158 159 160 161 162 163 164 165 166
/// The track painting can be skipped by specifying 0 for [trackHeight].
/// The thumb painting can be skipped by specifying
/// [SliderComponentShape.noThumb] for [SliderThemeData.thumbShape].
/// The overlay painting can be skipped by specifying
/// [SliderComponentShape.noOverlay] for [SliderThemeData.overlayShape].
/// The tick mark painting can be skipped by specifying
/// [SliderTickMarkShape.noTickMark] for [SliderThemeData.tickMarkShape].
/// The value indicator painting can be skipped by specifying the
/// appropriate [ShowValueIndicator] for [SliderThemeData.showValueIndicator].
///
167 168 169 170 171 172 173
/// See also:
///
///  * [SliderTheme] widget, which can override the slider theme of its
///    children.
///  * [Theme] widget, which performs a similar function to [SliderTheme],
///    but for overall themes.
///  * [ThemeData], which has a default [SliderThemeData].
174
///  * [SliderTrackShape], to define custom slider track shapes.
175
///  * [SliderComponentShape], to define custom slider component shapes.
176
///  * [SliderTickMarkShape], to define custom slider tick mark shapes.
177 178 179 180 181 182 183 184 185
class SliderThemeData extends Diagnosticable {
  /// Create a [SliderThemeData] given a set of exact values. All the values
  /// must be specified.
  ///
  /// This will rarely be used directly. It is used by [lerp] to
  /// create intermediate themes based on two themes.
  ///
  /// The simplest way to create a SliderThemeData is to use
  /// [copyWith] on the one you get from [SliderTheme.of], or create an
186 187
  /// entirely new one with [SliderThemeData.fromPrimaryColors].
  ///
188
  /// {@tool sample}
189 190 191 192
  ///
  /// ```dart
  /// class Blissful extends StatefulWidget {
  ///   @override
193
  ///   State createState() => BlissfulState();
194 195 196 197 198 199 200
  /// }
  ///
  /// class BlissfulState extends State<Blissful> {
  ///   double _bliss;
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
201
  ///     return SliderTheme(
202
  ///       data: SliderTheme.of(context).copyWith(activeTrackColor: const Color(0xff404080)),
203
  ///       child: Slider(
204 205 206 207 208 209 210
  ///         onChanged: (double value) { setState(() { _bliss = value; }); },
  ///         value: _bliss,
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
211
  /// {@end-tool}
212
  const SliderThemeData({
213
    @required this.trackHeight,
214 215 216 217
    @required this.activeTrackColor,
    @required this.inactiveTrackColor,
    @required this.disabledActiveTrackColor,
    @required this.disabledInactiveTrackColor,
218 219 220 221 222 223 224 225
    @required this.activeTickMarkColor,
    @required this.inactiveTickMarkColor,
    @required this.disabledActiveTickMarkColor,
    @required this.disabledInactiveTickMarkColor,
    @required this.thumbColor,
    @required this.disabledThumbColor,
    @required this.overlayColor,
    @required this.valueIndicatorColor,
226 227
    @required this.trackShape,
    @required this.tickMarkShape,
228
    @required this.thumbShape,
229
    @required this.overlayShape,
230 231
    @required this.valueIndicatorShape,
    @required this.showValueIndicator,
232
    @required this.valueIndicatorTextStyle,
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
  })  : assert(trackHeight != null),
        assert(activeTrackColor != null),
        assert(inactiveTrackColor != null),
        assert(disabledActiveTrackColor != null),
        assert(disabledInactiveTrackColor != null),
        assert(activeTickMarkColor != null),
        assert(inactiveTickMarkColor != null),
        assert(disabledActiveTickMarkColor != null),
        assert(disabledInactiveTickMarkColor != null),
        assert(thumbColor != null),
        assert(disabledThumbColor != null),
        assert(overlayColor != null),
        assert(valueIndicatorColor != null),
        assert(trackShape != null),
        assert(tickMarkShape != null),
        assert(thumbShape != null),
        assert(overlayShape != null),
        assert(valueIndicatorShape != null),
        assert(valueIndicatorTextStyle != null),
        assert(showValueIndicator != null);
253 254 255 256 257 258 259 260 261 262

  /// Generates a SliderThemeData from three main colors.
  ///
  /// Usually these are the primary, dark and light colors from
  /// a [ThemeData].
  ///
  /// The opacities of these colors will be overridden with the Material Design
  /// defaults when assigning them to the slider theme component colors.
  ///
  /// This is used to generate the default slider theme for a [ThemeData].
263
  factory SliderThemeData.fromPrimaryColors({
264 265 266
    @required Color primaryColor,
    @required Color primaryColorDark,
    @required Color primaryColorLight,
267
    @required TextStyle valueIndicatorTextStyle,
268 269 270 271
  }) {
    assert(primaryColor != null);
    assert(primaryColorDark != null);
    assert(primaryColorLight != null);
272
    assert(valueIndicatorTextStyle != null);
273 274 275

    // These are Material Design defaults, and are used to derive
    // component Colors (with opacity) from base colors.
276 277 278 279
    const int activeTrackAlpha = 0xff;
    const int inactiveTrackAlpha = 0x3d; // 24% opacity
    const int disabledActiveTrackAlpha = 0x52; // 32% opacity
    const int disabledInactiveTrackAlpha = 0x1f; // 12% opacity
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    const int activeTickMarkAlpha = 0x8a; // 54% opacity
    const int inactiveTickMarkAlpha = 0x8a; // 54% opacity
    const int disabledActiveTickMarkAlpha = 0x1f; // 12% opacity
    const int disabledInactiveTickMarkAlpha = 0x1f; // 12% opacity
    const int thumbAlpha = 0xff;
    const int disabledThumbAlpha = 0x52; // 32% opacity
    const int valueIndicatorAlpha = 0xff;

    // 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.
    const int overlayLightAlpha = 0x29; // 16% opacity

295
    return SliderThemeData(
296
      trackHeight: 2.0,
297 298 299 300
      activeTrackColor: primaryColor.withAlpha(activeTrackAlpha),
      inactiveTrackColor: primaryColor.withAlpha(inactiveTrackAlpha),
      disabledActiveTrackColor: primaryColorDark.withAlpha(disabledActiveTrackAlpha),
      disabledInactiveTrackColor: primaryColorDark.withAlpha(disabledInactiveTrackAlpha),
301 302 303 304 305 306 307 308
      activeTickMarkColor: primaryColorLight.withAlpha(activeTickMarkAlpha),
      inactiveTickMarkColor: primaryColor.withAlpha(inactiveTickMarkAlpha),
      disabledActiveTickMarkColor: primaryColorLight.withAlpha(disabledActiveTickMarkAlpha),
      disabledInactiveTickMarkColor: primaryColorDark.withAlpha(disabledInactiveTickMarkAlpha),
      thumbColor: primaryColor.withAlpha(thumbAlpha),
      disabledThumbColor: primaryColorDark.withAlpha(disabledThumbAlpha),
      overlayColor: primaryColor.withAlpha(overlayLightAlpha),
      valueIndicatorColor: primaryColor.withAlpha(valueIndicatorAlpha),
309 310
      trackShape: const RectangularSliderTrackShape(),
      tickMarkShape: const RoundSliderTickMarkShape(),
311
      thumbShape: const RoundSliderThumbShape(),
312
      overlayShape: const RoundSliderOverlayShape(),
313
      valueIndicatorShape: const PaddleSliderValueIndicatorShape(),
314
      valueIndicatorTextStyle: valueIndicatorTextStyle,
315 316 317 318
      showValueIndicator: ShowValueIndicator.onlyForDiscrete,
    );
  }

319 320 321
  /// The height of the [Slider] track.
  final double trackHeight;

322
  /// The color of the [Slider] track between the [Slider.min] position and the
323
  /// current thumb position.
324
  final Color activeTrackColor;
325

326
  /// The color of the [Slider] track between the current thumb position and the
327
  /// [Slider.max] position.
328
  final Color inactiveTrackColor;
329

330
  /// The color of the [Slider] track between the [Slider.min] position and the
331
  /// current thumb position when the [Slider] is disabled.
332
  final Color disabledActiveTrackColor;
333

334
  /// The color of the [Slider] track between the current thumb position and the
335
  /// [Slider.max] position when the [Slider] is disabled.
336
  final Color disabledInactiveTrackColor;
337

338
  /// The color of the track's tick marks that are drawn between the [Slider.min]
339
  /// position and the current thumb position.
340
  final Color activeTickMarkColor;
341

342
  /// The color of the track's tick marks that are drawn between the current
343
  /// thumb position and the [Slider.max] position.
344
  final Color inactiveTickMarkColor;
345

346
  /// The color of the track's tick marks that are drawn between the [Slider.min]
347
  /// position and the current thumb position when the [Slider] is disabled.
348
  final Color disabledActiveTickMarkColor;
349

350
  /// The color of the track's tick marks that are drawn between the current
351 352
  /// thumb position and the [Slider.max] position when the [Slider] is
  /// disabled.
353
  final Color disabledInactiveTickMarkColor;
354 355

  /// The color given to the [thumbShape] to draw itself with.
356
  final Color thumbColor;
357 358 359

  /// The color given to the [thumbShape] to draw itself with when the
  /// [Slider] is disabled.
360
  final Color disabledThumbColor;
361 362 363 364

  /// The color of the overlay drawn around the slider thumb when it is pressed.
  ///
  /// This is typically a semi-transparent color.
365
  final Color overlayColor;
366 367

  /// The color given to the [valueIndicatorShape] to draw itself with.
368
  final Color valueIndicatorColor;
369

370
  /// The shape that will be used to draw the [Slider]'s track.
371
  ///
372
  /// The [SliderTrackShape.getPreferredRect] method is used to map
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
  /// slider-relative gesture coordinates to the correct thumb position on the
  /// track. It is also used to horizontally position tick marks, when he slider
  /// is discrete.
  ///
  /// The default value is [RectangularSliderTrackShape].
  final SliderTrackShape trackShape;

  /// The shape that will be used to draw the [Slider]'s tick marks.
  ///
  /// The [SliderTickMarkShape.getPreferredSize] is used to help determine the
  /// location of each tick mark on the track. The slider's minimum size will
  /// be at least this big.
  ///
  /// The default value is [RoundSliderTickMarkShape].
  final SliderTickMarkShape tickMarkShape;

  /// The shape that will be used to draw the [Slider]'s overlay.
  ///
  /// Both the [overlayColor] and a non default [overlayShape] may be specified.
  /// In this case, the [overlayColor] is only used if the [overlayShape]
  /// explicitly does so.
  ///
  /// The default value is [RoundSliderOverlayShape].
  final SliderComponentShape overlayShape;

  /// The shape that will be used to draw the [Slider]'s thumb.
399
  final SliderComponentShape thumbShape;
400

401
  /// The shape that will be used to draw the [Slider]'s value
402
  /// indicator.
403 404
  final SliderComponentShape valueIndicatorShape;

405 406
  /// Whether the value indicator should be shown for different types of
  /// sliders.
407 408 409 410 411 412
  ///
  /// By default, [showValueIndicator] is set to
  /// [ShowValueIndicator.onlyForDiscrete]. The value indicator is only shown
  /// when the thumb is being touched.
  final ShowValueIndicator showValueIndicator;

413 414 415 416 417
  /// The text style for the text on the value indicator.
  ///
  /// By default this is the [ThemeData.accentTextTheme.body2] text theme.
  final TextStyle valueIndicatorTextStyle;

418 419
  /// Creates a copy of this object but with the given fields replaced with the
  /// new values.
420
  SliderThemeData copyWith({
421
    double trackHeight,
422 423 424 425
    Color activeTrackColor,
    Color inactiveTrackColor,
    Color disabledActiveTrackColor,
    Color disabledInactiveTrackColor,
426 427 428 429 430 431 432 433
    Color activeTickMarkColor,
    Color inactiveTickMarkColor,
    Color disabledActiveTickMarkColor,
    Color disabledInactiveTickMarkColor,
    Color thumbColor,
    Color disabledThumbColor,
    Color overlayColor,
    Color valueIndicatorColor,
434 435
    SliderTrackShape trackShape,
    SliderTickMarkShape tickMarkShape,
436
    SliderComponentShape thumbShape,
437
    SliderComponentShape overlayShape,
438 439
    SliderComponentShape valueIndicatorShape,
    ShowValueIndicator showValueIndicator,
440
    TextStyle valueIndicatorTextStyle,
441
  }) {
442
    return SliderThemeData(
443
      trackHeight: trackHeight ?? this.trackHeight,
444 445 446 447
      activeTrackColor: activeTrackColor ?? this.activeTrackColor,
      inactiveTrackColor: inactiveTrackColor ?? this.inactiveTrackColor,
      disabledActiveTrackColor: disabledActiveTrackColor ?? this.disabledActiveTrackColor,
      disabledInactiveTrackColor: disabledInactiveTrackColor ?? this.disabledInactiveTrackColor,
448 449 450
      activeTickMarkColor: activeTickMarkColor ?? this.activeTickMarkColor,
      inactiveTickMarkColor: inactiveTickMarkColor ?? this.inactiveTickMarkColor,
      disabledActiveTickMarkColor: disabledActiveTickMarkColor ?? this.disabledActiveTickMarkColor,
451
      disabledInactiveTickMarkColor: disabledInactiveTickMarkColor ?? this.disabledInactiveTickMarkColor,
452 453 454 455
      thumbColor: thumbColor ?? this.thumbColor,
      disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor,
      overlayColor: overlayColor ?? this.overlayColor,
      valueIndicatorColor: valueIndicatorColor ?? this.valueIndicatorColor,
456 457
      trackShape: trackShape ?? this.trackShape,
      tickMarkShape: tickMarkShape ?? this.tickMarkShape,
458
      thumbShape: thumbShape ?? this.thumbShape,
459
      overlayShape: overlayShape ?? this.overlayShape,
460 461
      valueIndicatorShape: valueIndicatorShape ?? this.valueIndicatorShape,
      showValueIndicator: showValueIndicator ?? this.showValueIndicator,
462
      valueIndicatorTextStyle: valueIndicatorTextStyle ?? this.valueIndicatorTextStyle,
463 464 465 466 467 468 469
    );
  }

  /// Linearly interpolate between two slider themes.
  ///
  /// The arguments must not be null.
  ///
470
  /// {@macro dart.ui.shadow.lerp}
471 472 473 474
  static SliderThemeData lerp(SliderThemeData a, SliderThemeData b, double t) {
    assert(a != null);
    assert(b != null);
    assert(t != null);
475
    return SliderThemeData(
476
      trackHeight: lerpDouble(a.trackHeight, b.trackHeight, t),
477 478 479 480
      activeTrackColor: Color.lerp(a.activeTrackColor, b.activeTrackColor, t),
      inactiveTrackColor: Color.lerp(a.inactiveTrackColor, b.inactiveTrackColor, t),
      disabledActiveTrackColor: Color.lerp(a.disabledActiveTrackColor, b.disabledActiveTrackColor, t),
      disabledInactiveTrackColor: Color.lerp(a.disabledInactiveTrackColor, b.disabledInactiveTrackColor, t),
481 482
      activeTickMarkColor: Color.lerp(a.activeTickMarkColor, b.activeTickMarkColor, t),
      inactiveTickMarkColor: Color.lerp(a.inactiveTickMarkColor, b.inactiveTickMarkColor, t),
483 484
      disabledActiveTickMarkColor: Color.lerp(a.disabledActiveTickMarkColor, b.disabledActiveTickMarkColor, t),
      disabledInactiveTickMarkColor: Color.lerp(a.disabledInactiveTickMarkColor, b.disabledInactiveTickMarkColor, t),
485 486 487 488
      thumbColor: Color.lerp(a.thumbColor, b.thumbColor, t),
      disabledThumbColor: Color.lerp(a.disabledThumbColor, b.disabledThumbColor, t),
      overlayColor: Color.lerp(a.overlayColor, b.overlayColor, t),
      valueIndicatorColor: Color.lerp(a.valueIndicatorColor, b.valueIndicatorColor, t),
489 490
      trackShape: t < 0.5 ? a.trackShape : b.trackShape,
      tickMarkShape: t < 0.5 ? a.tickMarkShape : b.tickMarkShape,
491
      thumbShape: t < 0.5 ? a.thumbShape : b.thumbShape,
492
      overlayShape: t < 0.5 ? a.overlayShape : b.overlayShape,
493 494
      valueIndicatorShape: t < 0.5 ? a.valueIndicatorShape : b.valueIndicatorShape,
      showValueIndicator: t < 0.5 ? a.showValueIndicator : b.showValueIndicator,
495
      valueIndicatorTextStyle: TextStyle.lerp(a.valueIndicatorTextStyle, b.valueIndicatorTextStyle, t),
496 497 498 499 500 501
    );
  }

  @override
  int get hashCode {
    return hashValues(
502
      trackHeight,
503 504 505 506
      activeTrackColor,
      inactiveTrackColor,
      disabledActiveTrackColor,
      disabledInactiveTrackColor,
507 508 509 510 511 512 513 514
      activeTickMarkColor,
      inactiveTickMarkColor,
      disabledActiveTickMarkColor,
      disabledInactiveTickMarkColor,
      thumbColor,
      disabledThumbColor,
      overlayColor,
      valueIndicatorColor,
515 516
      trackShape,
      tickMarkShape,
517
      thumbShape,
518
      overlayShape,
519 520
      valueIndicatorShape,
      showValueIndicator,
521
      valueIndicatorTextStyle,
522 523 524 525 526
    );
  }

  @override
  bool operator ==(Object other) {
527 528 529
    if (identical(this, other)) {
      return true;
    }
530 531 532 533
    if (other.runtimeType != runtimeType) {
      return false;
    }
    final SliderThemeData otherData = other;
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553
    return otherData.trackHeight == trackHeight
      && otherData.activeTrackColor == activeTrackColor
      && otherData.inactiveTrackColor == inactiveTrackColor
      && otherData.disabledActiveTrackColor == disabledActiveTrackColor
      && otherData.disabledInactiveTrackColor == disabledInactiveTrackColor
      && otherData.activeTickMarkColor == activeTickMarkColor
      && otherData.inactiveTickMarkColor == inactiveTickMarkColor
      && otherData.disabledActiveTickMarkColor == disabledActiveTickMarkColor
      && otherData.disabledInactiveTickMarkColor == disabledInactiveTickMarkColor
      && otherData.thumbColor == thumbColor
      && otherData.disabledThumbColor == disabledThumbColor
      && otherData.overlayColor == overlayColor
      && otherData.valueIndicatorColor == valueIndicatorColor
      && otherData.trackShape == trackShape
      && otherData.tickMarkShape == tickMarkShape
      && otherData.thumbShape == thumbShape
      && otherData.overlayShape == overlayShape
      && otherData.valueIndicatorShape == valueIndicatorShape
      && otherData.showValueIndicator == showValueIndicator
      && otherData.valueIndicatorTextStyle == valueIndicatorTextStyle;
554 555 556
  }

  @override
557 558
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
559 560
    final ThemeData defaultTheme = ThemeData.fallback();
    final SliderThemeData defaultData = SliderThemeData.fromPrimaryColors(
561 562 563
      primaryColor: defaultTheme.primaryColor,
      primaryColorDark: defaultTheme.primaryColorDark,
      primaryColorLight: defaultTheme.primaryColorLight,
564
      valueIndicatorTextStyle: defaultTheme.accentTextTheme.body2,
565
    );
566
    properties.add(DiagnosticsProperty<Color>('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
567
    properties.add(DiagnosticsProperty<Color>('activeTrackColor', activeTrackColor, defaultValue: defaultData.activeTrackColor));
568 569 570 571 572 573 574 575 576 577 578
    properties.add(DiagnosticsProperty<Color>('inactiveTrackColor', inactiveTrackColor, defaultValue: defaultData.inactiveTrackColor));
    properties.add(DiagnosticsProperty<Color>('disabledActiveTrackColor', disabledActiveTrackColor, defaultValue: defaultData.disabledActiveTrackColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('disabledInactiveTrackColor', disabledInactiveTrackColor, defaultValue: defaultData.disabledInactiveTrackColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('activeTickMarkColor', activeTickMarkColor, defaultValue: defaultData.activeTickMarkColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('inactiveTickMarkColor', inactiveTickMarkColor, defaultValue: defaultData.inactiveTickMarkColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('disabledActiveTickMarkColor', disabledActiveTickMarkColor, defaultValue: defaultData.disabledActiveTickMarkColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('disabledInactiveTickMarkColor', disabledInactiveTickMarkColor, defaultValue: defaultData.disabledInactiveTickMarkColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('thumbColor', thumbColor, defaultValue: defaultData.thumbColor));
    properties.add(DiagnosticsProperty<Color>('disabledThumbColor', disabledThumbColor, defaultValue: defaultData.disabledThumbColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('overlayColor', overlayColor, defaultValue: defaultData.overlayColor, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<Color>('valueIndicatorColor', valueIndicatorColor, defaultValue: defaultData.valueIndicatorColor));
579 580
    properties.add(DiagnosticsProperty<SliderTrackShape>('trackShape', trackShape, defaultValue: defaultData.trackShape, level: DiagnosticLevel.debug));
    properties.add(DiagnosticsProperty<SliderTickMarkShape>('tickMarkShape', tickMarkShape, defaultValue: defaultData.tickMarkShape, level: DiagnosticLevel.debug));
581
    properties.add(DiagnosticsProperty<SliderComponentShape>('thumbShape', thumbShape, defaultValue: defaultData.thumbShape, level: DiagnosticLevel.debug));
582
    properties.add(DiagnosticsProperty<SliderComponentShape>('overlayShape', overlayShape, defaultValue: defaultData.overlayShape, level: DiagnosticLevel.debug));
583 584 585
    properties.add(DiagnosticsProperty<SliderComponentShape>('valueIndicatorShape', valueIndicatorShape, defaultValue: defaultData.valueIndicatorShape, level: DiagnosticLevel.debug));
    properties.add(EnumProperty<ShowValueIndicator>('showValueIndicator', showValueIndicator, defaultValue: defaultData.showValueIndicator));
    properties.add(DiagnosticsProperty<TextStyle>('valueIndicatorTextStyle', valueIndicatorTextStyle, defaultValue: defaultData.valueIndicatorTextStyle));
586 587 588
  }
}

589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
// TEMPLATES FOR ALL SHAPES

/// {@template flutter.material.slider.shape.context}
/// [context] is the same context for the render box of the [Slider].
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.center}
/// [center] is the offset of the center where this shape should be painted.
/// This offset is relative to the origin of the [context] canvas.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.sliderTheme}
/// [sliderTheme] is the theme assigned to the [Slider] that this shape
/// belongs to.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.isEnabled}
/// [isEnabled] has the same value as [Slider.isInteractive]. If true, the
/// slider will respond to input.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.enableAnimation}
/// [enableAnimation] is an animation triggered when the [Slider] is enabled,
/// and it reverses when the slider is disabled. Enabled is the
/// [Slider.isInteractive] state. Use this to paint intermediate frames for
/// this shape when the slider changes enabled state.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.isDiscrete}
/// [isDiscrete] is true if [Slider.divisions] is non-null. If true, the
/// slider will render tick marks on top of the track.
/// {@endtemplate}
///
/// {@template flutter.material.slider.shape.parentBox}
/// [parentBox] is the [RenderBox] of the [Slider]. Its attributes, such as
/// size, can be used to assist in painting this shape.
/// {@endtemplate}

/// Base class for slider track shapes.
///
/// The slider's thumb moves along the track. A discrete slider's tick marks
/// are drawn after the track, but before the thumb, and are aligned with the
/// track.
///
/// The [getPreferredRect] helps position the slider thumb and tick marks
/// relative to the track.
///
/// See also:
///
638
///  * [RectangularSliderTrackShape], which is the default track shape.
639 640 641 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 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 711 712 713
///  * [SliderTickMarkShape], which is the default tick mark shape.
///  * [SliderComponentShape], which is the base class for custom a component
///    shape.
abstract class SliderTrackShape {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliderTrackShape();

  /// Returns the preferred bounds of the shape.
  ///
  /// It is used to provide horizontal boundaries for the thumb's position, and
  /// to help position the slider thumb and tick marks relative to the track.
  ///
  /// [parentBox] can be used to help determine the preferredRect relative to
  /// attributes of the render box of the slider itself, such as size.
  ///
  /// [offset] is relative to the caller's bounding box. It can be used to
  /// convert gesture coordinates from global to slider-relative coordinates.
  ///
  /// {@macro flutter.material.slider.shape.sliderTheme}
  ///
  /// {@macro flutter.material.slider.shape.isEnabled}
  ///
  /// {@macro flutter.material.slider.shape.isDiscrete}
  Rect getPreferredRect({
    RenderBox parentBox,
    Offset offset = Offset.zero,
    SliderThemeData sliderTheme,
    bool isEnabled,
    bool isDiscrete,
  });

  /// Paints the track shape based on the state passed to it.
  ///
  /// {@macro flutter.material.slider.shape.context}
  ///
  /// [offset] is the offset of the origin of the [parentBox] to the origin of
  /// its [context] canvas. This shape must be painted relative to this
  /// offset. See [PaintingContextCallback].
  ///
  /// {@macro flutter.material.slider.shape.parentBox}
  ///
  /// {@macro flutter.material.slider.shape.sliderTheme}
  ///
  /// {@macro flutter.material.slider.shape.enableAnimation}
  ///
  /// [thumbCenter] is the offset of the center of the thumb relative to the
  /// origin of the [PaintingContext.canvas]. It can be used as the point that
  /// divides the track into 2 segments.
  ///
  /// {@macro flutter.material.slider.shape.isEnabled}
  ///
  /// {@macro flutter.material.slider.shape.isDiscrete}
  ///
  /// [textDirection] can be used to determine how the track segments are
  /// painted depending on whether they are active or not. The track segment
  /// between the start of the slider and the thumb is the active track segment.
  /// The track segment between the thumb and the end of the slider is the
  /// inactive track segment. In LTR text direction, the start of the slider is
  /// on the left, and in RTL text direction, the start of the slider is on the
  /// right.
  void paint(
    PaintingContext context,
    Offset offset, {
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    Animation<double> enableAnimation,
    Offset thumbCenter,
    bool isEnabled,
    bool isDiscrete,
    TextDirection textDirection,
  });
}

/// Base class for slider tick mark shapes.
714
///
715 716 717
/// Create a subclass of this if you would like a custom slider tick mark shape.
/// This is a simplified version of [SliderComponentShape] with a
/// [SliderThemeData] passed when getting the preferred size.
718
///
719 720 721
/// The tick mark painting can be skipped by specifying [noTickMark] for
/// [SliderThemeData.tickMarkShape].
///
722 723
/// See also:
///
724 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 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
///  * [RoundSliderTickMarkShape] for a simple example of a tick mark shape.
///  * [SliderTrackShape] for the base class for custom a track shape.
///  * [SliderComponentShape] for the base class for custom a component shape.
abstract class SliderTickMarkShape {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliderTickMarkShape();

  /// Returns the preferred size of the shape.
  ///
  /// It is used to help position the tick marks within the slider.
  ///
  /// {@macro flutter.material.slider.shape.sliderTheme}
  ///
  /// {@macro flutter.material.slider.shape.isEnabled}
  Size getPreferredSize({
    SliderThemeData sliderTheme,
    bool isEnabled,
  });

  /// Paints the slider track.
  ///
  /// {@macro flutter.material.slider.shape.context}
  ///
  /// {@macro flutter.material.slider.shape.center}
  ///
  /// {@macro flutter.material.slider.shape.parentBox}
  ///
  /// {@macro flutter.material.slider.shape.sliderTheme}
  ///
  /// {@macro flutter.material.slider.shape.enableAnimation}
  ///
  /// {@macro flutter.material.slider.shape.isEnabled}
  ///
  /// [textDirection] can be used to determine how the tick marks are painting
  /// depending on whether they are on an active track segment or not. The track
  /// segment between the start of the slider and the thumb is the active track
  /// segment. The track segment between the thumb and the end of the slider is
  /// the inactive track segment. In LTR text direction, the start of the slider
  /// is on the left, and in RTL text direction, the start of the slider is on
  /// the right.
  void paint(
    PaintingContext context,
    Offset center, {
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    Animation<double> enableAnimation,
    Offset thumbCenter,
    bool isEnabled,
    TextDirection textDirection,
  });
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 802 803 804 805 806 807 808 809 810 811 812 813

  /// Special instance of [SliderTickMarkShape] to skip the tick mark painting.
  ///
  /// See also:
  ///
  /// * [SliderThemeData.tickMarkShape], which is the shape that the [Slider]
  /// uses when painting tick marks.
  static final SliderTickMarkShape noTickMark = _EmptySliderTickMarkShape();
}

/// A special version of [SliderTickMarkShape] that has a zero size and paints
/// nothing.
///
/// This class is used to create a special instance of a [SliderTickMarkShape]
/// that will not paint any tick mark shape. A static reference is stored in
/// [SliderTickMarkShape.noTickMark]. When this value  is specified for
/// [SliderThemeData.tickMarkShape], the tick mark painting is skipped.
class _EmptySliderTickMarkShape extends SliderTickMarkShape {
  @override
  Size getPreferredSize({
    SliderThemeData sliderTheme,
    bool isEnabled,
  }) {
    return Size.zero;
  }

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    Animation<double> enableAnimation,
    Offset thumbCenter,
    bool isEnabled,
    TextDirection textDirection,
  }) {
    // no-op.
  }
814 815 816 817 818 819 820 821 822
}

/// Base class for slider thumb, thumb overlay, and value indicator shapes.
///
/// Create a subclass of this if you would like a custom shape.
///
/// All shapes are painted to the same canvas and ordering is important.
/// The overlay is painted first, then the value indicator, then the thumb.
///
823 824 825 826 827 828
/// The thumb painting can be skipped by specifying [noThumb] for
/// [SliderThemeData.thumbShape].
///
/// The overlay painting can be skipped by specifying [noOverlay] for
/// [SliderThemeData.overlayShape].
///
829 830 831 832 833
/// See also:
///
///  * [RoundSliderThumbShape], which is the the default thumb shape.
///  * [RoundSliderOverlayShape], which is the the default overlay shape.
///  * [PaddleSliderValueIndicatorShape], which is the the default value
834 835 836 837 838 839 840 841 842 843 844
///    indicator shape.
abstract class SliderComponentShape {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const SliderComponentShape();

  /// Returns the preferred size of the shape, based on the given conditions.
  Size getPreferredSize(bool isEnabled, bool isDiscrete);

  /// Paints the shape, taking into account the state passed to it.
  ///
845 846 847 848
  /// {@macro flutter.material.slider.shape.context}
  ///
  /// {@macro flutter.material.slider.shape.center}
  ///
849 850 851
  /// [activationAnimation] is an animation triggered when the user beings
  /// to interact with the slider. It reverses when the user stops interacting
  /// with the slider.
852
  ///
853
  /// {@macro flutter.material.slider.shape.enableAnimation}
854
  ///
855
  /// {@macro flutter.material.slider.shape.isDiscrete}
856
  ///
857 858 859
  /// If [labelPainter] is non-null, then [labelPainter.paint] should be
  /// called with the location that the label should appear. If the labelPainter
  /// passed is null, then no label was supplied to the [Slider].
860 861 862 863 864 865 866 867 868 869
  ///
  /// {@macro flutter.material.slider.shape.parentBox}
  ///
  /// {@macro flutter.material.slider.shape.sliderTheme}
  ///
  /// [textDirection] can be used to determine how any extra text or graphics,
  /// besides the text painted by the [labelPainter] should be positioned. The
  /// [labelPainter] already has the [textDirection] set.
  ///
  /// [value] is the current parametric value (from 0.0 to 1.0) of the slider.
870 871
  void paint(
    PaintingContext context,
872
    Offset center, {
873 874
    Animation<double> activationAnimation,
    Animation<double> enableAnimation,
875
    bool isDiscrete,
876
    TextPainter labelPainter,
877
    RenderBox parentBox,
878 879 880
    SliderThemeData sliderTheme,
    TextDirection textDirection,
    double value,
881
  });
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927

  /// Special instance of [SliderComponentShape] to skip the thumb drawing.
  ///
  /// See also:
  ///
  /// * [SliderThemeData.thumbShape], which is the shape that the [Slider]
  /// uses when painting the thumb.
  static final SliderComponentShape noThumb = _EmptySliderComponentShape();

  /// Special instance of [SliderComponentShape] to skip the overlay drawing.
  ///
  /// See also:
  ///
  /// * [SliderThemeData.overlayShape], which is the shape that the [Slider]
  /// uses when painting the overlay.
  static final SliderComponentShape noOverlay = _EmptySliderComponentShape();
}

/// A special version of [SliderComponentShape] that has a zero size and paints
/// nothing.
///
/// This class is used to create a special instance of a [SliderComponentShape]
/// that will not paint any component shape. A static reference is stored in
/// [SliderTickMarkShape.noThumb] and [SliderTickMarkShape.noOverlay]. When this value
/// is specified for [SliderThemeData.thumbShape], the thumb painting is
/// skipped.  When this value is specified for [SliderThemeData.overlaySHape],
/// the overlay painting is skipped.
class _EmptySliderComponentShape extends SliderComponentShape {
  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    Animation<double> activationAnimation,
    Animation<double> enableAnimation,
    bool isDiscrete,
    TextPainter labelPainter,
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    TextDirection textDirection,
    double value,
  }) {
    // no-op.
  }
928 929
}

930 931 932 933 934 935 936 937 938 939 940 941 942 943
// The following shapes are the material defaults.

/// This is the default shape of a [Slider]'s track.
///
/// It paints a solid colored rectangle, vertically centered in the
/// [parentBox]. The track rectangle extends to the bounds of the [parentBox],
/// but is padded by the [RoundSliderOverlayShape] radius. The height is defined
/// by the [SliderThemeData.trackHeight]. The color is determined by the
/// [Slider]'s enabled state and the track piece's active state which are
/// defined by:
///   [SliderThemeData.activeTrackColor],
///   [SliderThemeData.inactiveTrackColor],
///   [SliderThemeData.disabledActiveTrackColor],
///   [SliderThemeData.disabledInactiveTrackColor].
944 945 946 947 948
///
/// See also:
///
///  * [Slider] for the component that this is meant to display this shape.
///  * [SliderThemeData] where an instance of this class is set to inform the
949 950 951 952
///    slider of the visual details of the its track.
///  * [SliderTrackShape] Base component for creating other custom track
///    shapes.
class RectangularSliderTrackShape extends SliderTrackShape {
953 954 955 956 957 958 959 960 961 962
  /// Create a slider track that draws 2 rectangles.
  const RectangularSliderTrackShape({ this.disabledThumbGapWidth = 2.0 });

  /// Horizontal spacing, or gap, between the disabled thumb and the track.
  ///
  /// This is only used when the slider is disabled. There is no gap around
  /// the thumb and any part of the track when the slider is enabled. The
  /// Material spec defaults this gap width 2, which is half of the disabled
  /// thumb radius.
  final double disabledThumbGapWidth;
963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000

  @override
  Rect getPreferredRect({
    RenderBox parentBox,
    Offset offset = Offset.zero,
    SliderThemeData sliderTheme,
    bool isEnabled,
    bool isDiscrete,
  }) {
    final double overlayWidth = sliderTheme.overlayShape.getPreferredSize(isEnabled, isDiscrete).width;
    final double trackHeight = sliderTheme.trackHeight;
    assert(overlayWidth >= 0);
    assert(trackHeight >= 0);
    assert(parentBox.size.width >= overlayWidth);
    assert(parentBox.size.height >= trackHeight);

    final double trackLeft = offset.dx + overlayWidth / 2;
    final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
    // TODO(clocksmith): Although this works for a material, perhaps the default
    // rectangular track should be padded not just by the overlay, but by the
    // max of the thumb and the overlay, in case there is no overlay.
    final double trackWidth = parentBox.size.width - overlayWidth;
    return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
  }


  @override
  void paint(
    PaintingContext context,
    Offset offset, {
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    Animation<double> enableAnimation,
    TextDirection textDirection,
    Offset thumbCenter,
    bool isDiscrete,
    bool isEnabled,
  }) {
1001 1002 1003 1004 1005 1006
    // If the slider track height is 0, then it makes no difference whether the
    // track is painted or not, therefore the painting can be a no-op.
    if (sliderTheme.trackHeight == 0) {
      return;
    }

1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
    // Assign the track segment paints, which are left: active, right: inactive,
    // but reversed for right to left text.
    final ColorTween activeTrackColorTween = ColorTween(begin: sliderTheme.disabledActiveTrackColor , end: sliderTheme.activeTrackColor);
    final ColorTween inactiveTrackColorTween = ColorTween(begin: sliderTheme.disabledInactiveTrackColor , end: sliderTheme.inactiveTrackColor);
    final Paint activePaint = Paint()..color = activeTrackColorTween.evaluate(enableAnimation);
    final Paint inactivePaint = Paint()..color = inactiveTrackColorTween.evaluate(enableAnimation);
    Paint leftTrackPaint;
    Paint rightTrackPaint;
    switch (textDirection) {
      case TextDirection.ltr:
        leftTrackPaint = activePaint;
        rightTrackPaint = inactivePaint;
        break;
      case TextDirection.rtl:
        leftTrackPaint = inactivePaint;
        rightTrackPaint = activePaint;
        break;
    }

    // Used to create a gap around the thumb iff the slider is disabled.
1027 1028 1029 1030
    // If the slider is enabled, the track can be drawn beneath the thumb
    // without a gap. But when the slider is disabled, the track is shortened
    // and this gap helps determine how much shorter it should be.
    // TODO(clocksmith): The new Material spec has a gray circle in place of this gap.
1031 1032
    double horizontalAdjustment = 0.0;
    if (!isEnabled) {
1033 1034 1035
      final double disabledThumbRadius = sliderTheme.thumbShape.getPreferredSize(false, isDiscrete).width / 2.0;
      final double gap = disabledThumbGapWidth * (1.0 - enableAnimation.value);
      horizontalAdjustment = disabledThumbRadius + gap;
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
    }

    final Rect trackRect = getPreferredRect(
        parentBox: parentBox,
        offset: offset,
        sliderTheme: sliderTheme,
        isEnabled: isEnabled,
        isDiscrete: isDiscrete
    );
    final Rect leftTrackSegment = Rect.fromLTRB(trackRect.left, trackRect.top, thumbCenter.dx - horizontalAdjustment, trackRect.bottom);
    context.canvas.drawRect(leftTrackSegment, leftTrackPaint);
    final Rect rightTrackSegment = Rect.fromLTRB(thumbCenter.dx + horizontalAdjustment, trackRect.top, trackRect.right, trackRect.bottom);
    context.canvas.drawRect(rightTrackSegment, rightTrackPaint);
  }
}

/// This is the default shape of each [Slider] tick mark.
///
/// Tick marks are only displayed if the slider is discrete, which can be done
/// by setting the [Slider.divisions] as non-null.
///
/// It paints a solid circle, centered in the on the track.
/// The color is determined by the [Slider]'s enabled state and track's active
/// states. These colors are defined in:
///   [SliderThemeData.activeTrackColor],
///   [SliderThemeData.inactiveTrackColor],
///   [SliderThemeData.disabledActiveTrackColor],
///   [SliderThemeData.disabledInactiveTrackColor].
///
/// See also:
///
///  * [Slider], which includes tick marks defined by this shape.
///  * [SliderTheme], which can be used to configure the tick mark shape of all
///    sliders in a widget subtree.
class RoundSliderTickMarkShape extends SliderTickMarkShape {
1071 1072 1073 1074 1075 1076 1077
  /// Create a slider tick mark that draws a circle.
  const RoundSliderTickMarkShape({ this.tickMarkRadius });

  /// The preferred radius of the round tick mark.
  ///
  /// If it is not provided, then half of the track height is used.
  final double tickMarkRadius;
1078 1079 1080 1081 1082 1083

  @override
  Size getPreferredSize({
    bool isEnabled,
    SliderThemeData sliderTheme,
  }) {
1084 1085 1086
    // The tick marks are tiny circles. If no radius is provided, then they are
    // defaulted to be the same height as the track.
    return Size.fromRadius(tickMarkRadius ?? sliderTheme.trackHeight / 2);
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
  }

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    Animation<double> enableAnimation,
    TextDirection textDirection,
    Offset thumbCenter,
    bool isEnabled,
  }) {
    // The paint color of the tick mark depends on its position relative
    // to the thumb and the text direction.
    Color begin;
    Color end;
    switch (textDirection) {
      case TextDirection.ltr:
        final bool isTickMarkRightOfThumb = center.dx > thumbCenter.dx;
        begin = isTickMarkRightOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
        end = isTickMarkRightOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
        break;
      case TextDirection.rtl:
        final bool isTickMarkLeftOfThumb = center.dx < thumbCenter.dx;
        begin = isTickMarkLeftOfThumb ? sliderTheme.disabledInactiveTickMarkColor : sliderTheme.disabledActiveTickMarkColor;
        end = isTickMarkLeftOfThumb ? sliderTheme.inactiveTickMarkColor : sliderTheme.activeTickMarkColor;
        break;
    }
    final Paint paint = Paint()..color = ColorTween(begin: begin, end: end).evaluate(enableAnimation);

    // The tick marks are tiny circles that are the same height as the track.
1119 1120 1121 1122
    final double tickMarkRadius = getPreferredSize(
      isEnabled: isEnabled,
      sliderTheme: sliderTheme,
    ).width / 2;
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
    context.canvas.drawCircle(center, tickMarkRadius, paint);
  }
}

/// This is the default shape of a [Slider]'s thumb.
///
/// See also:
///
///  * [Slider], which includes a thumb defined by this shape.
///  * [SliderTheme], which can be used to configure the thumb shape of all
///    sliders in a widget subtree.
1134
class RoundSliderThumbShape extends SliderComponentShape {
1135
  /// Create a slider thumb that draws a circle.
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
  // TODO(clocksmith): This needs to be changed to 10 according to spec.
  const RoundSliderThumbShape({
    this.enabledThumbRadius = 6.0,
    this.disabledThumbRadius
  });

  /// The preferred radius of the round thumb shape when the slider is enabled.
  ///
  /// If it is not provided, then the material default is used.
  final double enabledThumbRadius;
1146

1147 1148 1149 1150 1151 1152 1153 1154 1155
  /// The preferred radius of the round thumb shape when the slider is disabled.
  ///
  /// If no disabledRadius is provided, then it is is derived from the enabled
  /// thumb radius and has the same ratio of enabled size to disabled size as
  /// the Material spec. The default resolves to 4, which is 2 / 3 of the
  /// default enabled thumb.
  final double disabledThumbRadius;
  // TODO(clocksmith): This needs to be updated once the thumb size is updated to the Material spec.
  double get _disabledThumbRadius =>  disabledThumbRadius ?? enabledThumbRadius * 2 / 3;
1156 1157 1158

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
1159
    return Size.fromRadius(isEnabled ? enabledThumbRadius : _disabledThumbRadius);
1160 1161 1162 1163 1164
  }

  @override
  void paint(
    PaintingContext context,
1165
    Offset center, {
1166 1167
    Animation<double> activationAnimation,
    Animation<double> enableAnimation,
1168
    bool isDiscrete,
1169
    TextPainter labelPainter,
1170
    RenderBox parentBox,
1171 1172 1173
    SliderThemeData sliderTheme,
    TextDirection textDirection,
    double value,
1174
  }) {
1175
    final Canvas canvas = context.canvas;
1176
    final Tween<double> radiusTween = Tween<double>(
1177
      begin: _disabledThumbRadius,
1178
      end: enabledThumbRadius,
1179
    );
1180
    final ColorTween colorTween = ColorTween(
1181 1182 1183
      begin: sliderTheme.disabledThumbColor,
      end: sliderTheme.thumbColor,
    );
1184
    canvas.drawCircle(
1185
      center,
1186
      radiusTween.evaluate(enableAnimation),
1187
      Paint()..color = colorTween.evaluate(enableAnimation),
1188 1189 1190 1191
    );
  }
}

1192 1193 1194 1195 1196 1197 1198 1199 1200
/// This is the default shape of a [Slider]'s thumb overlay.
///
/// The shape of the overlay is a circle with the same center as the thumb, but
/// with a larger radius. It animates to full size when the thumb is pressed,
/// and animates back down to size 0 when it is released. It is painted behind
/// the thumb, and is expected to extend beyond the bounds of the thumb so that
/// it is visible.
///
/// The overlay color is defined by [SliderThemeData.overlayColor].
1201 1202 1203
///
/// See also:
///
1204 1205 1206 1207 1208
///  * [Slider], which includes an overlay defined by this shape.
///  * [SliderTheme], which can be used to configure the overlay shape of all
///    sliders in a widget subtree.
class RoundSliderOverlayShape extends SliderComponentShape {
  /// Create a slider thumb overlay that draws a circle.
1209 1210
  // TODO(clocksmith): This needs to be changed to 24 according to spec.
  const RoundSliderOverlayShape({ this.overlayRadius = 16.0 });
1211

1212 1213 1214 1215
  /// The preferred radius of the round thumb shape when enabled.
  ///
  /// If it is not provided, then half of the track height is used.
  final double overlayRadius;
1216 1217 1218

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
1219
    return Size.fromRadius(overlayRadius);
1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237
  }

  @override
  void paint(
    PaintingContext context,
    Offset center, {
    Animation<double> activationAnimation,
    Animation<double> enableAnimation,
    bool isDiscrete,
    TextPainter labelPainter,
    RenderBox parentBox,
    SliderThemeData sliderTheme,
    TextDirection textDirection,
    double value,
  }) {
    final Canvas canvas = context.canvas;
    final Tween<double> radiusTween = Tween<double>(
      begin: 0.0,
1238
      end: overlayRadius,
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
    );

    // 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.
    canvas.drawCircle(
      center,
      radiusTween.evaluate(activationAnimation),
      Paint()..color = sliderTheme.overlayColor,
    );
  }
}

/// This is the default shape of a [Slider]'s value indicator.
///
/// See also:
///
///  * [Slider], which includes a value indicator defined by this shape.
///  * [SliderTheme], which can be used to configure the slider value indicator
///    of all sliders in a widget subtree.
1261
class PaddleSliderValueIndicatorShape extends SliderComponentShape {
1262
  /// Create a slider value indicator in the shape of an upside-down pear.
1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
  const PaddleSliderValueIndicatorShape();

  // These constants define the shape of the default value indicator.
  // The value indicator changes shape based on the size of
  // the label: The top lobe spreads horizontally, and the
  // top arc on the neck moves down to keep it merging smoothly
  // with the top lobe as it expands.

  // Radius of the top lobe of the value indicator.
  static const double _topLobeRadius = 16.0;
1273 1274
  // Designed size of the label text. This is the size that the value indicator
  // was designed to contain. We scale it from here to fit other sizes.
1275
  static const double _labelTextDesignSize = 14.0;
1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286
  // Radius of the bottom lobe of the value indicator.
  static const double _bottomLobeRadius = 6.0;
  // The starting angle for the bottom lobe. Picked to get the desired
  // thickness for the neck.
  static const double _bottomLobeStartAngle = -1.1 * math.pi / 4.0;
  // The ending angle for the bottom lobe. Picked to get the desired
  // thickness for the neck.
  static const double _bottomLobeEndAngle = 1.1 * 5 * math.pi / 4.0;
  // The padding on either side of the label.
  static const double _labelPadding = 8.0;
  static const double _distanceBetweenTopBottomCenters = 40.0;
1287
  static const Offset _topLobeCenter = Offset(0.0, -_distanceBetweenTopBottomCenters);
1288 1289 1290 1291 1292 1293 1294 1295 1296
  static const double _topNeckRadius = 14.0;
  // The length of the hypotenuse of the triangle formed by the center
  // of the left top lobe arc and the center of the top left neck arc.
  // Used to calculate the position of the center of the arc.
  static const double _neckTriangleHypotenuse = _topLobeRadius + _topNeckRadius;
  // Some convenience values to help readability.
  static const double _twoSeventyDegrees = 3.0 * math.pi / 2.0;
  static const double _ninetyDegrees = math.pi / 2.0;
  static const double _thirtyDegrees = math.pi / 6.0;
1297
  static const Size _preferredSize = Size.fromHeight(_distanceBetweenTopBottomCenters + _topLobeRadius + _bottomLobeRadius);
1298 1299 1300 1301 1302
  // Set to true if you want a rectangle to be drawn around the label bubble.
  // This helps with building tests that check that the label draws in the right
  // place (because it prints the rect in the failed test output). It should not
  // be checked in while set to "true".
  static const bool _debuggingLabelLocation = false;
1303 1304 1305 1306 1307

  static Path _bottomLobePath; // Initialized by _generateBottomLobe
  static Offset _bottomLobeEnd; // Initialized by _generateBottomLobe

  @override
1308
  Size getPreferredSize(bool isEnabled, bool isDiscrete) => _preferredSize;
1309 1310 1311 1312

  // Adds an arc to the path that has the attributes passed in. This is
  // a convenience to make adding arcs have less boilerplate.
  static void _addArc(Path path, Offset center, double radius, double startAngle, double endAngle) {
1313
    final Rect arcRect = Rect.fromCircle(center: center, radius: radius);
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323
    path.arcTo(arcRect, startAngle, endAngle - startAngle, false);
  }

  // Generates the bottom lobe path, which is the same for all instances of
  // the value indicator, so we reuse it for each one.
  static void _generateBottomLobe() {
    const double bottomNeckRadius = 4.5;
    const double bottomNeckStartAngle = _bottomLobeEndAngle - math.pi;
    const double bottomNeckEndAngle = 0.0;

1324 1325
    final Path path = Path();
    final Offset bottomKnobStart = Offset(
1326 1327 1328 1329
      _bottomLobeRadius * math.cos(_bottomLobeStartAngle),
      _bottomLobeRadius * math.sin(_bottomLobeStartAngle),
    );
    final Offset bottomNeckRightCenter = bottomKnobStart +
1330
        Offset(
1331 1332 1333
          bottomNeckRadius * math.cos(bottomNeckStartAngle),
          -bottomNeckRadius * math.sin(bottomNeckStartAngle),
        );
1334
    final Offset bottomNeckLeftCenter = Offset(
1335 1336 1337
      -bottomNeckRightCenter.dx,
      bottomNeckRightCenter.dy,
    );
1338
    final Offset bottomNeckStartRight = Offset(
1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364
      bottomNeckRightCenter.dx - bottomNeckRadius,
      bottomNeckRightCenter.dy,
    );
    path.moveTo(bottomNeckStartRight.dx, bottomNeckStartRight.dy);
    _addArc(
      path,
      bottomNeckRightCenter,
      bottomNeckRadius,
      math.pi - bottomNeckEndAngle,
      math.pi - bottomNeckStartAngle,
    );
    _addArc(
      path,
      Offset.zero,
      _bottomLobeRadius,
      _bottomLobeStartAngle,
      _bottomLobeEndAngle,
    );
    _addArc(
      path,
      bottomNeckLeftCenter,
      bottomNeckRadius,
      bottomNeckStartAngle,
      bottomNeckEndAngle,
    );

1365
    _bottomLobeEnd = Offset(
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380
      -bottomNeckStartRight.dx,
      bottomNeckStartRight.dy,
    );
    _bottomLobePath = path;
  }

  Offset _addBottomLobe(Path path) {
    if (_bottomLobePath == null || _bottomLobeEnd == null) {
      // Generate this lazily so as to not slow down app startup.
      _generateBottomLobe();
    }
    path.extendWithPath(_bottomLobePath, Offset.zero);
    return _bottomLobeEnd;
  }

1381 1382 1383 1384 1385 1386 1387 1388 1389
  // Determines the "best" offset to keep the bubble on the screen. The calling
  // code will bound that with the available movement in the paddle shape.
  double _getIdealOffset(
    RenderBox parentBox,
    double halfWidthNeeded,
    double scale,
    Offset center,
  ) {
    const double edgeMargin = 4.0;
1390
    final Rect topLobeRect = Rect.fromLTWH(
1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406
      -_topLobeRadius - halfWidthNeeded,
      -_topLobeRadius - _distanceBetweenTopBottomCenters,
      2.0 * (_topLobeRadius + halfWidthNeeded),
      2.0 * _topLobeRadius,
    );
    // We can just multiply by scale instead of a transform, since we're scaling
    // around (0, 0).
    final Offset topLeft = (topLobeRect.topLeft * scale) + center;
    final Offset bottomRight = (topLobeRect.bottomRight * scale) + center;
    double shift = 0.0;
    if (topLeft.dx < edgeMargin) {
      shift = edgeMargin - topLeft.dx;
    }
    if (bottomRight.dx > parentBox.size.width - edgeMargin) {
      shift = parentBox.size.width - bottomRight.dx - edgeMargin;
    }
1407
    shift = scale == 0.0 ? 0.0 : shift / scale;
1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418
    return shift;
  }

  void _drawValueIndicator(
    RenderBox parentBox,
    Canvas canvas,
    Offset center,
    Paint paint,
    double scale,
    TextPainter labelPainter,
  ) {
1419 1420
    canvas.save();
    canvas.translate(center.dx, center.dy);
1421
    // The entire value indicator should scale with the size of the label,
1422
    // to keep it large enough to encompass the label text.
1423 1424 1425 1426
    final double textScaleFactor = labelPainter.height / _labelTextDesignSize;
    final double overallScale = scale * textScaleFactor;
    canvas.scale(overallScale, overallScale);
    final double inverseTextScale = textScaleFactor != 0 ? 1.0 / textScaleFactor : 0.0;
1427 1428 1429 1430
    final double labelHalfWidth = labelPainter.width / 2.0;

    // This is the needed extra width for the label.  It is only positive when
    // the label exceeds the minimum size contained by the round top lobe.
1431 1432 1433 1434 1435 1436 1437 1438
    final double halfWidthNeeded = math.max(
      0.0,
      inverseTextScale * labelHalfWidth - (_topLobeRadius - _labelPadding),
    );

    double shift = _getIdealOffset(parentBox, halfWidthNeeded, overallScale, center);
    double leftWidthNeeded;
    double rightWidthNeeded;
1439 1440
    if (shift < 0.0) {
      // shifting to the left
1441
      shift = math.max(shift, -halfWidthNeeded);
1442 1443
    } else {
      // shifting to the right
1444 1445 1446 1447
      shift = math.min(shift, halfWidthNeeded);
    }
    rightWidthNeeded = halfWidthNeeded + shift;
    leftWidthNeeded = halfWidthNeeded - shift;
1448

1449
    final Path path = Path();
1450
    final Offset bottomLobeEnd = _addBottomLobe(path);
1451

1452 1453 1454 1455 1456
    // The base of the triangle between the top lobe center and the centers of
    // the two top neck arcs.
    final double neckTriangleBase = _topNeckRadius - bottomLobeEnd.dx;
    // The parameter that describes how far along the transition from round to
    // stretched we are.
1457 1458
    final double leftAmount = math.max(0.0, math.min(1.0, leftWidthNeeded / neckTriangleBase));
    final double rightAmount = math.max(0.0, math.min(1.0, rightWidthNeeded / neckTriangleBase));
1459 1460
    // The angle between the top neck arc's center and the top lobe's center
    // and vertical.
1461 1462
    final double leftTheta = (1.0 - leftAmount) * _thirtyDegrees;
    final double rightTheta = (1.0 - rightAmount) * _thirtyDegrees;
1463
    // The center of the top left neck arc.
1464
    final Offset neckLeftCenter = Offset(
1465 1466 1467
      -neckTriangleBase,
      _topLobeCenter.dy + math.cos(leftTheta) * _neckTriangleHypotenuse,
    );
1468
    final Offset neckRightCenter = Offset(
1469 1470 1471 1472 1473
      neckTriangleBase,
      _topLobeCenter.dy + math.cos(rightTheta) * _neckTriangleHypotenuse,
    );
    final double leftNeckArcAngle = _ninetyDegrees - leftTheta;
    final double rightNeckArcAngle = math.pi + _ninetyDegrees - rightTheta;
1474 1475 1476 1477 1478 1479
    // The distance between the end of the bottom neck arc and the beginning of
    // the top neck arc. We use this to shrink/expand it based on the scale
    // factor of the value indicator.
    final double neckStretchBaseline = bottomLobeEnd.dy - math.max(neckLeftCenter.dy, neckRightCenter.dy);
    final double t = math.pow(inverseTextScale, 3.0);
    final double stretch = (neckStretchBaseline * t).clamp(0.0, 10.0 * neckStretchBaseline);
1480
    final Offset neckStretch = Offset(0.0, neckStretchBaseline - stretch);
1481

1482 1483
    assert(!_debuggingLabelLocation ||
        () {
1484 1485 1486
          final Offset leftCenter = _topLobeCenter - Offset(leftWidthNeeded, 0.0) + neckStretch;
          final Offset rightCenter = _topLobeCenter + Offset(rightWidthNeeded, 0.0) + neckStretch;
          final Rect valueRect = Rect.fromLTRB(
1487 1488 1489 1490 1491
            leftCenter.dx - _topLobeRadius,
            leftCenter.dy - _topLobeRadius,
            rightCenter.dx + _topLobeRadius,
            rightCenter.dy + _topLobeRadius,
          );
1492
          final Paint outlinePaint = Paint()
1493 1494 1495 1496 1497 1498
            ..color = const Color(0xffff0000)
            ..style = PaintingStyle.stroke
            ..strokeWidth = 1.0;
          canvas.drawRect(valueRect, outlinePaint);
          return true;
        }());
1499

1500 1501
    _addArc(
      path,
1502
      neckLeftCenter + neckStretch,
1503 1504
      _topNeckRadius,
      0.0,
1505 1506 1507 1508
      -leftNeckArcAngle,
    );
    _addArc(
      path,
1509
      _topLobeCenter - Offset(leftWidthNeeded, 0.0) + neckStretch,
1510 1511 1512 1513 1514 1515
      _topLobeRadius,
      _ninetyDegrees + leftTheta,
      _twoSeventyDegrees,
    );
    _addArc(
      path,
1516
      _topLobeCenter + Offset(rightWidthNeeded, 0.0) + neckStretch,
1517 1518 1519
      _topLobeRadius,
      _twoSeventyDegrees,
      _twoSeventyDegrees + math.pi - rightTheta,
1520 1521 1522
    );
    _addArc(
      path,
1523
      neckRightCenter + neckStretch,
1524
      _topNeckRadius,
1525
      rightNeckArcAngle,
1526 1527 1528 1529 1530 1531
      math.pi,
    );
    canvas.drawPath(path, paint);

    // Draw the label.
    canvas.save();
1532
    canvas.translate(shift, -_distanceBetweenTopBottomCenters + neckStretch.dy);
1533
    canvas.scale(inverseTextScale, inverseTextScale);
1534
    labelPainter.paint(canvas, Offset.zero - Offset(labelHalfWidth, labelPainter.height / 2.0));
1535 1536 1537 1538 1539 1540 1541
    canvas.restore();
    canvas.restore();
  }

  @override
  void paint(
    PaintingContext context,
1542
    Offset center, {
1543 1544
    Animation<double> activationAnimation,
    Animation<double> enableAnimation,
1545
    bool isDiscrete,
1546
    TextPainter labelPainter,
1547
    RenderBox parentBox,
1548 1549 1550
    SliderThemeData sliderTheme,
    TextDirection textDirection,
    double value,
1551
  }) {
1552
    final ColorTween enableColor = ColorTween(
1553 1554 1555
      begin: sliderTheme.disabledThumbColor,
      end: sliderTheme.valueIndicatorColor,
    );
1556
    _drawValueIndicator(
1557
      parentBox,
1558
      context.canvas,
1559
      center,
1560
      Paint()..color = enableColor.evaluate(enableAnimation),
1561
      activationAnimation.value,
1562 1563 1564
      labelPainter,
    );
  }
1565
}