// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; import 'thumb_painter.dart'; /// An iOS-style slider. /// /// Used to select from a range of values. /// /// A slider can be used to select from either a continuous or a discrete set of /// values. The default is use a continuous range of values from [min] to [max]. /// To use discrete values, use a non-null value for [divisions], which /// indicates the number of discrete intervals. For example, if [min] is 0.0 and /// [max] is 50.0 and [divisions] is 5, then the slider can take on the values /// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0. /// /// The slider itself does not maintain any state. Instead, when the state of /// the slider changes, the widget calls the [onChanged] callback. Most widgets /// that use a slider will listen for the [onChanged] callback and rebuild the /// slider with a new [value] to update the visual appearance of the slider. /// /// See also: /// /// * <https://developer.apple.com/ios/human-interface-guidelines/ui-controls/sliders/> class CupertinoSlider extends StatefulWidget { /// Creates an iOS-style slider. /// /// The slider itself does not maintain any state. Instead, when the state of /// the slider changes, the widget calls the [onChanged] callback. Most widgets /// that use a slider will listen for the [onChanged] callback and rebuild the /// slider with a new [value] to update the visual appearance of the slider. /// /// * [value] determines currently selected value for this slider. /// * [onChanged] is called when the user selects a new value for the slider. const CupertinoSlider({ Key key, @required this.value, @required this.onChanged, this.min: 0.0, this.max: 1.0, this.divisions, this.activeColor: CupertinoColors.activeBlue, }) : assert(value != null), assert(min != null), assert(max != null), assert(value >= min && value <= max), assert(divisions == null || divisions > 0), super(key: key); /// The currently selected value for this slider. /// /// The slider's thumb is drawn at a position that corresponds to this value. final double value; /// Called when the user selects a new value for the slider. /// /// The slider passes the new value to the callback but does not actually /// change state until the parent widget rebuilds the slider with the new /// value. /// /// If null, the slider will be displayed as disabled. /// /// The callback provided to onChanged should update the state of the parent /// [StatefulWidget] using the [State.setState] method, so that the parent /// gets rebuilt; for example: /// /// ```dart /// new CupertinoSlider( /// value: _duelCommandment.toDouble(), /// min: 1.0, /// max: 10.0, /// divisions: 10, /// onChanged: (double newValue) { /// setState(() { /// _duelCommandment = newValue.round(); /// }); /// }, /// ) /// ``` final ValueChanged<double> onChanged; /// The minimum value the user can select. /// /// Defaults to 0.0. final double min; /// The maximum value the user can select. /// /// Defaults to 1.0. final double max; /// The number of discrete divisions. /// /// If null, the slider is continuous. final int divisions; /// The color to use for the portion of the slider that has been selected. final Color activeColor; @override _CupertinoSliderState createState() => new _CupertinoSliderState(); @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description.add(new DoubleProperty('value', value)); description.add(new DoubleProperty('min', min)); description.add(new DoubleProperty('max', max)); } } class _CupertinoSliderState extends State<CupertinoSlider> with TickerProviderStateMixin { void _handleChanged(double value) { assert(widget.onChanged != null); widget.onChanged(value * (widget.max - widget.min) + widget.min); } @override Widget build(BuildContext context) { return new _CupertinoSliderRenderObjectWidget( value: (widget.value - widget.min) / (widget.max - widget.min), divisions: widget.divisions, activeColor: widget.activeColor, onChanged: widget.onChanged != null ? _handleChanged : null, vsync: this, ); } } class _CupertinoSliderRenderObjectWidget extends LeafRenderObjectWidget { const _CupertinoSliderRenderObjectWidget({ Key key, this.value, this.divisions, this.activeColor, this.onChanged, this.vsync, }) : super(key: key); final double value; final int divisions; final Color activeColor; final ValueChanged<double> onChanged; final TickerProvider vsync; @override _RenderCupertinoSlider createRenderObject(BuildContext context) { return new _RenderCupertinoSlider( value: value, divisions: divisions, activeColor: activeColor, onChanged: onChanged, vsync: vsync, textDirection: Directionality.of(context), ); } @override void updateRenderObject(BuildContext context, _RenderCupertinoSlider renderObject) { renderObject ..value = value ..divisions = divisions ..activeColor = activeColor ..onChanged = onChanged ..textDirection = Directionality.of(context); // Ticker provider cannot change since there's a 1:1 relationship between // the _SliderRenderObjectWidget object and the _SliderState object. } } const double _kPadding = 8.0; const double _kTrackHeight = 2.0; const Color _kTrackColor = const Color(0xFFB5B5B5); const double _kSliderHeight = 2.0 * (CupertinoThumbPainter.radius + _kPadding); const double _kSliderWidth = 176.0; // Matches Material Design slider. final Duration _kDiscreteTransitionDuration = const Duration(milliseconds: 500); const double _kAdjustmentUnit = 0.1; // Matches iOS implementation of material slider. class _RenderCupertinoSlider extends RenderConstrainedBox { _RenderCupertinoSlider({ @required double value, int divisions, Color activeColor, ValueChanged<double> onChanged, TickerProvider vsync, @required TextDirection textDirection, }) : assert(value != null && value >= 0.0 && value <= 1.0), assert(textDirection != null), _value = value, _divisions = divisions, _activeColor = activeColor, _onChanged = onChanged, _textDirection = textDirection, super(additionalConstraints: const BoxConstraints.tightFor(width: _kSliderWidth, height: _kSliderHeight)) { _drag = new HorizontalDragGestureRecognizer() ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate ..onEnd = _handleDragEnd; _position = new AnimationController( value: value, duration: _kDiscreteTransitionDuration, vsync: vsync, )..addListener(markNeedsPaint); } double get value => _value; double _value; set value(double newValue) { assert(newValue != null && newValue >= 0.0 && newValue <= 1.0); if (newValue == _value) return; _value = newValue; if (divisions != null) _position.animateTo(newValue, curve: Curves.fastOutSlowIn); else _position.value = newValue; } int get divisions => _divisions; int _divisions; set divisions(int value) { if (value == _divisions) return; _divisions = value; markNeedsPaint(); } Color get activeColor => _activeColor; Color _activeColor; set activeColor(Color value) { if (value == _activeColor) return; _activeColor = value; markNeedsPaint(); } ValueChanged<double> get onChanged => _onChanged; ValueChanged<double> _onChanged; set onChanged(ValueChanged<double> value) { if (value == _onChanged) return; final bool wasInteractive = isInteractive; _onChanged = value; if (wasInteractive != isInteractive) markNeedsSemanticsUpdate(); } TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { assert(value != null); if (_textDirection == value) return; _textDirection = value; markNeedsPaint(); } AnimationController _position; HorizontalDragGestureRecognizer _drag; double _currentDragValue = 0.0; double get _discretizedCurrentDragValue { double dragValue = _currentDragValue.clamp(0.0, 1.0); if (divisions != null) dragValue = (dragValue * divisions).round() / divisions; return dragValue; } double get _trackLeft => _kPadding; double get _trackRight => size.width - _kPadding; double get _thumbCenter { double visualPosition; switch (textDirection) { case TextDirection.rtl: visualPosition = 1.0 - _value; break; case TextDirection.ltr: visualPosition = _value; break; } return lerpDouble(_trackLeft + CupertinoThumbPainter.radius, _trackRight - CupertinoThumbPainter.radius, visualPosition); } bool get isInteractive => onChanged != null; void _handleDragStart(DragStartDetails details) { if (isInteractive) { _currentDragValue = _value; onChanged(_discretizedCurrentDragValue); } } void _handleDragUpdate(DragUpdateDetails details) { if (isInteractive) { final double extent = math.max(_kPadding, size.width - 2.0 * (_kPadding + CupertinoThumbPainter.radius)); final double valueDelta = details.primaryDelta / extent; switch (textDirection) { case TextDirection.rtl: _currentDragValue -= valueDelta; break; case TextDirection.ltr: _currentDragValue += valueDelta; break; } onChanged(_discretizedCurrentDragValue); } } void _handleDragEnd(DragEndDetails details) { _currentDragValue = 0.0; } @override bool hitTestSelf(Offset position) { return (position.dx - _thumbCenter).abs() < CupertinoThumbPainter.radius + _kPadding; } @override void handleEvent(PointerEvent event, BoxHitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent && isInteractive) _drag.addPointer(event); } final CupertinoThumbPainter _thumbPainter = new CupertinoThumbPainter(); @override void paint(PaintingContext context, Offset offset) { double visualPosition; Color leftColor; Color rightColor; switch (textDirection) { case TextDirection.rtl: visualPosition = 1.0 - _position.value; leftColor = _kTrackColor; rightColor = _activeColor; break; case TextDirection.ltr: visualPosition = _position.value; leftColor = _activeColor; rightColor = _kTrackColor; break; } final double trackCenter = offset.dy + size.height / 2.0; final double trackLeft = offset.dx + _trackLeft; final double trackTop = trackCenter - 1.0; final double trackBottom = trackCenter + 1.0; final double trackRight = offset.dx + _trackRight; final double trackActive = offset.dx + _thumbCenter; final Canvas canvas = context.canvas; final Paint paint = new Paint(); if (visualPosition > 0.0) { paint.color = rightColor; canvas.drawRRect(new RRect.fromLTRBXY(trackLeft, trackTop, trackActive, trackBottom, 1.0, 1.0), paint); } if (visualPosition < 1.0) { paint.color = leftColor; canvas.drawRRect(new RRect.fromLTRBXY(trackActive, trackTop, trackRight, trackBottom, 1.0, 1.0), paint); } final Offset thumbCenter = new Offset(trackActive, trackCenter); _thumbPainter.paint(canvas, new Rect.fromCircle(center: thumbCenter, radius: CupertinoThumbPainter.radius)); } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config.isSemanticBoundary = isInteractive; if (isInteractive) { config.addAction(SemanticsAction.increase, _increaseAction); config.addAction(SemanticsAction.decrease, _decreaseAction); } } double get _semanticActionUnit => divisions != null ? 1.0 / divisions : _kAdjustmentUnit; void _increaseAction() { if (isInteractive) onChanged((value + _semanticActionUnit).clamp(0.0, 1.0)); } void _decreaseAction() { if (isInteractive) onChanged((value - _semanticActionUnit).clamp(0.0, 1.0)); } }