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

7 8
import 'package:flutter/material.dart';

9 10
import '../../gallery/demo.dart';

11
class SliderDemo extends StatefulWidget {
12
  const SliderDemo({super.key});
13

14
  static const String routeName = '/material/slider';
15

16
  @override
17
  State<SliderDemo> createState() => _SliderDemoState();
18 19
}

20
Path _downTriangle(double size, Offset thumbCenter, { bool invert = false }) {
21
  final Path thumbPath = Path();
22 23
  final double height = math.sqrt(3.0) / 2.0;
  final double centerHeight = size * height / 3.0;
24
  final double halfSize = size / 2.0;
25
  final double sign = invert ? -1.0 : 1.0;
26
  thumbPath.moveTo(thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
27
  thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2.0 * sign * centerHeight);
28
  thumbPath.lineTo(thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight);
29 30 31 32
  thumbPath.close();
  return thumbPath;
}

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
Path _rightTriangle(double size, Offset thumbCenter, { bool invert = false }) {
  final Path thumbPath = Path();
  final double halfSize = size / 2.0;
  final double sign = invert ? -1.0 : 1.0;
  thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy);
  thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size);
  thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size);
  thumbPath.close();
  return thumbPath;
}

Path _upTriangle(double size, Offset thumbCenter) => _downTriangle(size, thumbCenter, invert: true);

Path _leftTriangle(double size, Offset thumbCenter) => _rightTriangle(size, thumbCenter, invert: true);

class _CustomRangeThumbShape extends RangeSliderThumbShape {
  static const double _thumbSize = 4.0;
  static const double _disabledThumbSize = 3.0;

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
    return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
  }

  static final Animatable<double> sizeTween = Tween<double>(
    begin: _disabledThumbSize,
    end: _thumbSize,
  );

  @override
  void paint(
    PaintingContext context,
    Offset center, {
66 67
    required Animation<double> activationAnimation,
    required Animation<double> enableAnimation,
68 69
    bool isDiscrete = false,
    bool isEnabled = false,
70 71 72 73 74
    bool? isOnTop,
    required SliderThemeData sliderTheme,
    TextDirection? textDirection,
    Thumb? thumb,
    bool? isPressed,
75 76 77 78 79 80 81 82
  }) {
    final Canvas canvas = context.canvas;
    final ColorTween colorTween = ColorTween(
      begin: sliderTheme.disabledThumbColor,
      end: sliderTheme.thumbColor,
    );

    final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
83
    late Path thumbPath;
84 85 86 87 88 89 90
    switch (textDirection) {
      case TextDirection.rtl:
        switch (thumb) {
          case Thumb.start:
            thumbPath = _rightTriangle(size, center);
          case Thumb.end:
            thumbPath = _leftTriangle(size, center);
91
          case null:
92
            break;
93 94 95 96 97 98 99
        }
      case TextDirection.ltr:
        switch (thumb) {
          case Thumb.start:
            thumbPath = _leftTriangle(size, center);
          case Thumb.end:
            thumbPath = _rightTriangle(size, center);
100
          case null:
101
            break;
102
        }
103
      case null:
104
        break;
105
    }
106
    canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation)!);
107 108 109
  }
}

110 111 112 113 114 115 116 117 118
class _CustomThumbShape extends SliderComponentShape {
  static const double _thumbSize = 4.0;
  static const double _disabledThumbSize = 3.0;

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
    return isEnabled ? const Size.fromRadius(_thumbSize) : const Size.fromRadius(_disabledThumbSize);
  }

119
  static final Animatable<double> sizeTween = Tween<double>(
120 121 122 123 124 125 126 127
    begin: _disabledThumbSize,
    end: _thumbSize,
  );

  @override
  void paint(
    PaintingContext context,
    Offset thumbCenter, {
128 129 130 131 132 133 134 135 136 137
    Animation<double>? activationAnimation,
    required Animation<double> enableAnimation,
    bool? isDiscrete,
    TextPainter? labelPainter,
    RenderBox? parentBox,
    required SliderThemeData sliderTheme,
    TextDirection? textDirection,
    double? value,
    double? textScaleFactor,
    Size? sizeWithOverflow,
138 139
  }) {
    final Canvas canvas = context.canvas;
140
    final ColorTween colorTween = ColorTween(
141 142 143 144
      begin: sliderTheme.disabledThumbColor,
      end: sliderTheme.thumbColor,
    );
    final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
145
    final Path thumbPath = _downTriangle(size, thumbCenter);
146
    canvas.drawPath(thumbPath, Paint()..color = colorTween.evaluate(enableAnimation)!);
147 148 149 150 151 152 153 154 155 156
  }
}

