rvamsikrishna / flutter_fluid_slider

A fluid design slider that works just like the Slider material widget.
MIT License
322 stars 47 forks source link

Migrate to NNBD #18

Open PcolBP opened 3 years ago

PcolBP commented 3 years ago

🚀 Feature Requests

Migrate to NNBD

Any plans on null-safety migration?

Platforms affected

IOS ANDROID

PcolBP commented 3 years ago

Already migrated on my own: https://pub.dev/packages/flutter_fluid_slider_nnbd

PcolBP commented 3 years ago

If anyone need to implement in your own project here is a migrated code:


import 'dart:ui' show lerpDouble;

import 'package:flutter/material.dart';

///A fluid design slider that works just like the [Slider] material widget.
///
/// Used to select from a range of values.
///
/// The fluid slider will be disabled if [onChanged] is null.
///
///
/// By default, a fluid slider will be as wide as possible, with a height of 60.0. When
/// given unbounded constraints, it will attempt to make itself 200.0 wide.
///

class FluidSlider extends StatefulWidget {
  ///Creates a fluid slider
  ///
  /// * [value] determines currently selected value for this slider.
  /// * [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.
  ///
  ///
  ///
  /// The currently selected value for this slider.
  ///
  /// The slider's thumb is drawn at a position that corresponds to this value.
  final double value;

  /// The minimum value the user can select.
  ///
  /// Defaults to 0.0. Must be less than or equal to [max].
  final double min;

  /// The maximum value the user can select.
  ///
  /// Defaults to 1.0. Must be greater than or equal to [min].
  final double max;

  ///The widget to be displayed as the min label. For eg: an Icon can be displayed.
  ///
  ///If not provided the [min] value is displayed as text.
  ///
  /// ## Sample code
  ///
  /// ```dart
  /// FluidSlider(
  ///   value: _value,
  ///   min: 1.0,
  ///   max: 100.0,
  ///   start: Icon(
  ///     Icons.money_off,
  ///     color: Colors.white,
  ///   ),
  ///   onChanged: (double newValue) {
  ///     setState(() {
  ///       _duelCommandment = newValue.round();
  ///     });
  ///   },
  /// )
  /// ```
  ///
  final Widget? start;

  ///The widget to be displayed as the max label. For eg: an Icon can be displayed.
  ///
  ///If not provided the [max] value is displayed as text.
  final Widget? end;

  /// Called during a drag when the user is selecting a new value for the slider
  /// by dragging.
  ///
  /// 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.

  final ValueChanged<double>? onChanged;

  /// Called when the user starts selecting a new value for the slider.
  ///
  /// The value passed will be the last [value] that the slider had before the
  /// change began.
  final ValueChanged<double>? onChangeStart;

  /// Called when the user is done selecting a new value for the slider.
  final ValueChanged<double>? onChangeEnd;

  ///The styling of the min and max text that gets displayed on the slider
  ///
  ///If not provided the ancestor [Theme]'s [accentTextTheme] text style will be applied.
  final TextStyle? labelsTextStyle;

  ///The styling of the current value text that gets displayed on the slider
  ///
  ///If not provided the ancestor [Theme]'s [textTheme.title] text style
  ///with bold will be applied .
  final TextStyle? valueTextStyle;

  ///The color of the slider.
  ///
  ///If not provided the ancestor [Theme]'s [primaryColor] will be applied.
  final Color? sliderColor;

  ///The color of the thumb.
  ///
  ///If not provided the [Colors.white] will be applied.
  final Color? thumbColor;

  ///Whether to display the first decimal value of the slider value
  ///
  ///defaults to false
  final bool showDecimalValue;

  ///Callback function to map the double values to String texts
  ///
  ///If null the value is converted to String based on [showDecimalValue]
  final String Function(double)? mapValueToString;

  ///The diameter of the thumb, it's also the height of the slider
  ///
  ///defaults to 60.0
  final double? thumbDiameter;

  const FluidSlider({
    Key? key,
    required this.value,
    this.min = 0.0,
    this.max = 1.0,
    this.start,
    this.end,
    this.onChanged,
    this.labelsTextStyle,
    this.valueTextStyle,
    this.onChangeStart,
    this.onChangeEnd,
    this.sliderColor,
    this.thumbColor,
    this.mapValueToString,
    this.showDecimalValue = false,
    this.thumbDiameter,
  })  : assert(min <= max),
        assert(value >= min && value <= max),
        super(key: key);

