slider.dart 66.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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/cupertino.dart';
9
import 'package:flutter/foundation.dart';
10 11
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
12
import 'package:flutter/scheduler.dart' show timeDilation;
13
import 'package:flutter/services.dart';
14

15 16
import 'color_scheme.dart';
import 'colors.dart';
17
import 'constants.dart';
18
import 'debug.dart';
19
import 'material.dart';
20
import 'material_state.dart';
21
import 'slider_theme.dart';
22 23
import 'theme.dart';

24
// Examples can assume:
25
// int _dollars = 0;
26
// int _duelCommandment = 1;
27
// void setState(VoidCallback fn) { }
28

29 30 31 32 33 34
/// [Slider] uses this callback to paint the value indicator on the overlay.
///
/// Since the value indicator is painted on the Overlay; this method paints the
/// value indicator in a [RenderBox] that appears in the [Overlay].
typedef PaintValueIndicator = void Function(PaintingContext context, Offset offset);

35 36
enum _SliderType { material, adaptive }

37
/// A Material Design slider.
38
///
39 40
/// Used to select from a range of values.
///
41 42
/// {@youtube 560 315 https://www.youtube.com/watch?v=ufb4gIPDmEs}
///
43
/// {@tool dartpad}
44
/// ![A legacy slider widget, consisting of 5 divisions and showing the default value
45 46 47 48 49
/// indicator.](https://flutter.github.io/assets-for-api-docs/assets/material/slider.png)
///
/// The Sliders value is part of the Stateful widget subclass to change the value
/// setState was called.
///
50
/// ** See code in examples/api/lib/material/slider/slider.0.dart **
51 52
/// {@end-tool}
///
53
/// {@tool dartpad}
54 55 56 57 58 59 60
/// This sample shows the creation of a [Slider] using [ThemeData.useMaterial3] flag,
/// as described in: https://m3.material.io/components/sliders/overview.
///
/// ** See code in examples/api/lib/material/slider/slider.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
61 62 63
/// This example shows a [Slider] widget using the [Slider.secondaryTrackValue]
/// to show a secondary track in the slider.
///
64
/// ** See code in examples/api/lib/material/slider/slider.2.dart **
65 66
/// {@end-tool}
///
67
/// A slider can be used to select from either a continuous or a discrete set of
68 69
/// 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
70
/// indicates the number of discrete intervals. For example, if [min] is 0.0 and
71
/// [max] is 50.0 and [divisions] is 5, then the slider can take on the
72
/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0.
73
///
74 75 76 77
/// The terms for the parts of a slider are:
///
///  * The "thumb", which is a shape that slides horizontally when the user
///    drags it.
78
///  * The "track", which is the line that the slider thumb slides along.
79 80 81 82 83 84 85
///  * 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.
///
86 87 88
/// 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]).
///
89 90 91 92
/// 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
93 94
/// slider. To know when the value starts to change, or when it is done
/// changing, set the optional callbacks [onChangeStart] and/or [onChangeEnd].
95
///
96
/// By default, a slider will be as wide as possible, centered vertically. When
97
/// given unbounded constraints, it will attempt to make the track 144 pixels
98
/// wide (with margins on each side) and will shrink-wrap vertically.
99
///
100 101
/// Requires one of its ancestors to be a [Material] widget.
///
102 103 104 105
/// 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.
///
106 107 108 109 110 111 112
/// 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].
///
113
/// See also:
114
///
115 116
///  * [SliderTheme] and [SliderThemeData] for information about controlling
///    the visual appearance of the slider.
117 118
///  * [Radio], for selecting among a set of explicit values.
///  * [Checkbox] and [Switch], for toggling a particular value on or off.
119
///  * <https://material.io/design/components/sliders.html>
120
///  * [MediaQuery], from which the text scale factor is obtained.
121
class Slider extends StatefulWidget {
122
  /// Creates a Material Design slider.
123 124
  ///
  /// The slider itself does not maintain any state. Instead, when the state of
125 126 127 128
  /// 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.
129 130
  ///
  /// * [value] determines currently selected value for this slider.
131 132 133 134 135 136
  /// * [onChanged] is called while the user is selecting a new value for the
  ///   slider.
  /// * [onChangeStart] is called when the user starts to select a new value for
  ///   the slider.
  /// * [onChangeEnd] is called when the user is done selecting a new value for
  ///   the slider.
137 138 139 140
  ///
  /// 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].
141
  const Slider({
142
    super.key,
143
    required this.value,
144
    this.secondaryTrackValue,
145
    required this.onChanged,
146 147
    this.onChangeStart,
    this.onChangeEnd,
148 149
    this.min = 0.0,
    this.max = 1.0,
150 151
    this.divisions,
    this.label,
152
    this.activeColor,
153
    this.inactiveColor,
154
    this.secondaryActiveColor,
155
    this.thumbColor,
156
    this.overlayColor,
157
    this.mouseCursor,
158
    this.semanticFormatterCallback,
159 160
    this.focusNode,
    this.autofocus = false,
161 162
  }) : _sliderType = _SliderType.material,
       assert(min <= max),
163 164
       assert(value >= min && value <= max,
         'Value $value is not between minimum $min and maximum $max'),
165 166
       assert(secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max),
         'SecondaryValue $secondaryTrackValue is not between $min and $max'),
167
       assert(divisions == null || divisions > 0);
168

169 170 171 172
  /// Creates an adaptive [Slider] based on the target platform, following
  /// Material design's
  /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
  ///
173
  /// Creates a [CupertinoSlider] if the target platform is iOS or macOS, creates a
174 175
  /// Material Design slider otherwise.
  ///
176 177 178
  /// If a [CupertinoSlider] is created, the following parameters are ignored:
  /// [secondaryTrackValue], [label], [inactiveColor], [secondaryActiveColor],
  /// [semanticFormatterCallback].
179 180 181
  ///
  /// The target platform is based on the current [Theme]: [ThemeData.platform].
  const Slider.adaptive({
182
    super.key,
183
    required this.value,
184
    this.secondaryTrackValue,
185
    required this.onChanged,
186 187 188 189 190 191
    this.onChangeStart,
    this.onChangeEnd,
    this.min = 0.0,
    this.max = 1.0,
    this.divisions,
    this.label,
192
    this.mouseCursor,
193 194
    this.activeColor,
    this.inactiveColor,
195
    this.secondaryActiveColor,
196
    this.thumbColor,
197
    this.overlayColor,
198
    this.semanticFormatterCallback,
199 200
    this.focusNode,
    this.autofocus = false,
201
  }) : _sliderType = _SliderType.adaptive,
202
       assert(min <= max),
203 204 205 206
       assert(value >= min && value <= max,
         'Value $value is not between minimum $min and maximum $max'),
       assert(secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max),
         'SecondaryValue $secondaryTrackValue is not between $min and $max'),
207
       assert(divisions == null || divisions > 0);
208

209 210 211
  /// The currently selected value for this slider.
  ///
  /// The slider's thumb is drawn at a position that corresponds to this value.
212
  final double value;
213

214 215 216 217 218 219 220 221 222 223 224
  /// The secondary track value for this slider.
  ///
  /// If not null, a secondary track using [Slider.secondaryActiveColor] color
  /// is drawn between the thumb and this value, over the inactive track.
  ///
  /// If less than [Slider.value], then the secondary track is not shown.
  ///
  /// It can be ideal for media scenarios such as showing the buffering progress
  /// while the [Slider.value] shows the play progress.
  final double? secondaryTrackValue;

225 226
  /// Called during a drag when the user is selecting a new value for the slider
  /// by dragging.
227 228 229 230 231 232
  ///
  /// 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.
233 234 235 236 237
  ///
  /// 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:
  ///
238
  /// {@tool snippet}
239
  ///
240
  /// ```dart
241
  /// Slider(
242 243 244 245 246 247 248 249 250 251
  ///   value: _duelCommandment.toDouble(),
  ///   min: 1.0,
  ///   max: 10.0,
  ///   divisions: 10,
  ///   label: '$_duelCommandment',
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _duelCommandment = newValue.round();
  ///     });
  ///   },
252
  /// )
253
  /// ```
254
  /// {@end-tool}
255 256 257 258 259 260 261
  ///
  /// See also:
  ///
  ///  * [onChangeStart] for a callback that is called when the user starts
  ///    changing the value.
  ///  * [onChangeEnd] for a callback that is called when the user stops
  ///    changing the value.
262
  final ValueChanged<double>? onChanged;
263

