diff --git a/CHANGELOG.md b/CHANGELOG.md index 63004ba..c2aa5e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.6.0 + +- Use `WheelSpinnerTheme` and `WheelSpinnerThemeData` to define styles, similar to `Theme` and + `ThemeData` in Flutter + +- Remove min/max labels and +/- symbols as they are too coupled to the widget which doesn't seem to + be in its' logical scope. It can be easily recreated and modified more extensively when done + manually. + +- Remove height/width params - the widget adheres to its parent constraints, and since the labels + are not being built, the constraints can reliably set the size of the widget. + ## 0.5.1 - Add example.dart diff --git a/README.md b/README.md index d14a494..40dd769 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,44 @@ event handlers. Here is a simple usage example: Widget build(BuildContext context) { return WheelSpinner( value: value, - width: width, min: 0.0, max: 100.0, - borderRadius: borderRadius, - minMaxLabelBuilder: (value) => value, onSlideUpdate: (val) => onChange(value), ); } ``` + +## Customizing the theme + +You can use the `theme` property to override a theme once, or wrap many sliders in the same +`WheelSpinnerTheme` widget, which references a theme in its' `data` property. + +**Direct override example:** + +```dart +WheelSpinner( + value: value, + min: 0.0, + max: 100.0, + onSlideUpdate: (val) => onChange(value), + theme: WheelSpinnerThemeData.light().copyWith( + borderRadius: BorderRadius.circular(10), + ), +) +``` + +**Inherited widget override example:** + +```dart +WheelSpinnerTheme( + data: WheelSpinnerThemeData.light().copyWith( + borderRadius: BorderRadius.circular(10), + ), + child: WheelSpinner( + value: value, + min: 0.0, + max: 100.0, + onSlideUpdate: (val) => onChange(value), + ), +) +``` diff --git a/example/example.dart b/example/example.dart index 0bee9ba..b532e2b 100644 --- a/example/example.dart +++ b/example/example.dart @@ -1,7 +1,6 @@ import 'dart:math'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter/src/widgets/container.dart'; +import 'package:flutter/material.dart'; import 'package:wheel_spinner/wheel_spinner.dart'; class MyWidget extends StatefulWidget { @@ -22,14 +21,23 @@ class _MyWidgetState extends State { @override Widget build(BuildContext context) { - final width = 100.0; - - return WheelSpinner( - value: value, - width: width, - min: 0.0, - max: 100.0, - onSlideUpdate: (val) => setState(() => value = val), + return Center( + child: SizedBox( + width: 100, + height: 60, + child: WheelSpinnerTheme( + data: WheelSpinnerThemeData.light().copyWith( + borderRadius: BorderRadius.circular(10), + dividerColor: Colors.black, + ), + child: WheelSpinner( + value: value, + min: 0.0, + max: 100.0, + onSlideUpdate: (val) => setState(() => value = val), + ), + ), + ), ); } } diff --git a/lib/wheel_spinner.dart b/lib/wheel_spinner.dart index 73fb754..25fb8b0 100644 --- a/lib/wheel_spinner.dart +++ b/lib/wheel_spinner.dart @@ -3,6 +3,9 @@ library wheel_spinner; import 'package:flutter/material.dart'; import 'package:wheel_spinner/utils.dart'; +import 'package:wheel_spinner/wheel_spinner_theme.dart'; + +export 'wheel_spinner_theme.dart'; typedef ValueBuilder = Widget Function(double value); typedef ValueStringBuilder = String Function(double value); @@ -16,12 +19,6 @@ class WheelSpinner extends StatefulWidget { /// Callback for when the user lets go of the slider final Function(double value)? onSlideDone; - /// The widget [width] - final double width; - - /// The widget [height] - final double height; - /// The initial [value] for the slider final double value; @@ -37,60 +34,26 @@ class WheelSpinner extends StatefulWidget { /// Allows overriding the format of the left top and bottom labels for the [min]/[max] values final ValueStringBuilder? minMaxLabelBuilder; - /// Allows to override style of labels - final TextStyle? labelStyle; - - /// Override box decoration for the control - final BoxDecoration? boxDecoration; - - /// Override box border for the control's [boxDecoration]. - /// If [boxDecoration] is specified, it overrides this property. - final Border? border; - - /// Override border radius for the control's [boxDecoration]. - /// If [boxDecoration] is specified, it overrides this property. - final BorderRadius? borderRadius; - - /// Override background color for the control's [boxDecoration]. - /// If [boxDecoration] is specified, it overrides this property. - final Color? color; - - /// Override background gradient for the control's [boxDecoration]. - /// If [boxDecoration] is specified, it overrides this property. - final Gradient? gradient; - - /// Amount of divisions to show on the knob - final int dividerCount; - - /// Color of the lines dividing the control. - final Color? dividerColor; - /// The drag speed factor final double _dragSpeedFactor = 1.0; + /// The theme for this wheel spinner + final WheelSpinnerThemeData? theme; + /// The default min/max label builder. static ValueStringBuilder defaultMinMaxLabelBuilder = (v) => v.toStringAsFixed(2); - const WheelSpinner( - {Key? key, - this.onSlideUpdate, - this.onSlideDone, - this.width = 60, - this.height = 100, - this.min = double.negativeInfinity, - this.max = double.infinity, - this.value = 0.5, - this.childBuilder, - this.minMaxLabelBuilder, - this.labelStyle, - this.dividerCount = 10, - this.dividerColor, - this.boxDecoration, - this.border, - this.borderRadius, - this.color, - this.gradient}) - : super(key: key); + const WheelSpinner({ + Key? key, + this.onSlideUpdate, + this.onSlideDone, + this.min = double.negativeInfinity, + this.max = double.infinity, + this.value = 0.5, + this.childBuilder, + this.minMaxLabelBuilder, + this.theme, + }) : super(key: key); @override // ignore: library_private_types_in_public_api @@ -115,57 +78,61 @@ class _WheelSpinnerState extends State with SingleTickerProviderSt super.initState(); } + WheelSpinnerThemeData get theme => defaultTheme.copyWith( + border: widget.theme?.border, + borderRadius: widget.theme?.borderRadius, + color: widget.theme?.color, + gradient: widget.theme?.gradient, + boxDecoration: widget.theme?.boxDecoration, + dividerCount: widget.theme?.dividerCount, + dividerColor: widget.theme?.dividerColor, + ); + + WheelSpinnerThemeData get defaultTheme => + WheelSpinnerTheme.of(context) ?? + (Theme.of(context).brightness == Brightness.light + ? WheelSpinnerThemeData.light() + : WheelSpinnerThemeData.dark()); + @override Widget build(BuildContext context) { - final minMaxBuilder = widget.minMaxLabelBuilder ?? WheelSpinner.defaultMinMaxLabelBuilder; - final labelFontSize = Theme.of(context).textTheme.bodyText2!.fontSize! * 0.75; - final labelStyle = TextStyle(fontSize: labelFontSize).merge(widget.labelStyle); - - final minText = widget.max < double.infinity ? minMaxBuilder(widget.max) : null; - final maxText = widget.min > double.negativeInfinity ? minMaxBuilder(widget.min) : null; - - return SizedBox( - height: widget.height, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - minText != null ? Text(minText, style: labelStyle) : Container(), - maxText != null ? Text(maxText, style: labelStyle) : Container(), - ], - ), - ), - ), - AnimatedBuilder( - animation: flingAnimation, - builder: (context, child) => GestureDetector( - onVerticalDragStart: onDragStart, - onVerticalDragUpdate: onDragUpdate, - onVerticalDragEnd: onDragDone, - child: SizedBox.fromSize( - size: Size(widget.width.toDouble(), widget.height.toDouble()), + return LayoutBuilder(builder: (context, constraints) { + debugPrint('borderRadius: ${theme.borderRadius}'); + return SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedBuilder( + animation: flingAnimation, + builder: (context, child) => GestureDetector( + onVerticalDragStart: onDragStart, + onVerticalDragUpdate: onDragUpdate, + onVerticalDragEnd: onDragDone, child: Container( - decoration: widget.boxDecoration ?? _boxDecorationBuilder(), + width: constraints.maxWidth, + height: constraints.maxHeight, + decoration: theme.boxDecoration ?? _boxDecorationBuilder(), child: Stack( children: List.generate( - widget.dividerCount + 1, + theme.dividerCount + 1, (i) { - final top = lineTopPos(value, i, flingAnimation.value); + final top = lineTopPos( + value, + i, + flingAnimation.value, + constraints.maxHeight, + ); return Positioned.fromRect( rect: Rect.fromLTWH( 0.0, top, - widget.width.toDouble(), + constraints.maxWidth, 0, ), child: Divider( - color: widget.dividerColor ?? Colors.grey[600], + color: theme.dividerColor, ), ); }, @@ -175,50 +142,24 @@ class _WheelSpinnerState extends State with SingleTickerProviderSt ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('+', style: labelStyle), - Text('-', style: labelStyle), - ], - ), - ), - ), - ], - ), - ); + ], + ), + ); + }); } BoxDecoration _boxDecorationBuilder() { - const shadowOffset = 0.2; return BoxDecoration( - gradient: (widget.gradient ?? widget.color) == null - ? LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - stops: const [0.0, shadowOffset, 1.0 - shadowOffset, 1.0], - colors: [Colors.grey[350]!, Colors.grey[50]!, Colors.grey[50]!, Colors.grey[350]!], - ) - : null, - color: widget.color, - border: widget.border ?? - Border.all( - width: 1, - style: BorderStyle.solid, - color: Colors.grey[600]!, - ), - borderRadius: widget.borderRadius ?? BorderRadius.circular(3.5), + gradient: theme.gradient, + color: theme.color, + border: theme.border, + borderRadius: theme.borderRadius, ); } - double lineTopPos(double value, int i, double fling) { - final valueFraction = (value.ceil() - value) * widget.dividerCount; - final indexedTop = (widget.height / widget.dividerCount * i); + double lineTopPos(double value, int i, double fling, double maxHeight) { + final valueFraction = (value.ceil() - value) * theme.dividerCount; + final indexedTop = (maxHeight / theme.dividerCount * i); final top = indexedTop + valueFraction; return top; } diff --git a/lib/wheel_spinner_theme.dart b/lib/wheel_spinner_theme.dart new file mode 100644 index 0000000..e0f44d7 --- /dev/null +++ b/lib/wheel_spinner_theme.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +/// The theme for the wheel spinner +/// You can use the constructor [WheelSpinnerThemeData] to create your own theme, +/// or use [WheelSpinnerThemeData.light()] or [WheelSpinnerThemeData.dark()] and then use [copyWith] to +/// override only specifics. +class WheelSpinnerThemeData { + /// Override box decoration for the control + final BoxDecoration? boxDecoration; + + /// Override box border for the control's [boxDecoration]. + /// If [boxDecoration] is specified, this is ignored. + final Border? border; + + /// Override border radius for the control's [boxDecoration]. + /// If [boxDecoration] is specified, this is ignored. + final BorderRadius? borderRadius; + + /// Override background color for the control's [boxDecoration]. + /// If [boxDecoration] or [gradient] is specified, this is ignored. + final Color? color; + + /// Override background gradient for the control's [boxDecoration]. + /// If [boxDecoration] is specified, this is ignored. + final Gradient? gradient; + + /// Amount of lines dividing the control. Defaults to 10. + final int dividerCount; + + /// Color of the lines dividing the control. + final Color? dividerColor; + + /// Create a new theme for the wheel spinner + WheelSpinnerThemeData({ + this.boxDecoration, + this.border, + this.borderRadius = defaultBorderRadius, + this.color, + this.gradient, + this.dividerCount = 10, + this.dividerColor, + }); + + /// default shadow offset for both light+dark themes + static const double defaultShadowOffset = 0.2; + + /// default border radius for both light+dark themes + static const BorderRadius defaultBorderRadius = BorderRadius.all(Radius.circular(8)); + + /// A default light theme + WheelSpinnerThemeData.light() + : dividerColor = Colors.grey[600], + dividerCount = 10, + border = Border.all( + width: 1, + style: BorderStyle.solid, + color: Colors.grey[600]!, + ), + gradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, defaultShadowOffset, 1.0 - defaultShadowOffset, 1.0], + colors: [Colors.grey[350]!, Colors.grey[50]!, Colors.grey[50]!, Colors.grey[350]!], + ), + borderRadius = defaultBorderRadius, + boxDecoration = null, + color = null; + + /// A default dark theme + WheelSpinnerThemeData.dark() + : dividerColor = Colors.grey[800], + dividerCount = 10, + border = Border.all( + width: 1, + style: BorderStyle.solid, + color: Colors.grey[600]!, + ), + gradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, defaultShadowOffset, 1 - defaultShadowOffset, 1.0], + colors: [Colors.black, Colors.grey[900]!, Colors.grey[900]!, Colors.black], + ), + borderRadius = defaultBorderRadius, + boxDecoration = null, + color = null; + + /// Create a new theme based on this one, but with the given properties overridden. + /// For each property that is null, the original value is used. + WheelSpinnerThemeData copyWith({ + BoxDecoration? boxDecoration, + Border? border, + BorderRadius? borderRadius, + Color? color, + Gradient? gradient, + int? dividerCount, + Color? dividerColor, + }) => + WheelSpinnerThemeData( + boxDecoration: boxDecoration ?? this.boxDecoration, + border: border ?? this.border, + borderRadius: borderRadius ?? this.borderRadius, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + dividerCount: dividerCount ?? this.dividerCount, + dividerColor: dividerColor ?? this.dividerColor, + ); +} + +/// Theme container widget for the wheel spinner theme. +/// +/// Any wheel spinners below this widget in the tree will +/// inherit the theme given in [data], except when overridden manually as a property of the wheel spinner itself. +class WheelSpinnerTheme extends InheritedWidget { + final WheelSpinnerThemeData data; + + const WheelSpinnerTheme({ + super.key, + required this.data, + required super.child, + }); + + @override + bool updateShouldNotify(covariant WheelSpinnerTheme oldWidget) => oldWidget.data != data; + + /// Get the nearest wheel spinner theme data up the widget tree from the given context. + static WheelSpinnerThemeData? of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()?.data; +} diff --git a/pubspec.yaml b/pubspec.yaml index b0177cf..fc6b2e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: wheel_spinner description: A simple Flutter widget for updating a number using a pitch bender-like spinner -version: 0.5.1 +version: 0.6.0 homepage: https://github.com/chenasraf/wheel_spinner +repository: https://github.com/chenasraf/wheel_spinner environment: sdk: '>=2.17.0 <3.0.0' @@ -51,3 +52,8 @@ flutter: # # For details regarding fonts in packages, see # https://flutter.io/custom-fonts/#from-packages + +script_runner: + shell: /bin/zsh + scripts: + - doc: flutter pub global run dartdoc:dartdoc