class _CustomValueIndicatorShape extends SliderComponentShape {
  static const double _indicatorSize = 4.0;
  static const double _disabledIndicatorSize = 3.0;
  static const double _slideUpHeight = 40.0;

  @override
  Size getPreferredSize(bool isEnabled, bool isDiscrete) {
157
    return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
158 159
  }

160
  static final Animatable<double> sizeTween = Tween<double>(
161 162 163 164 165 166 167 168
    begin: _disabledIndicatorSize,
    end: _indicatorSize,
  );

  @override
  void paint(
    PaintingContext context,
    Offset thumbCenter, {
169 170 171 172 173 174 175 176 177 178
    required Animation<double> activationAnimation,
    required Animation<double> enableAnimation,
    bool? isDiscrete,
    required TextPainter labelPainter,
    RenderBox? parentBox,
    required SliderThemeData sliderTheme,
    TextDirection? textDirection,
    double? value,
    double? textScaleFactor,
    Size? sizeWithOverflow,
179 180
  }) {
    final Canvas canvas = context.canvas;
181
    final ColorTween enableColor = ColorTween(
182 183 184
      begin: sliderTheme.disabledThumbColor,
      end: sliderTheme.valueIndicatorColor,
    );
185
    final Tween<double> slideUpTween = Tween<double>(
186 187 188 189
      begin: 0.0,
      end: _slideUpHeight,
    );
    final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
190
    final Offset slideUpOffset = Offset(0.0, -slideUpTween.evaluate(activationAnimation));
191
    final Path thumbPath = _upTriangle(size, thumbCenter + slideUpOffset);
192
    final Color paintColor = enableColor.evaluate(enableAnimation)!.withAlpha((255.0 * activationAnimation.value).round());
193 194
    canvas.drawPath(
      thumbPath,
195
      Paint()..color = paintColor,
196 197 198 199
    );
    canvas.drawLine(
        thumbCenter,
        thumbCenter + slideUpOffset,
200
        Paint()
201 202 203
          ..color = paintColor
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2.0);
204
    labelPainter.paint(canvas, thumbCenter + slideUpOffset + Offset(-labelPainter.width / 2.0, -labelPainter.height - 4.0));
205 206 207
  }
}

208
class _SliderDemoState extends State<SliderDemo> {
209 210
  @override
  Widget build(BuildContext context) {
211
    const List<ComponentDemoTabData> demos = <ComponentDemoTabData>[
212 213 214 215
      ComponentDemoTabData(
        tabName: 'SINGLE',
        description: 'Sliders containing 1 thumb',
        demoWidget: _Sliders(),
216
        documentationUrl: 'https://api.flutter.dev/flutter/material/Slider-class.html',
217 218 219 220 221
      ),
      ComponentDemoTabData(
        tabName: 'RANGE',
        description: 'Sliders containing 2 thumbs',
        demoWidget: _RangeSliders(),
222
        documentationUrl: 'https://api.flutter.dev/flutter/material/RangeSlider-class.html',
223 224 225
      ),
    ];

226
    return const TabbedComponentDemoScaffold(
227 228 229 230 231 232 233 234 235
      title: 'Sliders',
      demos: demos,
      isScrollable: false,
      showExampleCodeAction: false,
    );
  }
}

class _Sliders extends StatefulWidget {
236 237
  const _Sliders();

238 239 240 241 242 243
  @override
  _SlidersState createState() => _SlidersState();
}

class _SlidersState extends State<_Sliders> {
  double _continuousValue = 25.0;
244
  double _discreteValue = 20.0;
245
  double _discreteCustomValue = 25.0;
246

247
  @override
248
  Widget build(BuildContext context) {
249
    final ThemeData theme = Theme.of(context);
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 40.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Semantics(
                label: 'Editable numerical value',
                child: SizedBox(
                  width: 64,
                  height: 48,
                  child: TextField(
                    textAlign: TextAlign.center,
                    onSubmitted: (String value) {
266
                      final double? newValue = double.tryParse(value);
267 268
                      if (newValue != null && newValue != _continuousValue) {
                        setState(() {
269
                          _continuousValue = newValue.clamp(0.0, 100.0);
270 271 272 273 274 275 276 277 278 279
                        });
                      }
                    },
                    keyboardType: TextInputType.number,
                    controller: TextEditingController(
                      text: _continuousValue.toStringAsFixed(0),
                    ),
                  ),
                ),
              ),
280
              Slider.adaptive(
281
                label: _continuousValue.toStringAsFixed(6),
282 283 284 285 286 287 288
                value: _continuousValue,
                max: 100.0,
                onChanged: (double value) {
                  setState(() {
                    _continuousValue = value;
                  });
                },
289 290 291 292
              ),
              const Text('Continuous with Editable Numerical Value'),
            ],
          ),