264 265 266 267 268 269 270 271 272
  /// Called when the user starts selecting a new value for the slider.
  ///
  /// This callback shouldn't be used to update the slider [value] (use
  /// [onChanged] for that), but rather to be notified when the user has started
  /// selecting a new value by starting a drag or with a tap.
  ///
  /// The value passed will be the last [value] that the slider had before the
  /// change began.
  ///
273
  /// {@tool snippet}
274 275
  ///
  /// ```dart
276
  /// Slider(
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
  ///   value: _duelCommandment.toDouble(),
  ///   min: 1.0,
  ///   max: 10.0,
  ///   divisions: 10,
  ///   label: '$_duelCommandment',
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _duelCommandment = newValue.round();
  ///     });
  ///   },
  ///   onChangeStart: (double startValue) {
  ///     print('Started change at $startValue');
  ///   },
  /// )
  /// ```
292
  /// {@end-tool}
293 294 295 296 297
  ///
  /// See also:
  ///
  ///  * [onChangeEnd] for a callback that is called when the value change is
  ///    complete.
298
  final ValueChanged<double>? onChangeStart;
299 300 301 302 303 304 305

  /// Called when the user is done selecting a new value for the slider.
  ///
  /// This callback shouldn't be used to update the slider [value] (use
  /// [onChanged] for that), but rather to know when the user has completed
  /// selecting a new [value] by ending a drag or a click.
  ///
306
  /// {@tool snippet}
307 308
  ///
  /// ```dart
309
  /// Slider(
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
  ///   value: _duelCommandment.toDouble(),
  ///   min: 1.0,
  ///   max: 10.0,
  ///   divisions: 10,
  ///   label: '$_duelCommandment',
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _duelCommandment = newValue.round();
  ///     });
  ///   },
  ///   onChangeEnd: (double newValue) {
  ///     print('Ended change on $newValue');
  ///   },
  /// )
  /// ```
325
  /// {@end-tool}
326 327 328 329 330
  ///
  /// See also:
  ///
  ///  * [onChangeStart] for a callback that is called when a value change
  ///    begins.
331
  final ValueChanged<double>? onChangeEnd;
332

333
  /// The minimum value the user can select.
334
  ///
335 336 337
  /// 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
338
  final double min;
339 340 341

  /// The maximum value the user can select.
  ///
342 343 344
  /// 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
345
  final double max;
346

347 348 349 350 351
  /// The number of discrete divisions.
  ///
  /// Typically used with [label] to show the current discrete value.
  ///
  /// If null, the slider is continuous.
352
  final int? divisions;
353

354 355
  /// A label to show above the slider when the slider is active and
  /// [SliderThemeData.showValueIndicator] is satisfied.
356
  ///
357 358 359
  /// It is used to display the value of a discrete slider, and it is displayed
  /// as part of the value indicator shape.
  ///
360
  /// The label is rendered using the active [ThemeData]'s [TextTheme.bodyLarge]
361 362 363
  /// text style, with the theme data's [ColorScheme.onPrimary] color. The
  /// label's text style can be overridden with
  /// [SliderThemeData.valueIndicatorTextStyle].
364 365 366
  ///
  /// If null, then the value indicator will not be displayed.
  ///
367 368
  /// Ignored if this slider is created with [Slider.adaptive].
  ///
369 370 371 372
  /// See also:
  ///
  ///  * [SliderComponentShape] for how to create a custom value indicator
  ///    shape.
373
  final String? label;
374

375
  /// The color to use for the portion of the slider track that is active.
376
  ///
377 378
  /// The "active" side of the slider is the side between the thumb and the
  /// minimum value.
379
  ///
380 381 382
  /// If null, [SliderThemeData.activeTrackColor] of the ambient
  /// [SliderTheme] is used. If that is null, [ColorScheme.primary] of the
  /// surrounding [ThemeData] is used.
383 384 385
  ///
  /// Using a [SliderTheme] gives much more fine-grained control over the
  /// appearance of various components of the slider.
386
  final Color? activeColor;
387

388
  /// The color for the inactive portion of the slider track.
389
  ///
390 391
  /// The "inactive" side of the slider is the side between the thumb and the
  /// maximum value.
392
  ///
393 394 395 396
  /// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme]
  /// is used. If that is null and [ThemeData.useMaterial3] is true,
  /// [ColorScheme.surfaceVariant] will be used, otherwise [ColorScheme.primary]
  /// with an opacity of 0.24 will be used.
397
  ///
398 399
  /// Using a [SliderTheme] gives much more fine-grained control over the
  /// appearance of various components of the slider.
400 401
  ///
  /// Ignored if this slider is created with [Slider.adaptive].
402
  final Color? inactiveColor;
403

404 405 406 407 408 409
  /// The color to use for the portion of the slider track between the thumb and
  /// the [Slider.secondaryTrackValue].
  ///
  /// Defaults to the [SliderThemeData.secondaryActiveTrackColor] of the current
  /// [SliderTheme].
  ///
410 411 412
  /// If that is also null, defaults to [ColorScheme.primary] with an
  /// opacity of 0.54.
  ///
413 414 415 416 417 418
  /// Using a [SliderTheme] gives much more fine-grained control over the
  /// appearance of various components of the slider.
  ///
  /// Ignored if this slider is created with [Slider.adaptive].
  final Color? secondaryActiveColor;

419 420
  /// The color of the thumb.
  ///
421 422 423 424 425
  /// If this color is null, [Slider] will use [activeColor], If [activeColor]
  /// is also null, [Slider] will use [SliderThemeData.thumbColor].
  ///
  /// If that is also null, defaults to [ColorScheme.primary].
  ///
426 427 428 429
  /// * [CupertinoSlider] will have a white thumb
  /// (like the native default iOS slider).
  final Color? thumbColor;

430
  /// The highlight color that's typically used to indicate that
431
  /// the slider thumb is focused, hovered, or dragged.
432 433
  ///
  /// If this property is null, [Slider] will use [activeColor] with
434
  /// an opacity of 0.12, If null, [SliderThemeData.overlayColor]
435 436 437 438 439 440 441
  /// will be used.
  ///
  /// If that is also null, If [ThemeData.useMaterial3] is true,
  /// Slider will use [ColorScheme.primary] with an opacity of 0.08 when
  /// slider thumb is hovered and with an opacity of 0.12 when slider thumb
  /// is focused or dragged, If [ThemeData.useMaterial3] is false, defaults
  /// to [ColorScheme.primary] with an opacity of 0.12.
442 443
  final MaterialStateProperty<Color?>? overlayColor;

444
  /// {@template flutter.material.slider.mouseCursor}
445 446 447 448 449 450
  /// The cursor for a mouse pointer when it enters or is hovering over the
  /// widget.
  ///
  /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
  /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
  ///
451
  ///  * [MaterialState.dragged].
452 453 454
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.disabled].
455
  /// {@endtemplate}
456
  ///
457 458 459 460 461 462 463
  /// If null, then the value of [SliderThemeData.mouseCursor] is used. If that
  /// is also null, then [MaterialStateMouseCursor.clickable] is used.
  ///
  /// See also:
  ///
  ///  * [MaterialStateMouseCursor], which can be used to create a [MouseCursor]
  ///    that is also a [MaterialStateProperty<MouseCursor>].
464
  final MouseCursor? mouseCursor;
465

466 467 468 469 470 471 472
  /// The callback used to create a semantic value from a slider value.
  ///
  /// Defaults to formatting values as a percentage.
  ///
  /// This is used by accessibility frameworks like TalkBack on Android to
  /// inform users what the currently selected value is with more context.
  ///
473
  /// {@tool snippet}
474 475 476 477 478
  ///
  /// In the example below, a slider for currency values is configured to
  /// announce a value with a currency label.
  ///
  /// ```dart
479
  /// Slider(
480 481 482 483 484 485 486 487 488 489 490 491 492 493
  ///   value: _dollars.toDouble(),
  ///   min: 20.0,
  ///   max: 330.0,
  ///   label: '$_dollars dollars',
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _dollars = newValue.round();
  ///     });
  ///   },
  ///   semanticFormatterCallback: (double newValue) {
  ///     return '${newValue.round()} dollars';
  ///   }
  ///  )
  /// ```
494
  /// {@end-tool}
495 496
  ///
  /// Ignored if this slider is created with [Slider.adaptive]
497
  final SemanticFormatterCallback? semanticFormatterCallback;
498

499
  /// {@macro flutter.widgets.Focus.focusNode}
500
  final FocusNode? focusNode;
501 502 503 504

  /// {@macro flutter.widgets.Focus.autofocus}
  final bool autofocus;

