diff --git a/.gitignore b/.gitignore index 1985397..ac0c6e2 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ build/ !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 + +# Coverage +coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 56198ab..a4c6778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## 0.8.0-beta.0 +## 0.8.0-beta.1 (2023-07-31) +- adds `active` to all constructors +- closes [#30](https://github.com/splashbyte/animated_toggle_switch/issues/30) +- minor improvements +- BREAKING: changes `separatorBuilder` parameters +- BREAKING: moves all cursor parameters to `cursors` + +## 0.8.0-beta.0 (2023-07-29) - adds tests for all `AnimatedToggleSwitch` constructors - adds `separatorBuilder`, `customSeparatorBuilder`, `style` and `styleAnimationType` to `AnimatedToggleSwitch` - adds `separatorBuilder` to `CustomAnimatedToggleSwitch` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..9b1ed37 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + language: + strict-casts: true + strict-raw-types: true + +linter: + rules: \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 96c6206..f4c229a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -161,7 +161,7 @@ class _MyHomePageState extends State { const CupertinoActivityIndicator(color: Colors.white), onChanged: (b) { setState(() => positive = b); - return Future.delayed(Duration(seconds: 2)); + return Future.delayed(Duration(seconds: 2)); }, styleBuilder: (b) => ToggleStyle( indicatorColor: b ? Colors.purple : Colors.green), @@ -206,7 +206,7 @@ class _MyHomePageState extends State { Colors.red[800], green, global.position)), onChanged: (b) { setState(() => positive = b); - return Future.delayed(Duration(seconds: 2)); + return Future.delayed(Duration(seconds: 2)); }, iconBuilder: (value) => value ? Icon(Icons.power_outlined, color: green, size: 32.0) @@ -280,7 +280,7 @@ class _MyHomePageState extends State { values: const [0, 1, 2, 3], onChanged: (i) { setState(() => value = i); - return Future.delayed(Duration(seconds: 3)); + return Future.delayed(Duration(seconds: 3)); }, iconBuilder: rollingIconBuilder, ), @@ -307,6 +307,7 @@ class _MyHomePageState extends State { ), SizedBox(height: 16.0), AnimatedToggleSwitch.rolling( + active: false, current: value, values: const [0, 1, 2, 3], onChanged: (i) { @@ -314,7 +315,7 @@ class _MyHomePageState extends State { value = i; loading = true; }); - return Future.delayed(Duration(seconds: 3)) + return Future.delayed(Duration(seconds: 3)) .then((_) => setState(() => loading = false)); }, iconBuilder: rollingIconBuilder, @@ -374,7 +375,8 @@ class _MyHomePageState extends State { values: const [0, 1, 2, 3], onChanged: (i) => setState(() => value = i), iconBuilder: rollingIconBuilder, - separatorBuilder: (context, index) => const VerticalDivider(), + separatorBuilder: (index) => + SizedBox.expand(child: ColoredBox(color: Colors.red)), borderWidth: 4.5, style: ToggleStyle( indicatorColor: Colors.white, @@ -423,7 +425,7 @@ class _MyHomePageState extends State { iconBuilder: (context, local, global) { return const SizedBox(); }, - defaultCursor: SystemMouseCursors.click, + cursors: ToggleCursors(defaultCursor: SystemMouseCursors.click), onTap: () => setState(() => positive = !positive), iconsTappable: false, wrapperBuilder: (context, global, child) { @@ -497,7 +499,7 @@ class _MyHomePageState extends State { i.isEven == true ? Colors.amber : Colors.red), onChanged: (i) { setState(() => value = i); - return Future.delayed(Duration(seconds: 3)); + return Future.delayed(Duration(seconds: 3)); }, ), SizedBox(height: 16.0), diff --git a/example/pubspec.lock b/example/pubspec.lock index 928d22e..dd466fc 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "0.8.0" + version: "0.8.0-beta.1" async: dependency: transitive description: diff --git a/lib/animated_toggle_switch.dart b/lib/animated_toggle_switch.dart index 7012f0a..7b7c20b 100644 --- a/lib/animated_toggle_switch.dart +++ b/lib/animated_toggle_switch.dart @@ -1,5 +1,6 @@ library animated_toggle_switch; +import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; @@ -8,6 +9,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; part 'src/properties.dart'; +part 'src/style.dart'; +part 'src/cursors.dart'; part 'src/widgets/animated_toggle_switch.dart'; part 'src/widgets/custom_animated_toggle_switch.dart'; part 'src/widgets/drag_region.dart'; +part 'src/widgets/conditional_wrapper.dart'; diff --git a/lib/src/cursors.dart b/lib/src/cursors.dart new file mode 100644 index 0000000..1449765 --- /dev/null +++ b/lib/src/cursors.dart @@ -0,0 +1,56 @@ +// coverage:ignore-file +part of 'package:animated_toggle_switch/animated_toggle_switch.dart'; + +class ToggleCursors { + /// [MouseCursor] to show when not hovering an indicator. + /// + /// Defaults to [SystemMouseCursors.click] if [iconsTappable] is [true] + /// and to [MouseCursor.defer] otherwise. + final MouseCursor? defaultCursor; + + /// [MouseCursor] to show when grabbing the indicators. + final MouseCursor draggingCursor; + + /// [MouseCursor] to show when hovering the indicators. + final MouseCursor dragCursor; + + /// [MouseCursor] to show during loading. + final MouseCursor loadingCursor; + + /// [MouseCursor] to show when [active] is set to [false]. + final MouseCursor inactiveCursor; + + const ToggleCursors({ + this.defaultCursor, + this.draggingCursor = SystemMouseCursors.grabbing, + this.dragCursor = SystemMouseCursors.grab, + this.loadingCursor = MouseCursor.defer, + this.inactiveCursor = SystemMouseCursors.forbidden, + }); + + const ToggleCursors.all(MouseCursor cursor) + : defaultCursor = cursor, + draggingCursor = cursor, + dragCursor = cursor, + loadingCursor = cursor, + inactiveCursor = cursor; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ToggleCursors && + runtimeType == other.runtimeType && + defaultCursor == other.defaultCursor && + draggingCursor == other.draggingCursor && + dragCursor == other.dragCursor && + loadingCursor == other.loadingCursor && + inactiveCursor == other.inactiveCursor; + + @override + int get hashCode => + defaultCursor.hashCode ^ + draggingCursor.hashCode ^ + dragCursor.hashCode ^ + loadingCursor.hashCode ^ + inactiveCursor.hashCode; +} diff --git a/lib/src/properties.dart b/lib/src/properties.dart index facf13b..366edc5 100644 --- a/lib/src/properties.dart +++ b/lib/src/properties.dart @@ -34,6 +34,8 @@ class GlobalToggleProperties { /// [0] means 'not loading' and [1] means 'loading'. final double loadingAnimationValue; + final bool active; + const GlobalToggleProperties({ required this.position, required this.current, @@ -43,6 +45,7 @@ class GlobalToggleProperties { required this.textDirection, required this.mode, required this.loadingAnimationValue, + required this.active, }); } @@ -74,6 +77,7 @@ class DetailedGlobalToggleProperties extends GlobalToggleProperties { required super.textDirection, required super.mode, required super.loadingAnimationValue, + required super.active, }); } @@ -177,7 +181,7 @@ class SizeProperties extends AnimatedToggleProperties { }); } -class DifProperties { +class SeparatorProperties { /// Index of the separator. /// /// The separator is located between the items at [index] and [index+1]. @@ -186,7 +190,7 @@ class DifProperties { /// The position of the separator relative to the indices of the values. double get position => index + 0.5; - const DifProperties({ + const SeparatorProperties({ required this.index, }); } diff --git a/lib/src/style.dart b/lib/src/style.dart new file mode 100644 index 0000000..7b795bf --- /dev/null +++ b/lib/src/style.dart @@ -0,0 +1,148 @@ +part of 'package:animated_toggle_switch/animated_toggle_switch.dart'; + +class ToggleStyle { + /// Background color of the indicator. + final Color? indicatorColor; + + /// Background color of the switch. + final Color? backgroundColor; + + /// Gradient of the background. Overwrites [innerColor] if not [null]. + final Gradient? backgroundGradient; + + /// Border color of the switch. + /// + /// For deactivating please set this to [Colors.transparent]. + final Color? borderColor; + + /// [BorderRadius] of the switch. + final BorderRadiusGeometry? borderRadius; + + /// [BorderRadius] of the indicator. + /// + /// Defaults to [borderRadius]. + final BorderRadiusGeometry? indicatorBorderRadius; + + /// [BorderRadius] of the indicator. + final BoxBorder? indicatorBorder; + + /// Shadow for the indicator [Container]. + final List? indicatorBoxShadow; + + /// Shadow for the [Container] in the background. + final List? boxShadow; + + /// Default constructor for [ToggleStyle]. + const ToggleStyle({ + this.indicatorColor, + this.backgroundColor, + this.backgroundGradient, + this.borderColor, + this.borderRadius, + this.indicatorBorderRadius, + this.indicatorBorder, + this.indicatorBoxShadow, + this.boxShadow, + }); + + /// Private constructor for setting all possible parameters. + ToggleStyle._({ + required this.indicatorColor, + required this.backgroundColor, + required this.backgroundGradient, + required this.borderColor, + required this.borderRadius, + required this.indicatorBorderRadius, + required this.indicatorBorder, + required this.indicatorBoxShadow, + required this.boxShadow, + }); + + ToggleStyle _merge(ToggleStyle? other) => other == null + ? this + : ToggleStyle._( + indicatorColor: other.indicatorColor ?? indicatorColor, + backgroundColor: other.backgroundColor ?? backgroundColor, + backgroundGradient: other.backgroundGradient ?? + (other.backgroundColor == null ? null : backgroundGradient), + borderColor: other.borderColor ?? borderColor, + borderRadius: other.borderRadius ?? borderRadius, + indicatorBorderRadius: other.indicatorBorderRadius ?? + other.borderRadius ?? + indicatorBorderRadius ?? + borderRadius, + indicatorBorder: other.indicatorBorder ?? indicatorBorder, + indicatorBoxShadow: other.indicatorBoxShadow ?? indicatorBoxShadow, + boxShadow: other.boxShadow ?? boxShadow, + ); + + static ToggleStyle _lerp(ToggleStyle style1, ToggleStyle style2, double t) => + ToggleStyle._( + indicatorColor: + Color.lerp(style1.indicatorColor, style2.indicatorColor, t), + backgroundColor: + Color.lerp(style1.backgroundColor, style2.backgroundColor, t), + backgroundGradient: Gradient.lerp( + style1.backgroundGradient ?? style1.backgroundColor?.toGradient(), + style2.backgroundGradient ?? style2.backgroundColor?.toGradient(), + t, + ), + borderColor: Color.lerp(style1.borderColor, style2.borderColor, t), + borderRadius: BorderRadiusGeometry.lerp( + style1.borderRadius, + style2.borderRadius, + t, + ), + indicatorBorderRadius: BorderRadiusGeometry.lerp( + style1.indicatorBorderRadius ?? style1.borderRadius, + style2.indicatorBorderRadius ?? style2.borderRadius, + t, + ), + indicatorBorder: BoxBorder.lerp( + style1.indicatorBorder, + style2.indicatorBorder, + t, + ), + indicatorBoxShadow: BoxShadow.lerpList( + style1.indicatorBoxShadow, + style2.indicatorBoxShadow, + t, + ), + boxShadow: BoxShadow.lerpList( + style1.boxShadow, + style2.boxShadow, + t, + ), + ); + + // coverage:ignore-start + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ToggleStyle && + runtimeType == other.runtimeType && + indicatorColor == other.indicatorColor && + backgroundColor == other.backgroundColor && + backgroundGradient == other.backgroundGradient && + borderColor == other.borderColor && + borderRadius == other.borderRadius && + indicatorBorderRadius == other.indicatorBorderRadius && + indicatorBorder == other.indicatorBorder && + indicatorBoxShadow == other.indicatorBoxShadow && + boxShadow == other.boxShadow; + + @override + int get hashCode => + indicatorColor.hashCode ^ + backgroundColor.hashCode ^ + backgroundGradient.hashCode ^ + borderColor.hashCode ^ + borderRadius.hashCode ^ + indicatorBorderRadius.hashCode ^ + indicatorBorder.hashCode ^ + indicatorBoxShadow.hashCode ^ + boxShadow.hashCode; + + // coverage:ignore-end +} diff --git a/lib/src/widgets/animated_toggle_switch.dart b/lib/src/widgets/animated_toggle_switch.dart index 635db47..a19eb61 100644 --- a/lib/src/widgets/animated_toggle_switch.dart +++ b/lib/src/widgets/animated_toggle_switch.dart @@ -42,121 +42,7 @@ typedef CustomStyleBuilder = ToggleStyle Function( GlobalToggleProperties global, ); -class ToggleStyle { - /// Background color of the indicator. - final Color? indicatorColor; - - /// Background color of the switch. - final Color? backgroundColor; - - /// Gradient of the background. Overwrites [innerColor] if not [null]. - final Gradient? backgroundGradient; - - /// Border color of the switch. - /// - /// For deactivating please set this to [Colors.transparent]. - final Color? borderColor; - - /// [BorderRadius] of the switch. - final BorderRadiusGeometry? borderRadius; - - /// [BorderRadius] of the indicator. - /// - /// Defaults to [borderRadius]. - final BorderRadiusGeometry? indicatorBorderRadius; - - /// [BorderRadius] of the indicator. - final BoxBorder? indicatorBorder; - - /// Shadow for the indicator [Container]. - final List? indicatorBoxShadow; - - /// Shadow for the [Container] in the background. - final List? boxShadow; - - /// Default constructor for [ToggleStyle]. - const ToggleStyle({ - this.indicatorColor, - this.backgroundColor, - this.backgroundGradient, - this.borderColor, - this.borderRadius, - this.indicatorBorderRadius, - this.indicatorBorder, - this.indicatorBoxShadow, - this.boxShadow, - }); - - /// Private constructor for setting all possible parameters. - ToggleStyle._({ - required this.indicatorColor, - required this.backgroundColor, - required this.backgroundGradient, - required this.borderColor, - required this.borderRadius, - required this.indicatorBorderRadius, - required this.indicatorBorder, - required this.indicatorBoxShadow, - required this.boxShadow, - }); - - ToggleStyle _merge(ToggleStyle? other) => other == null - ? this - : ToggleStyle._( - indicatorColor: other.indicatorColor ?? indicatorColor, - backgroundColor: other.backgroundColor ?? backgroundColor, - backgroundGradient: other.backgroundGradient ?? - (other.backgroundColor == null ? null : backgroundGradient), - borderColor: other.borderColor ?? borderColor, - borderRadius: other.borderRadius ?? borderRadius, - indicatorBorderRadius: other.indicatorBorderRadius ?? - other.borderRadius ?? - indicatorBorderRadius ?? - borderRadius, - indicatorBorder: other.indicatorBorder ?? indicatorBorder, - indicatorBoxShadow: other.indicatorBoxShadow ?? indicatorBoxShadow, - boxShadow: other.boxShadow ?? boxShadow, - ); - - static ToggleStyle _lerp(ToggleStyle style1, ToggleStyle style2, double t) => - ToggleStyle._( - indicatorColor: - Color.lerp(style1.indicatorColor, style2.indicatorColor, t), - backgroundColor: - Color.lerp(style1.backgroundColor, style2.backgroundColor, t), - backgroundGradient: Gradient.lerp( - style1.backgroundGradient ?? style1.backgroundColor?.toGradient(), - style2.backgroundGradient ?? style2.backgroundColor?.toGradient(), - t, - ), - borderColor: Color.lerp(style1.borderColor, style2.borderColor, t), - borderRadius: BorderRadiusGeometry.lerp( - style1.borderRadius, - style2.borderRadius, - t, - ), - indicatorBorderRadius: BorderRadiusGeometry.lerp( - style1.indicatorBorderRadius ?? style1.borderRadius, - style2.indicatorBorderRadius ?? style2.borderRadius, - t, - ), - indicatorBorder: BoxBorder.lerp( - style1.indicatorBorder, - style2.indicatorBorder, - t, - ), - indicatorBoxShadow: BoxShadow.lerpList( - style1.indicatorBoxShadow, - style2.indicatorBoxShadow, - t, - ), - boxShadow: BoxShadow.lerpList( - style1.boxShadow, - style2.boxShadow, - t, - ), - ); -} +typedef SeparatorBuilder = Widget Function(int index); /// Specifies when an value should be animated. enum AnimationType { @@ -237,7 +123,7 @@ class AnimatedToggleSwitch extends StatelessWidget { final Size indicatorSize; /// Callback for selecting a new value. The new [current] should be set here. - final Function(T)? onChanged; + final ChangeCallback? onChanged; /// Width of the border of the switch. For deactivating please set this to [0.0]. final double borderWidth; @@ -277,24 +163,12 @@ class AnimatedToggleSwitch extends StatelessWidget { final AnimationType indicatorAnimationType; /// Callback for tapping anywhere on the widget. - final Function()? onTap; + final TapCallback? onTap; final IconArrangement _iconArrangement; - /// [MouseCursor] to show when not hovering an indicator. - /// - /// Defaults to [SystemMouseCursors.click] if [iconsTappable] is [true] - /// and to [MouseCursor.defer] otherwise. - final MouseCursor? defaultCursor; - - /// [MouseCursor] to show when grabbing the indicators. - final MouseCursor draggingCursor; - - /// [MouseCursor] to show when hovering the indicators. - final MouseCursor dragCursor; - - /// [MouseCursor] to show during loading. - final MouseCursor loadingCursor; + /// The [MouseCursor] settings for this switch. + final ToggleCursors cursors; /// The [FittingMode] of the switch. /// @@ -358,7 +232,7 @@ class AnimatedToggleSwitch extends StatelessWidget { /// The available width is specified by [dif]. /// /// This builder is supported by [IconArrangement.row] only. - final IndexedWidgetBuilder? separatorBuilder; + final SeparatorBuilder? separatorBuilder; /// Builder for divider or other separators between the icons. Consider using [separatorBuilder] for a simpler builder function. /// @@ -367,6 +241,28 @@ class AnimatedToggleSwitch extends StatelessWidget { /// This builder is supported by [IconArrangement.row] only. final CustomSeparatorBuilder? customSeparatorBuilder; + /// Indicates if the switch is active. + /// + /// Please use [inactiveOpacity] for changing the opacity in inactive state. + /// + /// For controlling the [AnimatedOpacity] you can use [inactiveOpacityCurve] and [inactiveOpacityDuration]. + final bool active; + + /// Opacity of the switch when [active] is set to [false]. + /// + /// Please set this to [1.0] for deactivating. + final double inactiveOpacity; + + /// [Curve] of the animation when getting inactive. + /// + /// For deactivating this animation please set [inactiveOpacity] to [1.0]. + final Curve inactiveOpacityCurve; + + /// [Duration] of the animation when getting inactive. + /// + /// For deactivating this animation please set [inactiveOpacity] to [1.0]. + final Duration inactiveOpacityDuration; + /// Constructor of AnimatedToggleSwitch with all possible settings. /// /// Consider using [CustomAnimatedToggleSwitch] for maximum customizability. @@ -379,7 +275,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, this.indicatorSize = const Size(48.0, double.infinity), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, @@ -398,10 +294,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -413,7 +306,11 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, - }) : this._iconArrangement = IconArrangement.row, + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), + }) : _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -430,12 +327,12 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, this.indicatorSize = const Size(48.0, double.infinity), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, - iconSize = const Size(23.0, 23.0), - selectedIconSize = const Size(34.5, 34.5), + Size iconSize = const Size(23.0, 23.0), + Size selectedIconSize = const Size(34.5, 34.5), this.iconAnimationCurve = Curves.easeOutBack, this.iconAnimationDuration, this.iconOpacity = 0.5, @@ -451,10 +348,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -466,9 +360,13 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), }) : animatedIconBuilder = _iconSizeBuilder( iconBuilder, customIconBuilder, iconSize, selectedIconSize), - this._iconArrangement = IconArrangement.row, + _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -487,16 +385,16 @@ class AnimatedToggleSwitch extends StatelessWidget { SimpleSizeIconBuilder? iconBuilder, SizeIconBuilder? customIconBuilder, this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, - iconSize = const Size(0.5, 0.5), - selectedIconSize = const Size(0.75, 0.75), + Size iconSize = const Size(0.5, 0.5), + Size selectedIconSize = const Size(0.75, 0.75), this.iconAnimationCurve = Curves.easeOutBack, this.iconAnimationDuration, this.iconOpacity = 0.5, - dif = 0.0, + double dif = 0.0, this.foregroundIndicatorIconBuilder, this.selectedIconOpacity = 1.0, this.iconAnimationType = AnimationType.onSelected, @@ -507,10 +405,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -522,14 +417,18 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, - }) : this.indicatorSize = indicatorSize * (height - 2 * borderWidth), - this.dif = dif * (height - 2 * borderWidth), + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), + }) : indicatorSize = indicatorSize * (height - 2 * borderWidth), + dif = dif * (height - 2 * borderWidth), animatedIconBuilder = _iconSizeBuilder( iconBuilder, customIconBuilder, iconSize * (height + 2 * borderWidth), selectedIconSize * (height + 2 * borderWidth)), - this._iconArrangement = IconArrangement.row, + _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -539,8 +438,9 @@ class AnimatedToggleSwitch extends StatelessWidget { Size iconSize, Size selectedIconSize) { assert(iconBuilder == null || customIconBuilder == null); - if (customIconBuilder == null && iconBuilder != null) + if (customIconBuilder == null && iconBuilder != null) { customIconBuilder = (c, l, g) => iconBuilder(l.value, l.iconSize); + } return customIconBuilder == null ? null : (context, local, global) => customIconBuilder!( @@ -574,14 +474,14 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, Size indicatorSize = const Size(1.0, 1.0), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, this.iconAnimationCurve = Curves.easeOutBack, this.iconAnimationDuration, this.iconOpacity = 0.5, - dif = 0.0, + double dif = 0.0, this.foregroundIndicatorIconBuilder, this.selectedIconOpacity = 1.0, this.iconAnimationType = AnimationType.onSelected, @@ -592,10 +492,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -607,9 +504,13 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, - }) : this.dif = dif * (height - 2 * borderWidth), - this.indicatorSize = indicatorSize * (height - 2 * borderWidth), - this._iconArrangement = IconArrangement.row, + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), + }) : dif = dif * (height - 2 * borderWidth), + indicatorSize = indicatorSize * (height - 2 * borderWidth), + _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -634,7 +535,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, Size indicatorSize = const Size(1.0, 1.0), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, @@ -649,10 +550,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -666,26 +564,29 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, - }) : this.iconAnimationCurve = Curves.linear, - this.dif = dif * (height - 2 * borderWidth), - this.iconAnimationDuration = Duration.zero, - this.indicatorSize = indicatorSize * (height - 2 * borderWidth), - this.selectedIconOpacity = iconOpacity, - this.iconAnimationType = AnimationType.onSelected, - this.foregroundIndicatorIconBuilder = - _rollingForegroundIndicatorIconBuilder( - values, - iconBuilder, - customIconBuilder, - Size.square( - selectedIconRadius * 2 * (height - 2 * borderWidth)), - transitionType), + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), + }) : iconAnimationCurve = Curves.linear, + dif = dif * (height - 2 * borderWidth), + iconAnimationDuration = Duration.zero, + indicatorSize = indicatorSize * (height - 2 * borderWidth), + selectedIconOpacity = iconOpacity, + iconAnimationType = AnimationType.onSelected, + foregroundIndicatorIconBuilder = _rollingForegroundIndicatorIconBuilder< + T>( + values, + iconBuilder, + customIconBuilder, + Size.square(selectedIconRadius * 2 * (height - 2 * borderWidth)), + transitionType), animatedIconBuilder = _standardIconBuilder( iconBuilder, customIconBuilder, Size.square(iconRadius * 2 * (height - 2 * borderWidth)), Size.square(iconRadius * 2 * (height - 2 * borderWidth))), - this._iconArrangement = IconArrangement.row, + _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -706,7 +607,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, this.indicatorSize = const Size(46.0, double.infinity), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, @@ -722,10 +623,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.minTouchTargetSize = 48.0, this.textDirection, this.iconsTappable = true, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loadingIconBuilder = _defaultLoadingIconBuilder, this.loading, this.loadingAnimationDuration, @@ -739,23 +637,27 @@ class AnimatedToggleSwitch extends StatelessWidget { this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.separatorBuilder, this.customSeparatorBuilder, - }) : this.iconAnimationCurve = Curves.linear, - this.iconAnimationDuration = Duration.zero, - this.selectedIconOpacity = iconOpacity, - this.iconAnimationType = AnimationType.onSelected, - this.foregroundIndicatorIconBuilder = + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), + }) : iconAnimationCurve = Curves.linear, + iconAnimationDuration = Duration.zero, + selectedIconOpacity = iconOpacity, + iconAnimationType = AnimationType.onSelected, + foregroundIndicatorIconBuilder = _rollingForegroundIndicatorIconBuilder( values, iconBuilder, customIconBuilder, Size.square(selectedIconRadius * 2), transitionType), - this.animatedIconBuilder = _standardIconBuilder( + animatedIconBuilder = _standardIconBuilder( iconBuilder, customIconBuilder, Size.square(iconRadius * 2), Size.square(iconRadius * 2)), - this._iconArrangement = IconArrangement.row, + _iconArrangement = IconArrangement.row, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); @@ -766,11 +668,12 @@ class AnimatedToggleSwitch extends StatelessWidget { Size iconSize, ForegroundIndicatorTransitionType transitionType) { assert(iconBuilder == null || customIconBuilder == null); - if (customIconBuilder == null && iconBuilder != null) + if (customIconBuilder == null && iconBuilder != null) { customIconBuilder = (c, l, g) => iconBuilder(l.value, l.iconSize, l.foreground); + } return (context, global) { - if (customIconBuilder == null) return SizedBox(); + if (customIconBuilder == null) return const SizedBox(); double distance = global.dif + global.indicatorSize.width; double angleDistance = transitionType == ForegroundIndicatorTransitionType.rolling @@ -824,9 +727,10 @@ class AnimatedToggleSwitch extends StatelessWidget { Size iconSize, Size selectedIconSize) { assert(iconBuilder == null || customIconBuilder == null); - if (customIconBuilder == null && iconBuilder != null) + if (customIconBuilder == null && iconBuilder != null) { customIconBuilder = (c, l, g) => iconBuilder(l.value, l.iconSize, l.foreground); + } return customIconBuilder == null ? null : (t, local, global) => customIconBuilder!( @@ -854,7 +758,7 @@ class AnimatedToggleSwitch extends StatelessWidget { this.animationCurve = Curves.easeInOutCirc, this.indicatorSize = const Size(46.0, double.infinity), this.onChanged, - this.borderWidth = 2, + this.borderWidth = 2.0, this.style = const ToggleStyle(), this.styleBuilder, this.customStyleBuilder, @@ -869,10 +773,7 @@ class AnimatedToggleSwitch extends StatelessWidget { Function()? onTap, this.minTouchTargetSize = 48.0, this.textDirection, - this.defaultCursor = SystemMouseCursors.click, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), EdgeInsetsGeometry textMargin = const EdgeInsets.symmetric(horizontal: 8.0), Offset animationOffset = const Offset(20.0, 0), bool clipAnimation = true, @@ -883,21 +784,24 @@ class AnimatedToggleSwitch extends StatelessWidget { this.loadingAnimationCurve, ForegroundIndicatorTransitionType transitionType = ForegroundIndicatorTransitionType.rolling, + this.active = true, + this.inactiveOpacity = 0.6, + this.inactiveOpacityCurve = Curves.easeInOut, + this.inactiveOpacityDuration = const Duration(milliseconds: 350), }) : assert(clipAnimation || opacityAnimation), - this.iconOpacity = 1.0, - this.selectedIconOpacity = 1.0, - this.values = [first, second], - this.iconAnimationType = AnimationType.onHover, - this.iconsTappable = false, - this.onTap = onTap ?? _dualOnTap(onChanged, [first, second], current), - this.foregroundIndicatorIconBuilder = - _rollingForegroundIndicatorIconBuilder( - [first, second], - iconBuilder == null ? null : (v, s, f) => iconBuilder(v), - customIconBuilder, - Size.square(iconRadius * 2), - transitionType), - this.animatedIconBuilder = _dualIconBuilder( + iconOpacity = 1.0, + selectedIconOpacity = 1.0, + values = [first, second], + iconAnimationType = AnimationType.onHover, + iconsTappable = false, + onTap = onTap ?? _dualOnTap(onChanged, [first, second], current), + foregroundIndicatorIconBuilder = _rollingForegroundIndicatorIconBuilder( + [first, second], + iconBuilder == null ? null : (v, s, f) => iconBuilder(v), + customIconBuilder, + Size.square(iconRadius * 2), + transitionType), + animatedIconBuilder = _dualIconBuilder( textBuilder, customTextBuilder, Size.square(iconRadius * 2), @@ -907,19 +811,19 @@ class AnimatedToggleSwitch extends StatelessWidget { clipAnimation, opacityAnimation, ), - this._iconArrangement = IconArrangement.overlap, - this.allowUnlistedValues = false, - this.indicatorAppearingBuilder = _defaultIndicatorAppearingBuilder, - this.indicatorAppearingDuration = + _iconArrangement = IconArrangement.overlap, + allowUnlistedValues = false, + indicatorAppearingBuilder = _defaultIndicatorAppearingBuilder, + indicatorAppearingDuration = _defaultIndicatorAppearingAnimationDuration, - this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, - this.separatorBuilder = null, - this.customSeparatorBuilder = null, + indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, + separatorBuilder = null, + customSeparatorBuilder = null, assert(styleBuilder == null || customStyleBuilder == null), super(key: key); static Function() _dualOnTap( - Function(T)? onChanged, List values, T? current) { + ChangeCallback? onChanged, List values, T? current) { return () => onChanged?.call(values.firstWhere((element) => element != current)); } @@ -979,8 +883,8 @@ class AnimatedToggleSwitch extends StatelessWidget { }; } - static Widget _defaultLoadingIconBuilder( - BuildContext context, dynamic properties) => + static Widget _defaultLoadingIconBuilder(BuildContext context, + DetailedGlobalToggleProperties properties) => const _MyLoading(); ToggleStyle? _styleBuilder(BuildContext context, @@ -1026,10 +930,7 @@ class AnimatedToggleSwitch extends StatelessWidget { indicatorSize: indicatorSize, iconArrangement: _iconArrangement, iconsTappable: iconsTappable, - defaultCursor: defaultCursor, - dragCursor: dragCursor, - draggingCursor: draggingCursor, - loadingCursor: loadingCursor, + cursors: cursors, minTouchTargetSize: minTouchTargetSize, textDirection: textDirection, loading: loading, @@ -1042,8 +943,7 @@ class AnimatedToggleSwitch extends StatelessWidget { separatorBuilder: customSeparatorBuilder ?? (separatorBuilder == null ? null - : (context, local, global) => - separatorBuilder!(context, local.index)), + : (context, local, global) => separatorBuilder!(local.index)), backgroundIndicatorBuilder: foregroundIndicatorIconBuilder != null ? null : (context, properties) => @@ -1055,42 +955,52 @@ class AnimatedToggleSwitch extends StatelessWidget { iconBuilder: (context, local, global) => _animatedOpacityIcon( _animatedSizeIcon(context, local, global), local.value == current), padding: EdgeInsets.all(borderWidth), + active: active, wrapperBuilder: (context, global, child) { - //TODO: extract this method to separate widget - return _animationTypeBuilder( - context, - styleAnimationType, - (local) => style._merge(_styleBuilder(context, local, global)), - ToggleStyle._lerp, - (style) => DecoratedBox( - decoration: BoxDecoration( - color: style.backgroundColor, - gradient: style.backgroundGradient, - borderRadius: style.borderRadius, - boxShadow: style.boxShadow, - ), - child: DecoratedBox( - position: DecorationPosition.foreground, + return _ConditionalWrapper( + condition: inactiveOpacity < 1.0, + wrapper: (context, child) => AnimatedOpacity( + opacity: global.active ? 1.0 : inactiveOpacity, + duration: inactiveOpacityDuration, + curve: inactiveOpacityCurve, + child: child, + ), + child: _animationTypeBuilder( + context, + styleAnimationType, + (local) => style._merge(_styleBuilder(context, local, global)), + ToggleStyle._lerp, + (style) => DecoratedBox( decoration: BoxDecoration( - border: borderWidth <= 0.0 - ? null - : Border.all( - color: style.borderColor!, - width: borderWidth, - ), + color: style.backgroundColor, + gradient: style.backgroundGradient, borderRadius: style.borderRadius, + boxShadow: style.boxShadow, ), - child: ClipRRect( - borderRadius: style.borderRadius, - child: child, + child: DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + border: borderWidth <= 0.0 + ? null + : Border.all( + color: style.borderColor!, + width: borderWidth, + ), + borderRadius: style.borderRadius, + ), + child: ClipRRect( + borderRadius: style.borderRadius, + child: child, + ), ), ), + global, ), - global, ); }); } + //TODO: extract this method to separate widget Widget _animationTypeBuilder( BuildContext context, AnimationType animationType, @@ -1169,10 +1079,11 @@ class AnimatedToggleSwitch extends StatelessWidget { double animationValue = 0.0; double localPosition = global.position - global.position.floorToDouble(); - if (values[global.position.floor()] == local.value) + if (values[global.position.floor()] == local.value) { animationValue = 1 - localPosition; - else if (values[global.position.ceil()] == local.value) + } else if (values[global.position.ceil()] == local.value) { animationValue = localPosition; + } return _animatedIcon( context, AnimatedToggleProperties._fromLocal( @@ -1211,12 +1122,12 @@ class AnimatedToggleSwitch extends StatelessWidget { children: [ if (loadingValue < 1.0) Opacity( - key: ValueKey(0), + key: const ValueKey(0), opacity: 1.0 - loadingValue, child: child), if (loadingValue > 0.0) Opacity( - key: ValueKey(1), + key: const ValueKey(1), opacity: loadingValue, child: loadingIconBuilder(context, global)), ], @@ -1284,7 +1195,7 @@ class _CustomTween extends Tween { _CustomTween(this.lerpFunction, {super.begin, super.end}); @override - V lerp(double t) => lerpFunction(begin!, end!, t); + V lerp(double t) => lerpFunction(begin as V, end as V, t); } extension _XTargetPlatform on TargetPlatform { diff --git a/lib/src/widgets/conditional_wrapper.dart b/lib/src/widgets/conditional_wrapper.dart new file mode 100644 index 0000000..0c38c2b --- /dev/null +++ b/lib/src/widgets/conditional_wrapper.dart @@ -0,0 +1,38 @@ +part of 'package:animated_toggle_switch/animated_toggle_switch.dart'; + +class _ConditionalWrapper extends StatefulWidget { + final Widget Function(BuildContext context, Widget child) wrapper; + final bool condition; + final Widget child; + + const _ConditionalWrapper({ + required this.wrapper, + required this.condition, + required this.child, + }); + + @override + State<_ConditionalWrapper> createState() => _ConditionalWrapperState(); +} + +class _ConditionalWrapperState extends State<_ConditionalWrapper> { + final _childKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final child = _EmptyWidget(key: _childKey, child: widget.child); + if (widget.condition) return widget.wrapper(context, child); + return child; + } +} + +class _EmptyWidget extends StatelessWidget { + final Widget child; + + const _EmptyWidget({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return child; + } +} diff --git a/lib/src/widgets/custom_animated_toggle_switch.dart b/lib/src/widgets/custom_animated_toggle_switch.dart index d5aa09b..0e298a3 100644 --- a/lib/src/widgets/custom_animated_toggle_switch.dart +++ b/lib/src/widgets/custom_animated_toggle_switch.dart @@ -14,7 +14,7 @@ typedef CustomWrapperBuilder = Widget Function( /// Custom builder for the dif section between the icons. typedef CustomSeparatorBuilder = Widget Function(BuildContext context, - DifProperties local, DetailedGlobalToggleProperties global); + SeparatorProperties local, DetailedGlobalToggleProperties global); /// Custom builder for the appearing animation of the indicator. /// @@ -24,6 +24,10 @@ typedef CustomSeparatorBuilder = Widget Function(BuildContext context, typedef IndicatorAppearingBuilder = Widget Function( BuildContext context, double value, Widget indicator); +typedef ChangeCallback = FutureOr Function(T value); + +typedef TapCallback = FutureOr Function(); + enum ToggleMode { animating, dragged, none } enum FittingMode { none, preventHorizontalOverlapping } @@ -109,11 +113,12 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { final Size indicatorSize; /// Callback for selecting a new value. The new [current] should be set here. - final Function(T)? onChanged; + final ChangeCallback? onChanged; /// Space between the "indicator rooms" of the adjacent icons. final double dif; + /// Builder for divider or other separators between the icons. /// Builder for divider or other separators between the icons. /// /// The available width is specified by [dif]. @@ -122,7 +127,7 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { final CustomSeparatorBuilder? separatorBuilder; /// Callback for tapping anywhere on the widget. - final Function()? onTap; + final TapCallback? onTap; /// Indicates if [onChanged] is called when an icon is tapped. /// If [false] the user can change the value only by dragging the indicator. @@ -160,20 +165,8 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { /// If set to [null], the [TextDirection] is taken from the [BuildContext]. final TextDirection? textDirection; - /// [MouseCursor] to show when not hovering an indicator. - /// - /// Defaults to [SystemMouseCursors.click] if [iconsTappable] is [true] - /// and to [MouseCursor.defer] otherwise. - final MouseCursor? defaultCursor; - - /// [MouseCursor] to show when grabbing the indicators. - final MouseCursor draggingCursor; - - /// [MouseCursor] to show when hovering the indicators. - final MouseCursor dragCursor; - - /// [MouseCursor] to show during loading. - final MouseCursor loadingCursor; + /// The [MouseCursor] settings for this switch. + final ToggleCursors cursors; /// Indicates if the switch is currently loading. /// @@ -188,7 +181,7 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { final bool allowUnlistedValues; /// Indicates if the switch is active. - final bool active = true; + final bool active; const CustomAnimatedToggleSwitch({ Key? key, @@ -215,10 +208,7 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { this.dragStartDuration = const Duration(milliseconds: 200), this.dragStartCurve = Curves.easeInOutCirc, this.textDirection, - this.defaultCursor, - this.draggingCursor = SystemMouseCursors.grabbing, - this.dragCursor = SystemMouseCursors.grab, - this.loadingCursor = MouseCursor.defer, + this.cursors = const ToggleCursors(), this.loading, this.loadingAnimationDuration, this.loadingAnimationCurve, @@ -226,6 +216,7 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { _defaultIndicatorAppearingAnimationDuration, this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve, this.allowUnlistedValues = false, + this.active = true, }) : assert(foregroundIndicatorBuilder != null || backgroundIndicatorBuilder != null), assert(separatorBuilder == null || @@ -233,7 +224,7 @@ class CustomAnimatedToggleSwitch extends StatefulWidget { super(key: key); @override - _CustomAnimatedToggleSwitchState createState() => + State> createState() => _CustomAnimatedToggleSwitchState(); bool get _isCurrentUnlisted => !values.contains(current); @@ -344,7 +335,7 @@ class _CustomAnimatedToggleSwitchState void _onChanged(T value) { if (!_isActive) return; - var result = widget.onChanged?.call(value); + final result = widget.onChanged?.call(value); if (result is Future && widget.loading == null) { _loading(true); result.whenComplete(() => _loading(false)); @@ -353,7 +344,7 @@ class _CustomAnimatedToggleSwitchState void _onTap() { if (!_isActive) return; - var result = widget.onTap?.call(); + final result = widget.onTap?.call(); if (result is Future && widget.loading == null) { _loading(true); result.whenComplete(() => _loading(false)); @@ -385,27 +376,29 @@ class _CustomAnimatedToggleSwitchState /// Returns the value position by the local position of the cursor. /// It is mainly intended as a helper function for the build method. double _doubleFromPosition( - double x, DetailedGlobalToggleProperties properties) { + double x, DetailedGlobalToggleProperties properties) { double result = (x.clamp( properties.indicatorSize.width / 2, properties.switchSize.width - properties.indicatorSize.width / 2) - properties.indicatorSize.width / 2) / (properties.indicatorSize.width + properties.dif); - if (properties.textDirection == TextDirection.rtl) + if (properties.textDirection == TextDirection.rtl) { result = widget.values.length - 1 - result; + } return result; } /// Returns the value index by the local position of the cursor. /// It is mainly intended as a helper function for the build method. - int _indexFromPosition(double x, DetailedGlobalToggleProperties properties) { + int _indexFromPosition( + double x, DetailedGlobalToggleProperties properties) { return _doubleFromPosition(x, properties).round(); } /// Returns the value by the local position of the cursor. /// It is mainly intended as a helper function for the build method. - T _valueFromPosition(double x, DetailedGlobalToggleProperties properties) { + T _valueFromPosition(double x, DetailedGlobalToggleProperties properties) { return widget.values[_indexFromPosition(x, properties)]; } @@ -438,6 +431,7 @@ class _CustomAnimatedToggleSwitchState textDirection: textDirection, mode: _animationInfo.toggleMode, loadingAnimationValue: loadingValue, + active: widget.active, ); Widget child = Padding( padding: widget.padding, @@ -541,6 +535,7 @@ class _CustomAnimatedToggleSwitchState textDirection: textDirection, mode: _animationInfo.toggleMode, loadingAnimationValue: loadingValue, + active: widget.active, ); List stack = [ @@ -583,15 +578,17 @@ class _CustomAnimatedToggleSwitchState // manual check if cursor is above indicator // to make sure that GestureDetector and MouseRegion match. // TODO: one widget for DragRegion and GestureDetector to avoid redundancy - child: DragRegion( + child: _DragRegion( dragging: _animationInfo.toggleMode == ToggleMode.dragged, - draggingCursor: widget.draggingCursor, - dragCursor: widget.dragCursor, + draggingCursor: widget.cursors.draggingCursor, + dragCursor: widget.cursors.dragCursor, hoverCheck: isHoveringIndicator, - defaultCursor: _animationInfo.loading - ? widget.loadingCursor - : (widget.defaultCursor ?? + defaultCursor: !_isActive + ? (_animationInfo.loading + ? widget.cursors.loadingCursor + : widget.cursors.inactiveCursor) + : (widget.cursors.defaultCursor ?? (widget.iconsTappable ? SystemMouseCursors.click : MouseCursor.defer)), @@ -606,8 +603,9 @@ class _CustomAnimatedToggleSwitchState _onChanged(newValue); }, onHorizontalDragStart: (details) { - if (!isHoveringIndicator(details.localPosition)) + if (!isHoveringIndicator(details.localPosition)) { return; + } _onDragged( _doubleFromPosition( details.localPosition.dx, properties), @@ -686,7 +684,7 @@ class _CustomAnimatedToggleSwitchState width: properties.dif, child: Center( child: widget.separatorBuilder!( - context, DifProperties(index: i), properties), + context, SeparatorProperties(index: i), properties), ), ), ] diff --git a/lib/src/widgets/drag_region.dart b/lib/src/widgets/drag_region.dart index 939e93e..3ef982c 100644 --- a/lib/src/widgets/drag_region.dart +++ b/lib/src/widgets/drag_region.dart @@ -62,7 +62,7 @@ class _HoverRegionState extends State<_HoverRegion> { } } -class DragRegion extends StatelessWidget { +class _DragRegion extends StatelessWidget { final bool dragging; final Widget child; final bool Function(Offset offset) hoverCheck; @@ -70,7 +70,7 @@ class DragRegion extends StatelessWidget { final MouseCursor dragCursor; final MouseCursor draggingCursor; - const DragRegion({ + const _DragRegion({ Key? key, this.dragging = false, required this.child, @@ -85,9 +85,9 @@ class DragRegion extends StatelessWidget { return _HoverRegion( cursor: dragging ? draggingCursor : null, hoverCursor: dragCursor, - child: child, hoverCheck: hoverCheck, defaultCursor: defaultCursor, + child: child, ); } } diff --git a/pubspec.lock b/pubspec.lock index 98112e1..798dcf2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -110,6 +110,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + url: "https://pub.dev" + source: hosted + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -163,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" logging: dependency: transitive description: @@ -388,10 +404,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b8c67f5fa3897b122cf60fe9ff314f7b0ef71eab25c5f8b771480bc338f48823 + sha256: ada49637c27973c183dad90beb6bd781eea4c9f5f955d35da172de0af7bd3440 url: "https://pub.dev" source: hosted - version: "11.7.2" + version: "11.8.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6626207..35a3c6b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: animated_toggle_switch description: Fully customizable, draggable and animated switch with multiple choices and smooth loading animation. It has prebuilt constructors for rolling and size animations. -version: 0.8.0-beta.0 +version: 0.8.0-beta.1 repository: https://github.com/SplashByte/animated_toggle_switch issue_tracker: https://github.com/SplashByte/animated_toggle_switch/issues @@ -13,6 +13,7 @@ dependencies: sdk: flutter dev_dependencies: + flutter_lints: ^2.0.2 flutter_test: sdk: flutter mocktail: ^0.3.0 @@ -24,5 +25,5 @@ funding: - https://ko-fi.com/splashbyte screenshots: - - description: 'This image shows three example usages of AnimatedToggleSwitch.rolling().' + - description: 'This image shows three example usages of AnimatedToggleSwitch.' path: screenshots/preview.webp diff --git a/test/gesture_test.dart b/test/gesture_test.dart index 043c7a5..5e70886 100644 --- a/test/gesture_test.dart +++ b/test/gesture_test.dart @@ -1,4 +1,5 @@ import 'package:animated_toggle_switch/animated_toggle_switch.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -11,7 +12,7 @@ void main() { final current = values.first; final next = values.last; final tapFunction = MockFunction(); - final changedFunction = MockOnChangedFunction(); + final changedFunction = MockOnChangedFunction(); await tester.pumpWidget(TestWrapper( child: buildSwitch( @@ -25,6 +26,8 @@ void main() { final currentFinder = find.byKey(iconKey(current)); final nextFinder = find.byKey(iconKey(next)); + debugDumpApp(); + await tester.tap(currentFinder, warnIfMissed: false); verify(() => tapFunction()).called(1); @@ -40,7 +43,7 @@ void main() { final values = defaultValues.sublist(0, 2); final current = values.first; final next = values.last; - final changedFunction = MockOnChangedFunction(); + final changedFunction = MockOnChangedFunction(); await tester.pumpWidget(TestWrapper( child: AnimatedToggleSwitch.dual( @@ -65,7 +68,7 @@ void main() { final current = values.first; final next = values.last; final tapFunction = MockFunction(); - final changedFunction = MockOnChangedFunction(); + final changedFunction = MockOnChangedFunction(); await tester.pumpWidget(TestWrapper( child: buildSwitch( @@ -73,6 +76,8 @@ void main() { iconBuilder: iconBuilder, onTap: tapFunction, onChanged: changedFunction, + // Necessary for AnimatedToggleSwitch.dual + dif: 5.0, ), )); final currentFinder = find.byKey(iconKey(current)); @@ -96,7 +101,7 @@ void main() { final current = values.first; final next = values.last; final tapFunction = MockFunction(); - final changedFunction = MockOnChangedFunction(); + final changedFunction = MockOnChangedFunction(); await tester.pumpWidget(TestWrapper( child: buildSwitch( diff --git a/test/helper.dart b/test/helper.dart index 1699ac0..bc6637d 100644 --- a/test/helper.dart +++ b/test/helper.dart @@ -20,7 +20,10 @@ class TestWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - return Directionality(textDirection: textDirection, child: child); + return Directionality( + textDirection: textDirection, + child: Center(child: child), + ); } } @@ -34,12 +37,19 @@ void checkValidSwitchIconBuilderState(T current, List values) { } } -Widget iconBuilder(T value, bool foreground) => - Text(key: iconKey(value, foreground: foreground), '$value'); +Widget iconBuilder(T value, bool foreground) => SizedBox.expand( + key: iconKey(value, foreground: foreground), + child: const ColoredBox(color: Colors.black), + ); Key iconKey(T value, {bool foreground = false}) => IconKey(value, foreground: foreground); +Widget separatorBuilder(int index) => + SizedBox.expand(key: separatorKey(index)); + +Key separatorKey(int index) => SeparatorKey(index); + final loadingIconKey = GlobalKey(); Widget _loadingIconBuilder( @@ -48,33 +58,37 @@ Widget _loadingIconBuilder( typedef TestIconBuilder = Widget Function(T value, bool foreground); -typedef SwitchBuilder = AnimatedToggleSwitch Function({ +typedef SwitchBuilder = AnimatedToggleSwitch Function({ required T current, required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }); -typedef SimpleSwitchBuilder = AnimatedToggleSwitch Function({ +typedef SimpleSwitchBuilder = AnimatedToggleSwitch Function({ required T current, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }); /// Tests all AnimatedToggleSwitch constructors @@ -92,14 +106,16 @@ void defaultTestAllSwitches( required int current, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(int)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => buildSwitch( current: current, @@ -114,6 +130,8 @@ void defaultTestAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable, + dif: dif, + separatorBuilder: separatorBuilder, ), defaultValues, )); @@ -127,14 +145,16 @@ void defaultTestAllSwitches( required int current, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(int)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.dual( current: current, @@ -154,6 +174,7 @@ void defaultTestAllSwitches( style: style ?? const ToggleStyle(), styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, + dif: dif ?? 40, ), values, ), @@ -175,14 +196,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.rolling( current: current, @@ -201,6 +224,8 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: dif ?? 0.0, + separatorBuilder: separatorBuilder, ))); testWidgets( '$description (AnimatedToggleSwitch.size)', @@ -211,14 +236,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.size( current: current, @@ -236,6 +263,8 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: dif ?? 0.0, + separatorBuilder: separatorBuilder, ))); testWidgets( '$description (AnimatedToggleSwitch.rollingByHeight)', @@ -246,14 +275,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.rollingByHeight( current: current, @@ -272,6 +303,8 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: _convertToByHeightValue(dif ?? 0.0, 50.0, 2.0), + separatorBuilder: separatorBuilder, ))); testWidgets( '$description (AnimatedToggleSwitch.sizeByHeight)', @@ -282,14 +315,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.sizeByHeight( current: current, @@ -307,6 +342,8 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: _convertToByHeightValue(dif ?? 0.0, 50.0, 2.0), + separatorBuilder: separatorBuilder, ))); testWidgets( '$description (AnimatedToggleSwitch.custom)', @@ -317,14 +354,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.custom( current: current, @@ -343,6 +382,8 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: dif ?? 0.0, + separatorBuilder: separatorBuilder, ))); testWidgets( '$description (AnimatedToggleSwitch.customByHeight)', @@ -353,14 +394,16 @@ void testAllSwitches( required List values, TestIconBuilder? iconBuilder, TextDirection? textDirection, - Function(T)? onChanged, - Function()? onTap, + ChangeCallback? onChanged, + TapCallback? onTap, bool? loading, bool allowUnlistedValues = false, ToggleStyle? style, StyleBuilder? styleBuilder, CustomStyleBuilder? customStyleBuilder, bool? iconsTappable, + double? dif, + SeparatorBuilder? separatorBuilder, }) => AnimatedToggleSwitch.customByHeight( current: current, @@ -379,5 +422,11 @@ void testAllSwitches( styleBuilder: styleBuilder, customStyleBuilder: customStyleBuilder, iconsTappable: iconsTappable ?? true, + dif: _convertToByHeightValue(dif ?? 0.0, 50.0, 2.0), + separatorBuilder: separatorBuilder, ))); } + +double _convertToByHeightValue( + double value, double height, double borderWidth) => + value / (height - 2 * borderWidth); diff --git a/test/keys.dart b/test/keys.dart index 1dbcccf..cebecf3 100644 --- a/test/keys.dart +++ b/test/keys.dart @@ -5,7 +5,7 @@ class IconKey extends LocalKey { final bool foreground; final T value; - IconKey(this.value, {this.foreground = false}); + const IconKey(this.value, {this.foreground = false}); @override bool operator ==(Object other) => @@ -17,3 +17,16 @@ class IconKey extends LocalKey { @override int get hashCode => foreground.hashCode ^ value.hashCode; } + +class SeparatorKey extends LocalKey { + final int index; + + const SeparatorKey(this.index); + + @override + bool operator ==(Object other) => + identical(this, other) || other is SeparatorKey && index == other.index; + + @override + int get hashCode => index.hashCode; +} diff --git a/test/loading_test.dart b/test/loading_test.dart index 786bd7b..1e654c5 100644 --- a/test/loading_test.dart +++ b/test/loading_test.dart @@ -14,8 +14,8 @@ void main() { child: buildSwitch( current: current, iconBuilder: iconBuilder, - onTap: () => Future.delayed(loadingDuration), - onChanged: (_) => Future.delayed(loadingDuration), + onTap: () => Future.delayed(loadingDuration), + onChanged: (_) => Future.delayed(loadingDuration), ), )); final currentFinder = find.byKey(iconKey(current)); @@ -104,8 +104,8 @@ void main() { child: buildSwitch( current: current, iconBuilder: iconBuilder, - onTap: () => Future.delayed(loadingDuration), - onChanged: (_) => Future.delayed(loadingDuration), + onTap: () => Future.delayed(loadingDuration), + onChanged: (_) => Future.delayed(loadingDuration), loading: false, ), )); diff --git a/test/separator_test.dart b/test/separator_test.dart new file mode 100644 index 0000000..8ea5108 --- /dev/null +++ b/test/separator_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'helper.dart'; + +void _checkValidSeparatorPositions( + WidgetTester tester, List values, double dif) { + for (int i = 0; i < values.length - 1; i++) { + final separatorFinder = find.byKey(separatorKey(i)); + final firstIconFinder = find.byKey(iconKey(values[i])); + final secondIconFinder = find.byKey(iconKey(values[i + 1])); + expect(separatorFinder, findsOneWidget); + expect( + tester.getBottomRight(firstIconFinder) - + tester.getBottomLeft(separatorFinder), + Offset.zero, + reason: 'separator is located right of the predecessor icon', + ); + expect( + tester.getBottomLeft(secondIconFinder) - + tester.getBottomRight(separatorFinder), + Offset.zero, + reason: 'separator is located left of the successor icon', + ); + expect( + tester.getBottomRight(separatorFinder).dx - + tester.getBottomLeft(separatorFinder).dx, + dif, + reason: 'separator has correct width', + ); + } +} + +void main() { + defaultTestAllSwitches('Switch builds separator at correct position', + (tester, buildSwitch, values) async { + const difs = [5.0, 10.0, 17.0]; + final current = values[1]; + for (final dif in difs) { + await tester.pumpWidget( + TestWrapper( + child: buildSwitch( + current: current, + dif: dif, + iconBuilder: iconBuilder, + separatorBuilder: separatorBuilder, + ), + ), + ); + _checkValidSeparatorPositions(tester, values, dif); + } + }, testDual: false); + defaultTestAllSwitches('separatorBuilder does not support dif = 0.0', + (tester, buildSwitch, values) async { + final current = values.first; + await tester.pumpWidget( + TestWrapper( + child: buildSwitch( + current: current, + dif: 0.0, + iconBuilder: iconBuilder, + separatorBuilder: separatorBuilder, + ), + ), + ); + expect(tester.takeException(), isAssertionError); + }, testDual: false); +}