293
          const Column(
294
            mainAxisSize: MainAxisSize.min,
295
            children: <Widget>[
296
              Slider.adaptive(value: 0.25, onChanged: null),
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
              Text('Disabled'),
            ],
          ),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Slider.adaptive(
                value: _discreteValue,
                max: 200.0,
                divisions: 5,
                label: '${_discreteValue.round()}',
                onChanged: (double value) {
                  setState(() {
                    _discreteValue = value;
                  });
                },
              ),
              const Text('Discrete'),
            ],
          ),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SliderTheme(
                data: theme.sliderTheme.copyWith(
                  activeTrackColor: Colors.deepPurple,
                  inactiveTrackColor: theme.colorScheme.onSurface.withOpacity(0.5),
                  activeTickMarkColor: theme.colorScheme.onSurface.withOpacity(0.7),
325
                  inactiveTickMarkColor: theme.colorScheme.surface.withOpacity(0.7),
326 327 328 329 330
                  overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
                  thumbColor: Colors.deepPurple,
                  valueIndicatorColor: Colors.deepPurpleAccent,
                  thumbShape: _CustomThumbShape(),
                  valueIndicatorShape: _CustomValueIndicatorShape(),
331
                  valueIndicatorTextStyle: theme.textTheme.bodyLarge!.copyWith(color: theme.colorScheme.onSurface),
332 333 334 335 336 337 338
                ),
                child: Slider(
                  value: _discreteCustomValue,
                  max: 200.0,
                  divisions: 5,
                  semanticFormatterCallback: (double value) => value.round().toString(),
                  label: '${_discreteCustomValue.round()}',
339 340
                  onChanged: (double value) {
                    setState(() {
341
                      _discreteCustomValue = value;
342
                    });
343
                  },
344
                ),
345 346 347 348 349 350 351 352 353 354 355
              ),
              const Text('Discrete with Custom Theme'),
            ],
          ),
        ],
      ),
    );
  }
}

class _RangeSliders extends StatefulWidget {
356 357
  const _RangeSliders();

358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
  @override
  _RangeSlidersState createState() => _RangeSlidersState();
}

class _RangeSlidersState extends State<_RangeSliders> {
  RangeValues _continuousValues = const RangeValues(25.0, 75.0);
  RangeValues _discreteValues = const RangeValues(40.0, 120.0);
  RangeValues _discreteCustomValues = const RangeValues(40.0, 160.0);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 40.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              RangeSlider(
                values: _continuousValues,
                max: 100.0,
                onChanged: (RangeValues values) {
                  setState(() {
                    _continuousValues = values;
                  });
                },
              ),
              const Text('Continuous'),
            ],
          ),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
392
              RangeSlider(values: const RangeValues(0.25, 0.75), onChanged: null),
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
              const Text('Disabled'),
            ],
          ),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              RangeSlider(
                values: _discreteValues,
                max: 200.0,
                divisions: 5,
                labels: RangeLabels('${_discreteValues.start.round()}', '${_discreteValues.end.round()}'),
                onChanged: (RangeValues values) {
                  setState(() {
                    _discreteValues = values;
                  });
                },
              ),
              const Text('Discrete'),
            ],
          ),
          Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SliderTheme(
                data: SliderThemeData(
                  activeTrackColor: Colors.deepPurple,
                  inactiveTrackColor: Colors.black26,
                  activeTickMarkColor: Colors.white70,
                  inactiveTickMarkColor: Colors.black,
                  overlayColor: Colors.black12,
                  thumbColor: Colors.deepPurple,
                  rangeThumbShape: _CustomRangeThumbShape(),
                  showValueIndicator: ShowValueIndicator.never,
426
                ),
427 428
                child: RangeSlider(
                  values: _discreteCustomValues,
429
                  max: 200.0,
430
                  divisions: 5,
431 432
                  labels: RangeLabels('${_discreteCustomValues.start.round()}', '${_discreteCustomValues.end.round()}'),
                  onChanged: (RangeValues values) {
433
                    setState(() {
434
                      _discreteCustomValues = values;
435
                    });
436
                  },
437
                ),
438 439 440 441 442
              ),
              const Text('Discrete with Custom Theme'),
            ],
          ),
        ],
443
      ),
444
    );
445 446
  }
}