505 506
  final _SliderType _sliderType ;

507
  @override
508
  State<Slider> createState() => _SliderState();
509 510

  @override
511 512
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
513
    properties.add(DoubleProperty('value', value));
514
    properties.add(DoubleProperty('secondaryTrackValue', secondaryTrackValue));
515 516 517
    properties.add(ObjectFlagProperty<ValueChanged<double>>('onChanged', onChanged, ifNull: 'disabled'));
    properties.add(ObjectFlagProperty<ValueChanged<double>>.has('onChangeStart', onChangeStart));
    properties.add(ObjectFlagProperty<ValueChanged<double>>.has('onChangeEnd', onChangeEnd));
518 519
    properties.add(DoubleProperty('min', min));
    properties.add(DoubleProperty('max', max));
520 521 522 523
    properties.add(IntProperty('divisions', divisions));
    properties.add(StringProperty('label', label));
    properties.add(ColorProperty('activeColor', activeColor));
    properties.add(ColorProperty('inactiveColor', inactiveColor));
524
    properties.add(ColorProperty('secondaryActiveColor', secondaryActiveColor));
525
    properties.add(ObjectFlagProperty<ValueChanged<double>>.has('semanticFormatterCallback', semanticFormatterCallback));
526 527
    properties.add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode));
    properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'autofocus'));
528
  }
529 530 531
}

class _SliderState extends State<Slider> with TickerProviderStateMixin {
532 533
  static const Duration enableAnimationDuration = Duration(milliseconds: 75);
  static const Duration valueIndicatorAnimationDuration = Duration(milliseconds: 100);
534 535 536

  // Animation controller that is run when the overlay (a.k.a radial reaction)
  // is shown in response to user interaction.
537
  late AnimationController overlayController;
538 539
  // Animation controller that is run when the value indicator is being shown
  // or hidden.
540
  late AnimationController valueIndicatorController;
541
  // Animation controller that is run when enabling/disabling the slider.
542
  late AnimationController enableController;
543 544
  // Animation controller that is run when transitioning between one value
  // and the next on a discrete slider.
545 546
  late AnimationController positionController;
  Timer? interactionTimer;
547 548

  final GlobalKey _renderObjectKey = GlobalKey();
549

550
  // Keyboard mapping for a focused slider.
551
  static const Map<ShortcutActivator, Intent> _traditionalNavShortcutMap = <ShortcutActivator, Intent>{
552 553 554 555 556
    SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustSliderIntent.up(),
    SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustSliderIntent.down(),
    SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(),
    SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(),
  };
557 558 559 560

  // Keyboard mapping for a focused slider when using directional navigation.
  // The vertical inputs are not handled to allow navigating out of the slider.
  static const Map<ShortcutActivator, Intent> _directionalNavShortcutMap = <ShortcutActivator, Intent>{
561 562 563
    SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(),
    SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(),
  };
564

565
  // Action mapping for a focused slider.
566
  late Map<Type, Action<Intent>> _actionMap;
567 568

  bool get _enabled => widget.onChanged != null;
569
  // Value Indicator Animation that appears on the Overlay.
570
  PaintValueIndicator? paintValueIndicator;
571

572 573
  bool _dragging = false;

574 575 576
  FocusNode? _focusNode;
  FocusNode get focusNode => widget.focusNode ?? _focusNode!;

577 578 579
  @override
  void initState() {
    super.initState();
580
    overlayController = AnimationController(
581 582 583
      duration: kRadialReactionDuration,
      vsync: this,
    );
584
    valueIndicatorController = AnimationController(
585 586 587
      duration: valueIndicatorAnimationDuration,
      vsync: this,
    );
588
    enableController = AnimationController(
589 590 591
      duration: enableAnimationDuration,
      vsync: this,
    );
592
    positionController = AnimationController(
593
      duration: Duration.zero,
594 595
      vsync: this,
    );
596
    enableController.value = widget.onChanged != null ? 1.0 : 0.0;
597
    positionController.value = _convert(widget.value);
598 599 600 601 602
    _actionMap = <Type, Action<Intent>>{
      _AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>(
        onInvoke: _actionHandler,
      ),
    };
603 604 605 606
    if (widget.focusNode == null) {
      // Only create a new node if the widget doesn't have one.
      _focusNode ??= FocusNode();
    }
607 608 609 610
  }

  @override
  void dispose() {
611
    interactionTimer?.cancel();
612 613
    overlayController.dispose();
    valueIndicatorController.dispose();
614 615
    enableController.dispose();
    positionController.dispose();
616
    if (overlayEntry != null) {
617
      overlayEntry!.remove();
618 619
      overlayEntry = null;
    }
620
    _focusNode?.dispose();
621
    super.dispose();
622 623
  }

Hixie's avatar
Hixie committed
624
  void _handleChanged(double value) {
625
    assert(widget.onChanged != null);
626 627
    final double lerpValue = _lerp(value);
    if (lerpValue != widget.value) {
628
      widget.onChanged!(lerpValue);
629
      _focusNode?.requestFocus();
630
    }
Hixie's avatar
Hixie committed
631 632
  }

633
  void _handleDragStart(double value) {
634 635
    _dragging = true;
    widget.onChangeStart?.call(_lerp(value));
636 637 638
  }

  void _handleDragEnd(double value) {
639 640
    _dragging = false;
    widget.onChangeEnd?.call(_lerp(value));
641 642
  }

643
  void _actionHandler(_AdjustSliderIntent intent) {
644
    final _RenderSlider renderSlider = _renderObjectKey.currentContext!.findRenderObject()! as _RenderSlider;
645
    final TextDirection textDirection = Directionality.of(_renderObjectKey.currentContext!);
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
    switch (intent.type) {
      case _SliderAdjustmentType.right:
        switch (textDirection) {
          case TextDirection.rtl:
            renderSlider.decreaseAction();
            break;
          case TextDirection.ltr:
            renderSlider.increaseAction();
            break;
        }
        break;
      case _SliderAdjustmentType.left:
        switch (textDirection) {
          case TextDirection.rtl:
            renderSlider.increaseAction();
            break;
          case TextDirection.ltr:
            renderSlider.decreaseAction();
            break;
        }
        break;
      case _SliderAdjustmentType.up:
        renderSlider.increaseAction();
        break;
      case _SliderAdjustmentType.down:
        renderSlider.decreaseAction();
        break;
    }
  }

  bool _focused = false;
  void _handleFocusHighlightChanged(bool focused) {
    if (focused != _focused) {
      setState(() { _focused = focused; });
    }
  }

  bool _hovering = false;
  void _handleHoverChanged(bool hovering) {
    if (hovering != _hovering) {
      setState(() { _hovering = hovering; });
    }
  }

690 691 692 693 694 695
  // 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;
696 697
  }

698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
  double _discretize(double value) {
    assert(widget.divisions != null);
    assert(value >= 0.0 && value <= 1.0);

    final int divisions = widget.divisions!;
    return (value * divisions).round() / divisions;
  }

  double _convert(double value) {
    double ret = _unlerp(value);
    if (widget.divisions != null) {
      ret = _discretize(ret);
    }
    return ret;
  }

714 715 716 717
  // 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);
718
    return widget.max > widget.min ? (value - widget.min) / (widget.max - widget.min) : 0.0;
719
  }
720

721
  @override