  @override
  _FluidSliderState createState() => _FluidSliderState();
}

class _FluidSliderState extends State<FluidSlider>
    with SingleTickerProviderStateMixin {
  late double _sliderWidth;
  late AnimationController _animationController;
  late CurvedAnimation _thumbAnimation;
  late double thumbDiameter;
  double _currX = 0.0;

  @override
  initState() {
    super.initState();
    //The radius of the slider thumb control
    thumbDiameter = widget.thumbDiameter ?? 60.0;
    _animationController = AnimationController(
      duration: Duration(milliseconds: 400),
      vsync: this,
    );

    _thumbAnimation = CurvedAnimation(
      curve: Curves.fastOutSlowIn,
      parent: _animationController,
    );
  }

  @override
  dispose() {
    _animationController.dispose();
    super.dispose();
  }

  Offset _getGlobalToLocal(Offset globalPosition) {
    final RenderBox renderBox = context.findRenderObject() as RenderBox;
    return renderBox.globalToLocal(globalPosition);
  }

  void _onHorizontalDragDown(DragDownDetails details) {
    if (_isInteractive) {
      _animationController.forward();
    }
  }

  void _onHorizontalDragStart(DragStartDetails details) {
    if (_isInteractive) {
      if (widget.onChangeStart != null) {
        _handleDragStart(widget.value);
      }
      _currX = _getGlobalToLocal(details.globalPosition).dx / _sliderWidth;
    }
  }

  void _onHorizontalDragUpdate(DragUpdateDetails details) {
    if (_isInteractive && details.primaryDelta != null) {
      final double valueDelta = details.primaryDelta! / _sliderWidth;
      _currX += valueDelta;

      _handleChanged(_clamp(_currX));
    }
  }

  void _onHorizontalDragEnd(DragEndDetails details) {
    if (widget.onChangeEnd != null) {
      _handleDragEnd(_clamp(_currX));
    }
    _currX = 0.0;
    _animationController.reverse();
  }

  void _onHorizontalDragCancel() {
    if (widget.onChangeEnd != null) {
      _handleDragEnd(_clamp(_currX));
    }
    _currX = 0.0;
    _animationController.reverse();
  }

  double _clamp(double value) {
    return value.clamp(0.0, 1.0);
  }

  void _handleChanged(double value) {
    if (widget.onChanged != null) {
      final double lerpValue = _lerp(value);
      if (lerpValue != widget.value) {
        widget.onChanged!(lerpValue);
      }
    }
  }

  void _handleDragStart(double value) {
    if (widget.onChangeStart != null) widget.onChangeStart!((value));
  }

  void _handleDragEnd(double value) {
    if (widget.onChangeEnd != null) widget.onChangeEnd!((_lerp(value)));
  }

  // 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;
  }

  // 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);
    return widget.max > widget.min
        ? (value - widget.min) / (widget.max - widget.min)
        : 0.0;
  }

  Color get _sliderColor {
    if (_isInteractive) {
      return widget.sliderColor ?? Theme.of(context).primaryColor;
    } else {
      return Colors.grey;
    }
  }

  Color get _thumbColor {
    if (_isInteractive) {
      return widget.thumbColor ?? Colors.white;
    } else {
      return Colors.grey.shade300;
    }
  }

  bool get _isInteractive => widget.onChanged != null;

  TextStyle _currentValTextStyle(BuildContext context) {
    final TextStyle defaultStyle = widget.showDecimalValue
        ? Theme.of(context)
            .textTheme
            .headline5!
            .copyWith(fontWeight: FontWeight.bold)
        : Theme.of(context)
            .textTheme
            .headline6!
            .copyWith(fontWeight: FontWeight.bold);

    return widget.valueTextStyle ?? defaultStyle;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        //The offset of the thumb so that it does not touch the slider border when at min/max position.
        final double thumbPadding = 8.0;
        //The value by which the thum positions should interpolate.
        final double thumbPosFactor = _unlerp(widget.value);
        double remainingWidth;

        //Setting the slider width to its parent's max width if constraint width is present else set to 200.0
        //This is used to compute the thumb position and also
        //calculate the delta drag value in the horizontal drag handlers.
        _sliderWidth =
            constraints.hasBoundedWidth ? constraints.maxWidth : 200.0;

        //The width remaining for the thumb to be dragged upto.
        remainingWidth = _sliderWidth - thumbDiameter - 2 * thumbPadding;

        //The position of the thumb control of the slider from max value.
        final double thumbPositionLeft =
            lerpDouble(thumbPadding, remainingWidth, thumbPosFactor)!;

        //The position of the thumb control of the slider from min value.
        final double thumbPositionRight =
            lerpDouble(remainingWidth, thumbPadding, thumbPosFactor)!;

        //Start position of slider thumb.
        final RelativeRect beginRect = RelativeRect.fromLTRB(
            thumbPositionLeft, 0.00, thumbPositionRight, 0.0);

        //Popped up position of slider thumb.
        final poppedPosition = thumbDiameter + 5;
        final RelativeRect endRect = RelativeRect.fromLTRB(thumbPositionLeft,
            poppedPosition * -1, thumbPositionRight, poppedPosition);

        //Describes the position of the thumb slider.
        //Mainly useful to animate the thumb popping up.
        Animation<RelativeRect> thumbPosition = RelativeRectTween(
          begin: beginRect,
          end: endRect,
        ).animate(_thumbAnimation);

        return Container(
          width: _sliderWidth,
          height: thumbDiameter,
          decoration: BoxDecoration(
            color: _sliderColor,
            borderRadius: BorderRadius.only(
              topLeft: Radius.circular(5.0),
              topRight: Radius.circular(5.0),
            ),
          ),
          child: Stack(
            clipBehavior: Clip.none,
            children: <Widget>[
              _MinMaxLabels(
                textStyle: widget.labelsTextStyle,
                alignment: Alignment.centerLeft,
                child: widget.start,
                value: widget.min,
                padding: EdgeInsets.only(left: 15.0),
              ),
              _MinMaxLabels(
                textStyle: widget.labelsTextStyle,
                alignment: Alignment.centerRight,
                child: widget.end,
                value: widget.max,
                padding: EdgeInsets.only(right: 15.0),
              ),
              PositionedTransition(
                rect: thumbPosition,
                child: CustomPaint(
                  painter: _ThumbSplashPainter(
                    showContact: _animationController,
                    thumbPadding: thumbPadding,
                    splashColor: _sliderColor,
                  ),
                  child: GestureDetector(
                    onHorizontalDragCancel: _onHorizontalDragCancel,
                    onHorizontalDragDown: _onHorizontalDragDown,
                    onHorizontalDragStart: _onHorizontalDragStart,
                    onHorizontalDragUpdate: _onHorizontalDragUpdate,
                    onHorizontalDragEnd: _onHorizontalDragEnd,
                    child: Container(
                      width: thumbDiameter,
                      height: thumbDiameter,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: _sliderColor,
                      ),
                      alignment: Alignment.center,
                      child: Container(
                        width: 0.75 * thumbDiameter,
                        height: 0.75 * thumbDiameter,
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: _thumbColor,
                        ),
                        child: Center(
                          child: Text(
                            widget.mapValueToString != null
                                ? widget.mapValueToString!(widget.value)
                                : widget.showDecimalValue
                                    ? widget.value.toStringAsFixed(1)
                                    : widget.value.toInt().toString(),
                            style: _currentValTextStyle(context),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

class _ThumbSplashPainter extends CustomPainter {
  final Animation showContact;

  //This is passed to calculate and compensate the value
  //of x for drawing the sticky fluid
  final thumbPadding;
  final Color splashColor;

  _ThumbSplashPainter({
    this.thumbPadding,
    required this.showContact,
    required this.splashColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // print(size);
    if (showContact.value >= 0.5) {
      final Offset center = Offset(size.width / 2, size.height / 2);

      final Path path = Path();
      path.moveTo(-0.0, size.height + 6.0);
      path.quadraticBezierTo(
          center.dx, size.height, thumbPadding / 2, center.dy);

      path.lineTo(size.width - thumbPadding / 2, center.dy);

      path.quadraticBezierTo(
          center.dx, size.height, size.width + 0.0, size.height + 6.0);

      path.close();
      canvas.drawPath(path, Paint()..color = splashColor);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

class _MinMaxLabels extends StatelessWidget {
  final Alignment alignment;
  final TextStyle? textStyle;
  final Widget? child;
  final double value;
  final EdgeInsets padding;

  const _MinMaxLabels({
    Key? key,
    required this.alignment,
    this.textStyle,
    this.child,
    required this.value,
    required this.padding,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: padding,
      child: Align(
        alignment: alignment,
        child: child ??
            Text(
              '${value.toInt()}',
              style: textStyle ?? Theme.of(context).accentTextTheme.headline6!,
            ),
      ),
    );
  }
}