722
  Widget build(BuildContext context) {
723
    assert(debugCheckHasMaterial(context));
724
    assert(debugCheckHasMediaQuery(context));
725

726 727 728 729 730
    switch (widget._sliderType) {
      case _SliderType.material:
        return _buildMaterialSlider(context);

      case _SliderType.adaptive: {
731
        final ThemeData theme = Theme.of(context);
732 733 734
        switch (theme.platform) {
          case TargetPlatform.android:
          case TargetPlatform.fuchsia:
735 736
          case TargetPlatform.linux:
          case TargetPlatform.windows:
737 738
            return _buildMaterialSlider(context);
          case TargetPlatform.iOS:
739
          case TargetPlatform.macOS:
740 741 742 743 744 745 746
            return _buildCupertinoSlider(context);
        }
      }
    }
  }

  Widget _buildMaterialSlider(BuildContext context) {
747
    final ThemeData theme = Theme.of(context);
748
    SliderThemeData sliderTheme = SliderTheme.of(context);
749
    final SliderThemeData defaults = theme.useMaterial3 ? _SliderDefaultsM3(context) : _SliderDefaultsM2(context);
750 751 752

    // 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
753 754 755 756
    // control than that, then they need to use a SliderTheme. The default
    // colors come from the ThemeData.colorScheme. These colors, along with
    // the default shapes and text styles are aligned to the Material
    // Guidelines.
757

758 759 760 761
    const SliderTrackShape defaultTrackShape = RoundedRectSliderTrackShape();
    const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();
    const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();
    const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();
762
    final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;
763
    const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;
764

765 766 767 768 769 770 771
    final Set<MaterialState> states = <MaterialState>{
      if (!_enabled) MaterialState.disabled,
      if (_hovering) MaterialState.hovered,
      if (_focused) MaterialState.focused,
      if (_dragging) MaterialState.dragged,
    };

772 773 774 775
    // The value indicator's color is not the same as the thumb and active track
    // (which can be defined by activeColor) if the
    // RectangularSliderValueIndicatorShape is used. In all other cases, the
    // value indicator is assumed to be the same as the active color.
776
    final SliderComponentShape valueIndicatorShape = sliderTheme.valueIndicatorShape ?? defaultValueIndicatorShape;
777
    final Color valueIndicatorColor;
778 779 780 781 782 783
    if (valueIndicatorShape is RectangularSliderValueIndicatorShape) {
      valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(0.60), theme.colorScheme.surface.withOpacity(0.90));
    } else {
      valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;
    }

784 785 786 787
    Color? effectiveOverlayColor() {
      return widget.overlayColor?.resolve(states)
        ?? widget.activeColor?.withOpacity(0.12)
        ?? MaterialStateProperty.resolveAs<Color?>(sliderTheme.overlayColor, states)
788
        ?? MaterialStateProperty.resolveAs<Color?>(defaults.overlayColor, states);
789 790
    }

791
    sliderTheme = sliderTheme.copyWith(
792 793 794 795 796 797 798 799 800 801 802 803 804
      trackHeight: sliderTheme.trackHeight ?? defaults.trackHeight,
      activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? defaults.activeTrackColor,
      inactiveTrackColor: widget.inactiveColor ?? sliderTheme.inactiveTrackColor ?? defaults.inactiveTrackColor,
      secondaryActiveTrackColor: widget.secondaryActiveColor ?? sliderTheme.secondaryActiveTrackColor ?? defaults.secondaryActiveTrackColor,
      disabledActiveTrackColor: sliderTheme.disabledActiveTrackColor ?? defaults.disabledActiveTrackColor,
      disabledInactiveTrackColor: sliderTheme.disabledInactiveTrackColor ?? defaults.disabledInactiveTrackColor,
      disabledSecondaryActiveTrackColor: sliderTheme.disabledSecondaryActiveTrackColor ?? defaults.disabledSecondaryActiveTrackColor,
      activeTickMarkColor: widget.inactiveColor ?? sliderTheme.activeTickMarkColor ?? defaults.activeTickMarkColor,
      inactiveTickMarkColor: widget.activeColor ?? sliderTheme.inactiveTickMarkColor ?? defaults.inactiveTickMarkColor,
      disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? defaults.disabledActiveTickMarkColor,
      disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? defaults.disabledInactiveTickMarkColor,
      thumbColor: widget.thumbColor ?? widget.activeColor ?? sliderTheme.thumbColor ?? defaults.thumbColor,
      disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,
805
      overlayColor: effectiveOverlayColor(),
806
      valueIndicatorColor: valueIndicatorColor,
807 808 809 810
      trackShape: sliderTheme.trackShape ?? defaultTrackShape,
      tickMarkShape: sliderTheme.tickMarkShape ?? defaultTickMarkShape,
      thumbShape: sliderTheme.thumbShape ?? defaultThumbShape,
      overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,
811
      valueIndicatorShape: valueIndicatorShape,
812
      showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,
813
      valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? defaults.valueIndicatorTextStyle,
814
    );
815 816 817
    final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
      ?? sliderTheme.mouseCursor?.resolve(states)
      ?? MaterialStateMouseCursor.clickable.resolve(states);
818

819 820 821
    // This size is used as the max bounds for the painting of the value
    // indicators It must be kept in sync with the function with the same name
    // in range_slider.dart.
822
    Size screenSize() => MediaQuery.sizeOf(context);
823

824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839
    VoidCallback? handleDidGainAccessibilityFocus;
    switch (theme.platform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.iOS:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
        break;
      case TargetPlatform.windows:
        handleDidGainAccessibilityFocus = () {
          // Automatically activate the slider when it receives a11y focus.
          if (!focusNode.hasFocus && focusNode.canRequestFocus) {
            focusNode.requestFocus();
          }
        };
        break;
840 841
    }

842
    final Map<ShortcutActivator, Intent> shortcutMap;
843
    switch (MediaQuery.navigationModeOf(context)) {
844 845 846 847 848 849 850 851
      case NavigationMode.directional:
        shortcutMap = _directionalNavShortcutMap;
        break;
      case NavigationMode.traditional:
        shortcutMap = _traditionalNavShortcutMap;
        break;
    }

852 853 854 855 856
    final double textScaleFactor = theme.useMaterial3
      // TODO(tahatesser): This is an eye-balled value.
      // This needs to be updated when accessibility
      // guidelines are available on the material specs page
      // https://m3.material.io/components/sliders/accessibility.
857 858
      ? math.min(MediaQuery.textScaleFactorOf(context), 1.3)
      : MediaQuery.textScaleFactorOf(context);
859

860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
    return Semantics(
      container: true,
      slider: true,
      onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
      child: FocusableActionDetector(
        actions: _actionMap,
        shortcuts: shortcutMap,
        focusNode: focusNode,
        autofocus: widget.autofocus,
        enabled: _enabled,
        onShowFocusHighlight: _handleFocusHighlightChanged,
        onShowHoverHighlight: _handleHoverChanged,
        mouseCursor: effectiveMouseCursor,
        child: CompositedTransformTarget(
          link: _layerLink,
          child: _SliderRenderObjectWidget(
            key: _renderObjectKey,
            value: _convert(widget.value),
            secondaryTrackValue: (widget.secondaryTrackValue != null) ? _convert(widget.secondaryTrackValue!) : null,
            divisions: widget.divisions,
            label: widget.label,
            sliderTheme: sliderTheme,
            textScaleFactor: textScaleFactor,
            screenSize: screenSize(),
            onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,
            onChangeStart: _handleDragStart,
            onChangeEnd: _handleDragEnd,
            state: this,
            semanticFormatterCallback: widget.semanticFormatterCallback,
            hasFocus: _focused,
            hovering: _hovering,
          ),
892
        ),
893
      ),
894 895
    );
  }
896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911

  Widget _buildCupertinoSlider(BuildContext context) {
    // The render box of a slider has a fixed height but takes up the available
    // width. Wrapping the [CupertinoSlider] in this manner will help maintain
    // the same size.
    return SizedBox(
      width: double.infinity,
      child: CupertinoSlider(
        value: widget.value,
        onChanged: widget.onChanged,
        onChangeStart: widget.onChangeStart,
        onChangeEnd: widget.onChangeEnd,
        min: widget.min,
        max: widget.max,
        divisions: widget.divisions,
        activeColor: widget.activeColor,
912
        thumbColor: widget.thumbColor ?? CupertinoColors.white,
913 914 915
      ),
    );
  }
916 917
  final LayerLink _layerLink = LayerLink();

918
  OverlayEntry? overlayEntry;
919 920 921 922 923 924 925 926 927 928 929 930 931

  void showValueIndicator() {
    if (overlayEntry == null) {
      overlayEntry = OverlayEntry(
        builder: (BuildContext context) {
          return CompositedTransformFollower(
            link: _layerLink,
            child: _ValueIndicatorRenderObjectWidget(
              state: this,
            ),
          );
        },
      );
932
      Overlay.of(context, debugRequiredFor: widget).insert(overlayEntry!);
933 934
    }
  }
935 936 937
}

class _SliderRenderObjectWidget extends LeafRenderObjectWidget {
938
  const _SliderRenderObjectWidget({
939
    super.key,
940
    required this.value,
941
    required this.secondaryTrackValue,
942 943 944 945 946 947 948 949 950 951 952 953
    required this.divisions,
    required this.label,
    required this.sliderTheme,
    required this.textScaleFactor,
    required this.screenSize,
    required this.onChanged,
    required this.onChangeStart,
    required this.onChangeEnd,
    required this.state,
    required this.semanticFormatterCallback,
    required this.hasFocus,
    required this.hovering,
954
  });
955 956

  final double value;
957
  final double? secondaryTrackValue;
958 959
  final int? divisions;
  final String? label;
960
  final SliderThemeData sliderTheme;
961 962
  final double textScaleFactor;
  final Size screenSize;
963 964 965 966
  final ValueChanged<double>? onChanged;
  final ValueChanged<double>? onChangeStart;
  final ValueChanged<double>? onChangeEnd;
  final SemanticFormatterCallback? semanticFormatterCallback;
967
  final _SliderState state;
968 969
  final bool hasFocus;
  final bool hovering;
970

971
  @override
972
  _RenderSlider createRenderObject(BuildContext context) {
973
    return _RenderSlider(
974
      value: value,
975
      secondaryTrackValue: secondaryTrackValue,
976 977
      divisions: divisions,
      label: label,
978
      sliderTheme: sliderTheme,
979 980
      textScaleFactor: textScaleFactor,
      screenSize: screenSize,
981
      onChanged: onChanged,
982 983
      onChangeStart: onChangeStart,
      onChangeEnd: onChangeEnd,
984
      state: state,
985
      textDirection: Directionality.of(context),
986
      semanticFormatterCallback: semanticFormatterCallback,
987
      platform: Theme.of(context).platform,
988 989
      hasFocus: hasFocus,
      hovering: hovering,
990
      gestureSettings: MediaQuery.gestureSettingsOf(context),
991 992
    );
  }
993

994
  @override
995
  void updateRenderObject(BuildContext context, _RenderSlider renderObject) {
996
    renderObject
997 998
      // We should update the `divisions` ahead of `value`, because the `value`
      // setter dependent on the `divisions`.
999
      ..divisions = divisions
1000
      ..value = value
1001
      ..secondaryTrackValue = secondaryTrackValue
1002
      ..label = label
1003
      ..sliderTheme = sliderTheme
1004 1005
      ..textScaleFactor = textScaleFactor
      ..screenSize = screenSize
1006
      ..onChanged = onChanged
1007 1008
      ..onChangeStart = onChangeStart
      ..onChangeEnd = onChangeEnd
1009
      ..textDirection = Directionality.of(context)
1010
      ..semanticFormatterCallback = semanticFormatterCallback
1011
      ..platform = Theme.of(context).platform
1012
      ..hasFocus = hasFocus
1013
      ..hovering = hovering
1014
      ..gestureSettings = MediaQuery.gestureSettingsOf(context);
1015 1016
    // Ticker provider cannot change since there's a 1:1 relationship between
    // the _SliderRenderObjectWidget object and the _SliderState object.
1017 1018 1019
  }
}

1020
class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
1021
  _RenderSlider({
1022
    required double value,
1023
    required double? secondaryTrackValue,
1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037
    required int? divisions,
    required String? label,
    required SliderThemeData sliderTheme,
    required double textScaleFactor,
    required Size screenSize,
    required TargetPlatform platform,
    required ValueChanged<double>? onChanged,
    required SemanticFormatterCallback? semanticFormatterCallback,
    required this.onChangeStart,
    required this.onChangeEnd,
    required _SliderState state,
    required TextDirection textDirection,
    required bool hasFocus,
    required bool hovering,
1038
    required DeviceGestureSettings gestureSettings,
1039
  }) : assert(value >= 0.0 && value <= 1.0),
1040
        assert(secondaryTrackValue == null || (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0)),
1041 1042 1043 1044
        _platform = platform,
        _semanticFormatterCallback = semanticFormatterCallback,
        _label = label,
        _value = value,
1045
        _secondaryTrackValue = secondaryTrackValue,
1046 1047 1048 1049 1050 1051 1052
        _divisions = divisions,
        _sliderTheme = sliderTheme,
        _textScaleFactor = textScaleFactor,
        _screenSize = screenSize,
        _onChanged = onChanged,
        _state = state,
        _textDirection = textDirection,
1053
        _hasFocus = hasFocus,
1054
        _hovering = hovering {
Ian Hickson's avatar
Ian Hickson committed
1055
    _updateLabelPainter();
1056 1057
    final GestureArenaTeam team = GestureArenaTeam();
    _drag = HorizontalDragGestureRecognizer()
1058
      ..team = team
1059 1060
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
1061
      ..onEnd = _handleDragEnd
1062 1063
      ..onCancel = _endInteraction
      ..gestureSettings = gestureSettings;
1064
    _tap = TapGestureRecognizer()
1065
      ..team = team
1066
      ..onTapDown = _handleTapDown
1067 1068
      ..onTapUp = _handleTapUp
      ..gestureSettings = gestureSettings;
1069
    _overlayAnimation = CurvedAnimation(
1070 1071 1072
      parent: _state.overlayController,
      curve: Curves.fastOutSlowIn,
    );
1073
    _valueIndicatorAnimation = CurvedAnimation(
1074
      parent: _state.valueIndicatorController,
1075
      curve: Curves.fastOutSlowIn,
1076 1077
    )..addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.dismissed && _state.overlayEntry != null) {
1078
        _state.overlayEntry!.remove();
1079 1080 1081
        _state.overlayEntry = null;
      }
    });
1082
    _enableAnimation = CurvedAnimation(
1083 1084 1085
      parent: _state.enableController,
      curve: Curves.easeInOut,
    );
1086
  }
1087 1088
  static const Duration _positionAnimationDuration = Duration(milliseconds: 75);
  static const Duration _minimumInteractionTime = Duration(milliseconds: 500);
1089 1090 1091 1092 1093 1094 1095 1096

  // This value is the touch target, 48, multiplied by 3.
  static const double _minPreferredTrackWidth = 144.0;

  // Compute the largest width and height needed to paint the slider shapes,
  // other than the track shape. It is assumed that these shapes are vertically
  // centered on the track.
  double get _maxSliderPartWidth => _sliderPartSizes.map((Size size) => size.width).reduce(math.max);
1097
  double get _maxSliderPartHeight => _sliderPartSizes.map((Size size) => size.height).reduce(math.max);
1098
  List<Size> get _sliderPartSizes => <Size>[
1099 1100 1101
    _sliderTheme.overlayShape!.getPreferredSize(isInteractive, isDiscrete),
    _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete),
    _sliderTheme.tickMarkShape!.getPreferredSize(isEnabled: isInteractive, sliderTheme: sliderTheme),
1102
  ];
1103
  double get _minPreferredTrackHeight => _sliderTheme.trackHeight!;
1104

1105
  final _SliderState _state;
1106 1107 1108
  late Animation<double> _overlayAnimation;
  late Animation<double> _valueIndicatorAnimation;
  late Animation<double> _enableAnimation;
1109
  final TextPainter _labelPainter = TextPainter();
1110 1111
  late HorizontalDragGestureRecognizer _drag;
  late TapGestureRecognizer _tap;
1112 1113
  bool _active = false;
  double _currentDragValue = 0.0;
1114
  Rect? overlayRect;
1115

1116 1117 1118
  // This rect is used in gesture calculations, where the gesture coordinates
  // are relative to the sliders origin. Therefore, the offset is passed as
  // (0,0).
1119
  Rect get _trackRect => _sliderTheme.trackShape!.getPreferredRect(
1120 1121 1122 1123
    parentBox: this,
    sliderTheme: _sliderTheme,
    isDiscrete: false,
  );
1124 1125 1126

  bool get isInteractive => onChanged != null;

1127
  bool get isDiscrete => divisions != null && divisions! > 0;
1128

1129 1130
  double get value => _value;
  double _value;
1131
  set value(double newValue) {
1132
    assert(newValue >= 0.0 && newValue <= 1.0);
1133 1134
    final double convertedValue = isDiscrete ? _discretize(newValue) : newValue;
    if (convertedValue == _value) {
1135
      return;
1136 1137 1138
    }
    _value = convertedValue;
    if (isDiscrete) {
1139 1140 1141 1142 1143 1144 1145
      // 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)
1146
        : Duration.zero;
1147 1148 1149 1150
      _state.positionController.animateTo(convertedValue, curve: Curves.easeInOut);
    } else {
      _state.positionController.value = convertedValue;
    }
1151 1152 1153
    markNeedsSemanticsUpdate();
  }

1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164
  double? get secondaryTrackValue => _secondaryTrackValue;
  double? _secondaryTrackValue;
  set secondaryTrackValue(double? newValue) {
    assert(newValue == null || (newValue >= 0.0 && newValue <= 1.0));
    if (newValue == _secondaryTrackValue) {
      return;
    }
    _secondaryTrackValue = newValue;
    markNeedsSemanticsUpdate();
  }

1165 1166 1167 1168 1169 1170
  DeviceGestureSettings? get gestureSettings => _drag.gestureSettings;
  set gestureSettings(DeviceGestureSettings? gestureSettings) {
    _drag.gestureSettings = gestureSettings;
    _tap.gestureSettings = gestureSettings;
  }

1171 1172 1173
  TargetPlatform _platform;
  TargetPlatform get platform => _platform;
  set platform(TargetPlatform value) {
1174
    if (_platform == value) {
1175
      return;
1176
    }
1177 1178 1179 1180
    _platform = value;
    markNeedsSemanticsUpdate();
  }

1181 1182 1183
  SemanticFormatterCallback? _semanticFormatterCallback;
  SemanticFormatterCallback? get semanticFormatterCallback => _semanticFormatterCallback;
  set semanticFormatterCallback(SemanticFormatterCallback? value) {
1184
    if (_semanticFormatterCallback == value) {
1185
      return;
1186
    }
1187 1188
    _semanticFormatterCallback = value;
    markNeedsSemanticsUpdate();
1189 1190
  }

1191 1192 1193
  int? get divisions => _divisions;
  int? _divisions;
  set divisions(int? value) {
1194
    if (value == _divisions) {
1195
      return;
1196
    }
1197
    _divisions = value;
1198 1199 1200
    markNeedsPaint();
  }

1201 1202 1203
  String? get label => _label;
  String? _label;
  set label(String? value) {
1204
    if (value == _label) {
1205
      return;
1206
    }
1207
    _label = value;
Ian Hickson's avatar
Ian Hickson committed
1208
    _updateLabelPainter();
1209 1210
  }

1211 1212 1213 1214
  SliderThemeData get sliderTheme => _sliderTheme;
  SliderThemeData _sliderTheme;
  set sliderTheme(SliderThemeData value) {
    if (value == _sliderTheme) {
1215
      return;
1216 1217
    }
    _sliderTheme = value;
1218
    _updateLabelPainter();
1219 1220
  }

1221 1222 1223 1224
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
  set textScaleFactor(double value) {
    if (value == _textScaleFactor) {
1225
      return;
1226
    }
1227
    _textScaleFactor = value;
1228 1229 1230
    _updateLabelPainter();
  }

1231 1232 1233 1234 1235 1236 1237 1238 1239 1240
  Size get screenSize => _screenSize;
  Size _screenSize;
  set screenSize(Size value) {
    if (value == _screenSize) {
      return;
    }
    _screenSize = value;
    markNeedsPaint();
  }

1241 1242 1243
  ValueChanged<double>? get onChanged => _onChanged;
  ValueChanged<double>? _onChanged;
  set onChanged(ValueChanged<double>? value) {
1244
    if (value == _onChanged) {
1245
      return;
1246
    }
1247 1248 1249
    final bool wasInteractive = isInteractive;
    _onChanged = value;
    if (wasInteractive != isInteractive) {
1250 1251 1252 1253 1254
      if (isInteractive) {
        _state.enableController.forward();
      } else {
        _state.enableController.reverse();
      }
1255
      markNeedsPaint();
1256
      markNeedsSemanticsUpdate();
1257 1258
    }
  }
1259

1260 1261
  ValueChanged<double>? onChangeStart;
  ValueChanged<double>? onChangeEnd;
1262

1263 1264 1265
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
1266
    if (value == _textDirection) {
1267
      return;
1268
    }
1269
    _textDirection = value;
Ian Hickson's avatar
Ian Hickson committed
1270 1271 1272
    _updateLabelPainter();
  }

1273 1274 1275 1276
  /// True if this slider has the input focus.
  bool get hasFocus => _hasFocus;
  bool _hasFocus;
  set hasFocus(bool value) {
1277
    if (value == _hasFocus) {
1278
      return;
1279
    }
1280
    _hasFocus = value;
1281
    _updateForFocus(_hasFocus);
1282
    markNeedsSemanticsUpdate();
1283 1284 1285 1286 1287 1288
  }

  /// True if this slider is being hovered over by a pointer.
  bool get hovering => _hovering;
  bool _hovering;
  set hovering(bool value) {
1289
    if (value == _hovering) {
1290
      return;
1291
    }
1292
    _hovering = value;
1293
    _updateForHover(_hovering);
1294 1295
  }

1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
  /// True if the slider is interactive and the slider thumb is being
  /// hovered over by a pointer.
  bool _hoveringThumb = false;
  bool get hoveringThumb => _hoveringThumb;
  set hoveringThumb(bool value) {
    if (value == _hoveringThumb) {
      return;
    }
    _hoveringThumb = value;
    _updateForHover(_hovering);
  }

  void _updateForFocus(bool focused) {
    if (focused) {
1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320
      _state.overlayController.forward();
      if (showValueIndicator) {
        _state.valueIndicatorController.forward();
      }
    } else {
      _state.overlayController.reverse();
      if (showValueIndicator) {
        _state.valueIndicatorController.reverse();
      }
    }
  }
1321

1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
  void _updateForHover(bool hovered) {
    // Only show overlay when pointer is hovering the thumb.
    if (hovered && hoveringThumb) {
      _state.overlayController.forward();
    } else {
      // Only remove overlay when Slider is unfocused.
      if (!hasFocus) {
        _state.overlayController.reverse();
      }
    }
  }

1334
  bool get showValueIndicator {
1335
    switch (_sliderTheme.showValueIndicator!) {
1336
      case ShowValueIndicator.onlyForDiscrete:
1337
        return isDiscrete;
1338
      case ShowValueIndicator.onlyForContinuous:
1339
        return !isDiscrete;
1340
      case ShowValueIndicator.always:
1341
        return true;
1342
      case ShowValueIndicator.never:
1343
        return false;
1344 1345 1346
    }
  }

1347 1348 1349
  double get _adjustmentUnit {
    switch (_platform) {
      case TargetPlatform.iOS:
1350
      case TargetPlatform.macOS:
1351
        // Matches iOS implementation of material slider.
1352 1353 1354
        return 0.1;
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
1355 1356 1357
      case TargetPlatform.linux:
      case TargetPlatform.windows:
        // Matches Android implementation of material slider.
1358 1359 1360 1361
        return 0.05;
    }
  }

Ian Hickson's avatar
Ian Hickson committed
1362 1363 1364
  void _updateLabelPainter() {
    if (label != null) {
      _labelPainter
1365
        ..text = TextSpan(
1366 1367 1368
          style: _sliderTheme.valueIndicatorTextStyle,
          text: label,
        )
Ian Hickson's avatar
Ian Hickson committed
1369
        ..textDirection = textDirection
1370
        ..textScaleFactor = textScaleFactor
Ian Hickson's avatar
Ian Hickson committed
1371 1372 1373 1374 1375 1376 1377 1378
        ..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();
1379 1380
  }

1381 1382 1383 1384 1385 1386 1387
  @override
  void systemFontsDidChange() {
    super.systemFontsDidChange();
    _labelPainter.markNeedsLayout();
    _updateLabelPainter();
  }

1388 1389 1390
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
1391 1392
    _overlayAnimation.addListener(markNeedsPaint);
    _valueIndicatorAnimation.addListener(markNeedsPaint);
1393 1394 1395 1396 1397 1398
    _enableAnimation.addListener(markNeedsPaint);
    _state.positionController.addListener(markNeedsPaint);
  }

  @override
  void detach() {
1399 1400
    _overlayAnimation.removeListener(markNeedsPaint);
    _valueIndicatorAnimation.removeListener(markNeedsPaint);
1401 1402 1403 1404 1405
    _enableAnimation.removeListener(markNeedsPaint);
    _state.positionController.removeListener(markNeedsPaint);
    super.detach();
  }

1406 1407 1408 1409 1410 1411
  @override
  void dispose() {
    _labelPainter.dispose();
    super.dispose();
  }

1412 1413 1414 1415 1416 1417 1418 1419 1420
  double _getValueFromVisualPosition(double visualPosition) {
    switch (textDirection) {
      case TextDirection.rtl:
        return 1.0 - visualPosition;
      case TextDirection.ltr:
        return visualPosition;
    }
  }

1421
  double _getValueFromGlobalPosition(Offset globalPosition) {
1422
    final double visualPosition = (globalToLocal(globalPosition).dx - _trackRect.left) / _trackRect.width;
1423
    return _getValueFromVisualPosition(visualPosition);
1424 1425
  }

1426
  double _discretize(double value) {
1427
    double result = clampDouble(value, 0.0, 1.0);
1428
    if (isDiscrete) {
1429
      result = (result * divisions!).round() / divisions!;
1430
    }
1431 1432
    return result;
  }
1433

1434
  void _startInteraction(Offset globalPosition) {
1435
    _state.showValueIndicator();
1436
    if (!_active && isInteractive) {
1437
      _active = true;
1438 1439 1440
      // We supply the *current* value as the start location, so that if we have
      // a tap, it consists of a call to onChangeStart with the previous value and
      // a call to onChangeEnd with the new value.
1441
      onChangeStart?.call(_discretize(value));
1442
      _currentDragValue = _getValueFromGlobalPosition(globalPosition);
1443
      onChanged!(_discretize(_currentDragValue));
1444 1445 1446
      _state.overlayController.forward();
      if (showValueIndicator) {
        _state.valueIndicatorController.forward();
1447
        _state.interactionTimer?.cancel();
1448
        _state.interactionTimer = Timer(_minimumInteractionTime * timeDilation, () {
1449
          _state.interactionTimer = null;
1450
          if (!_active && !hasFocus &&
1451
              _state.valueIndicatorController.status == AnimationStatus.completed) {
1452 1453 1454 1455
            _state.valueIndicatorController.reverse();
          }
        });
      }
1456 1457 1458 1459
    }
  }

  void _endInteraction() {
1460 1461 1462 1463
    if (!_state.mounted) {
      return;
    }

1464
    if (_active && _state.mounted) {
1465
      onChangeEnd?.call(_discretize(_currentDragValue));
1466 1467
      _active = false;
      _currentDragValue = 0.0;
1468 1469 1470
      if (!hasFocus) {
        _state.overlayController.reverse();
      }
1471

1472
      if (showValueIndicator && _state.interactionTimer == null) {
1473 1474
        _state.valueIndicatorController.reverse();
      }
1475 1476 1477
    }
  }

1478 1479 1480
  void _handleDragStart(DragStartDetails details) {
    _startInteraction(details.globalPosition);
  }
1481

1482
  void _handleDragUpdate(DragUpdateDetails details) {
1483 1484 1485 1486
    if (!_state.mounted) {
      return;
    }

1487
    if (isInteractive) {
1488
      final double valueDelta = details.primaryDelta! / _trackRect.width;
1489 1490 1491 1492 1493 1494 1495 1496
      switch (textDirection) {
        case TextDirection.rtl:
          _currentDragValue -= valueDelta;
          break;
        case TextDirection.ltr:
          _currentDragValue += valueDelta;
          break;
      }
1497
      onChanged!(_discretize(_currentDragValue));
1498 1499 1500
    }
  }

1501 1502 1503
  void _handleDragEnd(DragEndDetails details) {
    _endInteraction();
  }
1504

1505 1506 1507
  void _handleTapDown(TapDownDetails details) {
    _startInteraction(details.globalPosition);
  }
1508

1509 1510 1511
  void _handleTapUp(TapUpDetails details) {
    _endInteraction();
  }
1512

1513
  @override
1514
  bool hitTestSelf(Offset position) => true;
1515

1516
  @override
Ian Hickson's avatar
Ian Hickson committed
1517
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
1518 1519 1520
    if (!_state.mounted) {
      return;
    }
1521
    assert(debugHandleEvent(event, entry));
1522 1523
    if (event is PointerDownEvent && isInteractive) {
      // We need to add the drag first so that it has priority.
1524
      _drag.addPointer(event);
1525 1526
      _tap.addPointer(event);
    }
1527 1528 1529
    if (isInteractive && overlayRect != null) {
      hoveringThumb = overlayRect!.contains(event.localPosition);
    }
1530 1531
  }

1532
  @override
1533
  double computeMinIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth;
1534 1535

  @override
1536
  double computeMaxIntrinsicWidth(double height) => _minPreferredTrackWidth + _maxSliderPartWidth;
1537 1538

  @override
1539
  double computeMinIntrinsicHeight(double width) => math.max(_minPreferredTrackHeight, _maxSliderPartHeight);
1540 1541

  @override
1542
  double computeMaxIntrinsicHeight(double width) => math.max(_minPreferredTrackHeight, _maxSliderPartHeight);
1543 1544 1545 1546 1547

  @override
  bool get sizedByParent => true;

  @override
1548 1549
  Size computeDryLayout(BoxConstraints constraints) {
    return Size(
1550
      constraints.hasBoundedWidth ? constraints.maxWidth : _minPreferredTrackWidth + _maxSliderPartWidth,
1551
      constraints.hasBoundedHeight ? constraints.maxHeight : math.max(_minPreferredTrackHeight, _maxSliderPartHeight),
1552 1553 1554
    );
  }

1555
  @override
1556
  void paint(PaintingContext context, Offset offset) {
1557
    final double value = _state.positionController.value;
1558
    final double? secondaryValue = _secondaryTrackValue;
1559

1560 1561 1562
    // The visual position is the position of the thumb from 0 to 1 from left
    // to right. In left to right, this is the same as the value, but it is
    // reversed for right to left text.
1563
    final double visualPosition;
1564
    final double? secondaryVisualPosition;
1565 1566 1567
    switch (textDirection) {
      case TextDirection.rtl:
        visualPosition = 1.0 - value;
1568
        secondaryVisualPosition = (secondaryValue != null) ? (1.0 - secondaryValue) : null;
1569 1570 1571
        break;
      case TextDirection.ltr:
        visualPosition = value;
1572
        secondaryVisualPosition = (secondaryValue != null) ? secondaryValue : null;
1573 1574
        break;
    }
1575

1576
    final Rect trackRect = _sliderTheme.trackShape!.getPreferredRect(
1577 1578 1579
      parentBox: this,
      offset: offset,
      sliderTheme: _sliderTheme,
1580
      isDiscrete: isDiscrete,
1581
    );
1582
    final Offset thumbCenter = Offset(trackRect.left + visualPosition * trackRect.width, trackRect.center.dy);
1583 1584
    if (isInteractive) {
      final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize(isInteractive, false);
1585
      overlayRect = Rect.fromCircle(center: thumbCenter, radius: overlaySize.width / 2.0);
1586
    }
1587
    final Offset? secondaryOffset = (secondaryVisualPosition != null) ? Offset(trackRect.left + secondaryVisualPosition * trackRect.width, trackRect.center.dy) : null;
1588

1589
    _sliderTheme.trackShape!.paint(
1590 1591 1592 1593 1594 1595
      context,
      offset,
      parentBox: this,
      sliderTheme: _sliderTheme,
      enableAnimation: _enableAnimation,
      textDirection: _textDirection,
1596
      thumbCenter: thumbCenter,
1597
      secondaryOffset: secondaryOffset,
1598
      isDiscrete: isDiscrete,
1599
      isEnabled: isInteractive,
1600 1601
    );

1602
    if (!_overlayAnimation.isDismissed) {
1603
      _sliderTheme.overlayShape!.paint(
1604
        context,
1605
        thumbCenter,
1606 1607 1608 1609 1610 1611 1612 1613
        activationAnimation: _overlayAnimation,
        enableAnimation: _enableAnimation,
        isDiscrete: isDiscrete,
        labelPainter: _labelPainter,
        parentBox: this,
        sliderTheme: _sliderTheme,
        textDirection: _textDirection,
        value: _value,
1614 1615
        textScaleFactor: _textScaleFactor,
        sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
1616 1617 1618 1619
      );
    }

    if (isDiscrete) {
1620
      final double tickMarkWidth = _sliderTheme.tickMarkShape!.getPreferredSize(
1621 1622 1623
        isEnabled: isInteractive,
        sliderTheme: _sliderTheme,
      ).width;
1624
      final double padding = trackRect.height;
1625
      final double adjustedTrackWidth = trackRect.width - padding;
1626
      // If the tick marks would be too dense, don't bother painting them.
1627
      if (adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth) {
1628
        final double dy = trackRect.center.dy;
1629 1630
        for (int i = 0; i <= divisions!; i++) {
          final double value = i / divisions!;
1631 1632
          // The ticks are mapped to be within the track, so the tick mark width
          // must be subtracted from the track width.
1633
          final double dx = trackRect.left + value * adjustedTrackWidth + padding / 2;
1634
          final Offset tickMarkOffset = Offset(dx, dy);
1635
          _sliderTheme.tickMarkShape!.paint(
1636 1637 1638 1639 1640 1641
            context,
            tickMarkOffset,
            parentBox: this,
            sliderTheme: _sliderTheme,
            enableAnimation: _enableAnimation,
            textDirection: _textDirection,
1642
            thumbCenter: thumbCenter,
1643 1644 1645
            isEnabled: isInteractive,
          );
        }
1646 1647 1648 1649
      }
    }

    if (isInteractive && label != null && !_valueIndicatorAnimation.isDismissed) {
1650
      if (showValueIndicator) {
1651
        _state.paintValueIndicator = (PaintingContext context, Offset offset) {
1652
          if (attached) {
1653
            _sliderTheme.valueIndicatorShape!.paint(
1654
              context,
1655
              offset + thumbCenter,
1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
              activationAnimation: _valueIndicatorAnimation,
              enableAnimation: _enableAnimation,
              isDiscrete: isDiscrete,
              labelPainter: _labelPainter,
              parentBox: this,
              sliderTheme: _sliderTheme,
              textDirection: _textDirection,
              value: _value,
              textScaleFactor: textScaleFactor,
              sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
            );
          }
1668
        };
1669
      }
1670
    }
Adam Barth's avatar
Adam Barth committed
1671

1672
    _sliderTheme.thumbShape!.paint(
1673
      context,
1674
      thumbCenter,
1675
      activationAnimation: _overlayAnimation,
1676 1677 1678 1679 1680 1681 1682
      enableAnimation: _enableAnimation,
      isDiscrete: isDiscrete,
      labelPainter: _labelPainter,
      parentBox: this,
      sliderTheme: _sliderTheme,
      textDirection: _textDirection,
      value: _value,
1683 1684
      textScaleFactor: textScaleFactor,
      sizeWithOverflow: screenSize.isEmpty ? size : screenSize,
1685
    );
1686
  }
1687 1688

  @override
1689 1690
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
1691

1692
    // The Slider widget has its own Focus widget with semantics information,
1693
    // and we want that semantics node to collect the semantics information here
1694 1695 1696 1697 1698 1699 1700
    // so that it's all in the same node: otherwise Talkback sees that the node
    // has focusable children, and it won't focus the Slider's Focus widget
    // because it thinks the Focus widget's node doesn't have anything to say
    // (which it doesn't, but this child does). Aggregating the semantic
    // information into one node means that Talkback will recognize that it has
    // something to say and focus it when it receives keyboard focus.
    // (See https://github.com/flutter/flutter/issues/57038 for context).
1701
    config.isSemanticBoundary = false;
1702 1703 1704

    config.isEnabled = isInteractive;
    config.textDirection = textDirection;
1705
    if (isInteractive) {
1706 1707
      config.onIncrease = increaseAction;
      config.onDecrease = decreaseAction;
1708
    }
1709

1710
    if (semanticFormatterCallback != null) {
1711
      config.value = semanticFormatterCallback!(_state._lerp(value));
1712 1713
      config.increasedValue = semanticFormatterCallback!(_state._lerp(clampDouble(value + _semanticActionUnit, 0.0, 1.0)));
      config.decreasedValue = semanticFormatterCallback!(_state._lerp(clampDouble(value - _semanticActionUnit, 0.0, 1.0)));
1714 1715
    } else {
      config.value = '${(value * 100).round()}%';
1716 1717
      config.increasedValue = '${(clampDouble(value + _semanticActionUnit, 0.0, 1.0) * 100).round()}%';
      config.decreasedValue = '${(clampDouble(value - _semanticActionUnit, 0.0, 1.0) * 100).round()}%';
1718 1719 1720
    }
  }

1721
  double get _semanticActionUnit => divisions != null ? 1.0 / divisions! : _adjustmentUnit;
1722

1723
  void increaseAction() {
1724
    if (isInteractive) {
1725
      onChanged!(clampDouble(value + _semanticActionUnit, 0.0, 1.0));
1726
    }
1727 1728
  }

1729
  void decreaseAction() {
1730
    if (isInteractive) {
1731
      onChanged!(clampDouble(value - _semanticActionUnit, 0.0, 1.0));
1732
    }
1733
  }
1734
}
1735

1736 1737
class _AdjustSliderIntent extends Intent {
  const _AdjustSliderIntent({
1738
    required this.type,
1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758
  });

  const _AdjustSliderIntent.right() : type = _SliderAdjustmentType.right;

  const _AdjustSliderIntent.left() : type = _SliderAdjustmentType.left;

  const _AdjustSliderIntent.up() : type = _SliderAdjustmentType.up;

  const _AdjustSliderIntent.down() : type = _SliderAdjustmentType.down;

  final _SliderAdjustmentType type;
}

enum _SliderAdjustmentType {
  right,
  left,
  up,
  down,
}

1759 1760
class _ValueIndicatorRenderObjectWidget extends LeafRenderObjectWidget {
  const _ValueIndicatorRenderObjectWidget({
1761
    required this.state,
1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779
  });

  final _SliderState state;

  @override
  _RenderValueIndicator createRenderObject(BuildContext context) {
    return _RenderValueIndicator(
      state: state,
    );
  }
  @override
  void updateRenderObject(BuildContext context, _RenderValueIndicator renderObject) {
    renderObject._state = state;
  }
}

class _RenderValueIndicator extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
  _RenderValueIndicator({
1780
    required _SliderState state,
1781 1782 1783 1784 1785 1786
  }) : _state = state {
    _valueIndicatorAnimation = CurvedAnimation(
      parent: _state.valueIndicatorController,
      curve: Curves.fastOutSlowIn,
    );
  }
1787
  late Animation<double> _valueIndicatorAnimation;
1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808
  _SliderState _state;

  @override
  bool get sizedByParent => true;

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _valueIndicatorAnimation.addListener(markNeedsPaint);
    _state.positionController.addListener(markNeedsPaint);
  }

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

  @override
  void paint(PaintingContext context, Offset offset) {
1809
    _state.paintValueIndicator?.call(context, offset);
1810
  }
1811 1812 1813 1814 1815

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return constraints.smallest;
  }
1816
}
1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863

class _SliderDefaultsM2 extends SliderThemeData {
  _SliderDefaultsM2(this.context)
    : _colors = Theme.of(context).colorScheme,
      super(trackHeight: 4.0);

  final BuildContext context;
  final ColorScheme _colors;

  @override
  Color? get activeTrackColor => _colors.primary;

  @override
  Color? get inactiveTrackColor => _colors.primary.withOpacity(0.24);

  @override
  Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);

  @override
  Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.32);

  @override
  Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);

  @override
  Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);

  @override
  Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.54);

  @override
  Color? get inactiveTickMarkColor => _colors.primary.withOpacity(0.54);

  @override
  Color? get disabledActiveTickMarkColor => _colors.onPrimary.withOpacity(0.12);

  @override
  Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.12);

  @override
  Color? get thumbColor => _colors.primary;

  @override
  Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(.38), _colors.surface);

  @override
  Color? get overlayColor => _colors.primary.withOpacity(0.12);
1864 1865 1866 1867 1868 1869 1870 1871

  @override
  TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.bodyLarge!.copyWith(
    color: _colors.onPrimary,
  );

  @override
  SliderComponentShape? get valueIndicatorShape => const RectangularSliderValueIndicatorShape();
1872 1873 1874 1875 1876 1877 1878 1879 1880
}

// BEGIN GENERATED TOKEN PROPERTIES - Slider

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

1881
// Token database version: v0_158
1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940

class _SliderDefaultsM3 extends SliderThemeData {
  _SliderDefaultsM3(this.context)
    : _colors = Theme.of(context).colorScheme,
      super(trackHeight: 4.0);

  final BuildContext context;
  final ColorScheme _colors;

  @override
  Color? get activeTrackColor => _colors.primary;

  @override
  Color? get inactiveTrackColor => _colors.surfaceVariant;

  @override
  Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);

  @override
  Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);

  @override
  Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);

  @override
  Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);

  @override
  Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38);

  @override
  Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38);

  @override
  Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38);

  @override
  Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38);

  @override
  Color? get thumbColor => _colors.primary;

  @override
  Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface);

  @override
  Color? get overlayColor => MaterialStateColor.resolveWith((Set<MaterialState> states) {
    if (states.contains(MaterialState.hovered)) {
      return _colors.primary.withOpacity(0.08);
    }
    if (states.contains(MaterialState.focused)) {
      return _colors.primary.withOpacity(0.12);
    }
    if (states.contains(MaterialState.dragged)) {
      return _colors.primary.withOpacity(0.12);
    }

    return Colors.transparent;
  });
1941 1942 1943 1944 1945 1946 1947 1948

  @override
  TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith(
    color: _colors.onPrimary,
  );

  @override
  SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
1949 1950 1951
}

// END GENERATED TOKEN PROPERTIES - Slider