Repository: splashbyte/animated_toggle_switch

Issue:
Option to execute custom code after completion of animation
All info on the desired functionality can be found here: [https://github.com/splashbyte/animated_toggle_switch/discussions/49](url)

In short: OnChange gives an option to execute code immediately after a change. It would be great if there was an option that gets triggerd after completion of the animation.



Fix patch:
diff --git a/lib/src/properties.dart b/lib/src/properties.dart
index b77298f..59c042f 100644
--- a/lib/src/properties.dart
+++ b/lib/src/properties.dart
@@ -226,8 +226,8 @@ class TapProperties<T> {
   });
 }
 
-class TapInfo<T> {
-  /// The value that the user has tapped.
+class TogglePosition<T> {
+  /// The value next to [position].
   final T value;
 
   /// The index of [value] in [values].
@@ -235,16 +235,77 @@ class TapInfo<T> {
   /// [index == position.round()] should always be [true].
   final int index;
 
-  /// The tapped position relative to the indices of the values.
+  /// The position relative to the indices of the values.
   ///
   /// [position] can be in the interval from [-0.5] to [values.length - 0.5].
   ///
   /// [position.round() == index] should always be [true].
   final double position;
 
-  TapInfo({
+  TogglePosition({
     required this.value,
     required this.index,
     required this.position,
   });
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is PositionListenerInfo &&
+          runtimeType == other.runtimeType &&
+          value == other.mode &&
+          index == other.index &&
+          position == other.position;
+
+  @override
+  int get hashCode => value.hashCode ^ index.hashCode ^ position.hashCode;
+}
+
+class TapInfo<T> extends TogglePosition<T> {
+  TapInfo({
+    required super.value,
+    required super.index,
+    required super.position,
+  });
+
+  TapInfo._fromPosition(TogglePosition<T> position)
+      : this(
+          value: position.value,
+          index: position.index,
+          position: position.position,
+        );
+}
+
+class PositionListenerInfo<T> extends TogglePosition<T> {
+  final ToggleMode mode;
+
+  PositionListenerInfo({
+    required super.value,
+    required super.index,
+    required super.position,
+    required this.mode,
+  });
+
+  PositionListenerInfo._fromPosition(
+      TogglePosition<T> position, ToggleMode mode)
+      : this(
+          value: position.value,
+          index: position.index,
+          position: position.position,
+          mode: mode,
+        );
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is PositionListenerInfo &&
+          runtimeType == other.runtimeType &&
+          value == other.value &&
+          index == other.index &&
+          position == other.position &&
+          mode == other.mode;
+
+  @override
+  int get hashCode =>
+      value.hashCode ^ index.hashCode ^ position.hashCode ^ mode.hashCode;
 }
diff --git a/lib/src/widgets/animated_toggle_switch.dart b/lib/src/widgets/animated_toggle_switch.dart
index 461258f..fd4b9f6 100644
--- a/lib/src/widgets/animated_toggle_switch.dart
+++ b/lib/src/widgets/animated_toggle_switch.dart
@@ -275,6 +275,9 @@ class AnimatedToggleSwitch<T extends Object?>
   /// For deactivating this animation please set [inactiveOpacity] to [1.0].
   final Duration inactiveOpacityDuration;
 
+  /// Listener for the current position and [ToggleMode] of the indicator.
+  final PositionListener<T>? positionListener;
+
   final bool animateStyleChanges = true;
 
   /// Constructor of AnimatedToggleSwitch with all possible settings.
@@ -327,6 +330,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
   })  : _iconArrangement = IconArrangement.row,
         super(
           values: values,
@@ -389,6 +393,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
   })  : spacing = spacing * (height - 2 * borderWidth),
         indicatorSize = indicatorSize * (height - 2 * borderWidth),
         _iconArrangement = IconArrangement.row,
@@ -453,6 +458,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
   })  : animatedIconBuilder = _iconSizeBuilder<T>(
             iconBuilder, customIconBuilder, iconList, selectedIconScale),
         _iconArrangement = IconArrangement.row,
@@ -520,6 +526,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
   })  : indicatorSize = indicatorSize * (height - 2 * borderWidth),
         spacing = spacing * (height - 2 * borderWidth),
         animatedIconBuilder = _iconSizeBuilder<T>(
@@ -614,6 +621,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
     double indicatorIconScale = 1.0,
   })  : iconAnimationCurve = Curves.linear,
         iconAnimationDuration = Duration.zero,
@@ -695,6 +703,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
     double indicatorIconScale = 1.0,
   })  : iconAnimationCurve = Curves.linear,
         spacing = spacing * (height - 2 * borderWidth),
@@ -892,6 +901,7 @@ class AnimatedToggleSwitch<T extends Object?>
     this.inactiveOpacity = 0.6,
     this.inactiveOpacityCurve = Curves.easeInOut,
     this.inactiveOpacityDuration = const Duration(milliseconds: 350),
+    this.positionListener,
   })  : assert(clipAnimation || opacityAnimation),
         iconOpacity = 1.0,
         selectedIconOpacity = 1.0,
@@ -1062,6 +1072,7 @@ class AnimatedToggleSwitch<T extends Object?>
         indicatorAppearingBuilder: indicatorAppearingBuilder,
         indicatorAppearingDuration: indicatorAppearingDuration,
         indicatorAppearingCurve: indicatorAppearingCurve,
+        positionListener: positionListener,
         separatorBuilder: customSeparatorBuilder ??
             (separatorBuilder == null
                 ? null
diff --git a/lib/src/widgets/custom_animated_toggle_switch.dart b/lib/src/widgets/custom_animated_toggle_switch.dart
index a2e02d6..7592a91 100644
--- a/lib/src/widgets/custom_animated_toggle_switch.dart
+++ b/lib/src/widgets/custom_animated_toggle_switch.dart
@@ -28,6 +28,9 @@ typedef ChangeCallback<T> = FutureOr<void> Function(T value);
 
 typedef TapCallback<T> = FutureOr<void> Function(TapProperties<T> props);
 
+/// Listener for the current position and [ToggleMode] of the indicator.
+typedef PositionListener<T> = void Function(PositionListenerInfo<T> position);
+
 enum ToggleMode { animating, dragged, none }
 
 enum FittingMode { none, preventHorizontalOverlapping }
@@ -187,6 +190,9 @@ class CustomAnimatedToggleSwitch<T extends Object?> extends StatefulWidget {
   /// Indicates if the switch is active.
   final bool active;
 
+  /// Listener for the current position and [ToggleMode] of the indicator.
+  final PositionListener<T>? positionListener;
+
   const CustomAnimatedToggleSwitch({
     Key? key,
     required this.current,
@@ -221,6 +227,7 @@ class CustomAnimatedToggleSwitch<T extends Object?> extends StatefulWidget {
     this.indicatorAppearingCurve = _defaultIndicatorAppearingAnimationCurve,
     this.allowUnlistedValues = false,
     this.active = true,
+    this.positionListener,
   })  : assert(foregroundIndicatorBuilder != null ||
             backgroundIndicatorBuilder != null),
         assert(separatorBuilder == null ||
@@ -256,6 +263,12 @@ class _CustomAnimatedToggleSwitchState<T>
 
   bool get _isCurrentUnlisted => _currentIndex < 0;
 
+  double get _positionValue => _animationInfo
+      .valueAt(_animation.value)
+      .clamp(0, widget.values.length - 1);
+
+  PositionListenerInfo<T>? _lastPositionListenerValue;
+
   @override
   void initState() {
     super.initState();
@@ -272,12 +285,13 @@ class _CustomAnimatedToggleSwitchState<T>
           ..addStatusListener((status) {
             if (status == AnimationStatus.completed &&
                 _animationInfo.toggleMode != ToggleMode.dragged) {
-              _animationInfo = _animationInfo.ended();
+              _setAnimationInfo(_animationInfo.ended());
             }
           });
 
     _animation =
-        CurvedAnimation(parent: _controller, curve: widget.animationCurve);
+        CurvedAnimation(parent: _controller, curve: widget.animationCurve)
+          ..addListener(_callPositionListener);
 
     _appearingController = AnimationController(
       vsync: this,
@@ -371,7 +385,7 @@ class _CustomAnimatedToggleSwitchState<T>
   void _loading(bool b) {
     if (b == _animationInfo.loading) return;
     _cancelDrag();
-    setState(() => _animationInfo = _animationInfo.setLoading(b));
+    _setAnimationInfo(_animationInfo.setLoading(b), setState: true);
   }
 
   /// Checks if the current value has a different position than the indicator
@@ -405,19 +419,41 @@ class _CustomAnimatedToggleSwitchState<T>
     return result;
   }
 
-  /// Returns the [TapInfo] by the local position of the cursor.
+  void _setAnimationInfo(_AnimationInfo info, {bool setState = false}) {
+    if (_animationInfo == info) return;
+    _animationInfo = info;
+    if (setState) this.setState(() {});
+    _callPositionListener();
+  }
+
+  void _callPositionListener() {
+    if (widget.positionListener == null) return;
+    final value = PositionListenerInfo._fromPosition(
+        _togglePositionFromPositionValue(_positionValue),
+        _animationInfo.toggleMode);
+    if (_lastPositionListenerValue == value) return;
+    _lastPositionListenerValue = value;
+    widget.positionListener?.call(value);
+  }
+
+  /// Returns the [TogglePosition] by the position value.
   /// It is mainly intended as a helper function for the build method.
-  TapInfo<T> _tapInfoFromPosition(
-      double x, DetailedGlobalToggleProperties<T> properties) {
-    final position = _doubleFromPosition(x, properties);
+  TogglePosition<T> _togglePositionFromPositionValue(double position) {
     final index = position.round();
-    return TapInfo(
+    return TogglePosition(
       value: widget.values[index],
       index: index,
       position: position,
     );
   }
 
+  /// Returns the [TogglePosition] by the local position of the cursor.
+  /// It is mainly intended as a helper function for the build method.
+  TogglePosition<T> _togglePositionFromRealPosition(
+      double x, DetailedGlobalToggleProperties<T> properties) {
+    return _togglePositionFromPositionValue(_doubleFromPosition(x, properties));
+  }
+
   @override
   Widget build(BuildContext context) {
     double spacing = widget.spacing;
@@ -454,9 +490,7 @@ class _CustomAnimatedToggleSwitchState<T>
             builder: (context, loadingValue, child) => AnimatedBuilder(
                 animation: _animation,
                 builder: (context, child) {
-                  double positionValue = _animationInfo
-                      .valueAt(_animation.value)
-                      .clamp(0, widget.values.length - 1);
+                  double positionValue = _positionValue;
                   GlobalToggleProperties<T> properties = GlobalToggleProperties(
                     position: positionValue,
                     current: widget.current,
@@ -654,17 +688,21 @@ class _CustomAnimatedToggleSwitchState<T>
                                     behavior: HitTestBehavior.translucent,
                                     dragStartBehavior: DragStartBehavior.down,
                                     onTapUp: (details) {
-                                      final tapInfo = _tapInfoFromPosition(
-                                          details.localPosition.dx, properties);
+                                      final togglePosition =
+                                          _togglePositionFromRealPosition(
+                                              details.localPosition.dx,
+                                              properties);
                                       _onTap(TapProperties(
-                                        tapped: tapInfo,
+                                        tapped: TapInfo._fromPosition(
+                                            togglePosition),
                                         values: widget.values,
                                       ));
                                       if (!widget.iconsTappable) return;
-                                      if (tapInfo.value == widget.current) {
+                                      if (togglePosition.value ==
+                                          widget.current) {
                                         return;
                                       }
-                                      _onChanged(tapInfo.value);
+                                      _onChanged(togglePosition.value);
                                     },
                                     onHorizontalDragStart: (details) {
                                       if (!isHoveringIndicator(
@@ -782,14 +820,14 @@ class _CustomAnimatedToggleSwitchState<T>
     if (_animationInfo.toggleMode == ToggleMode.dragged) return;
     if (_appearingController.value > 0.0) {
       if (index.toDouble() != _animationInfo.end) {
-        _animationInfo = _animationInfo.toEnd(index.toDouble(),
-            current: current ?? _animationInfo.valueAt(_animation.value));
+        _setAnimationInfo(_animationInfo.toEnd(index.toDouble(),
+            current: current ?? _animationInfo.valueAt(_animation.value)));
         _controller.duration = widget.animationDuration;
         _animation.curve = widget.animationCurve;
         _controller.forward(from: 0.0);
       }
     } else {
-      _animationInfo = _animationInfo.toEnd(index.toDouble()).ended();
+      _setAnimationInfo(_animationInfo.toEnd(index.toDouble()).ended());
     }
     _appearingController.forward();
   }
@@ -798,7 +836,7 @@ class _CustomAnimatedToggleSwitchState<T>
   /// the current cursor position.
   void _onDragged(double indexPosition, double pos) {
     if (!_isActive) return;
-    _animationInfo = _animationInfo.dragged(indexPosition, pos: pos);
+    _setAnimationInfo(_animationInfo.dragged(indexPosition, pos: pos));
     _controller.duration = widget.dragStartDuration;
     _animation.curve = widget.dragStartCurve;
     _controller.forward(from: 0.0);
@@ -807,9 +845,7 @@ class _CustomAnimatedToggleSwitchState<T>
   /// Updates the current drag position.
   void _onDragUpdate(double indexPosition) {
     if (_animationInfo.toggleMode != ToggleMode.dragged) return;
-    setState(() {
-      _animationInfo = _animationInfo.dragged(indexPosition);
-    });
+    _setAnimationInfo(_animationInfo.dragged(indexPosition), setState: true);
   }
 
   /// Ends the dragging of the indicator and starts an animation
@@ -818,14 +854,14 @@ class _CustomAnimatedToggleSwitchState<T>
     if (_animationInfo.toggleMode != ToggleMode.dragged) return;
     int index = _animationInfo.end.round();
     T newValue = widget.values[index];
-    _animationInfo = _animationInfo.none(current: _animationInfo.end);
+    _setAnimationInfo(_animationInfo.none(current: _animationInfo.end));
     if (widget.current != newValue) _onChanged(newValue);
     _checkValuePosition();
   }
 
   /// Cancels drag because of loading or inactivity
   void _cancelDrag() {
-    _animationInfo = _animationInfo.none();
+    _setAnimationInfo(_animationInfo.none());
     _checkValuePosition();
   }
 
@@ -955,4 +991,18 @@ class _AnimationInfo {
           toggleMode: toggleMode, loading: loading);
 
   double valueAt(num position) => start + (end - start) * position;
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is _AnimationInfo &&
+          runtimeType == other.runtimeType &&
+          start == other.start &&
+          end == other.end &&
+          toggleMode == other.toggleMode &&
+          loading == other.loading;
+
+  @override
+  int get hashCode =>
+      start.hashCode ^ end.hashCode ^ toggleMode.hashCode ^ loading.hashCode;
 }


Test patch:
diff --git a/test/helper.dart b/test/helper.dart
index 2fa1469..98dcc35 100644
--- a/test/helper.dart
+++ b/test/helper.dart
@@ -90,6 +90,7 @@ typedef SwitchBuilder<T> = AnimatedToggleSwitch<T> Function({
   Size? indicatorSize,
   AnimationType? indicatorAnimationType,
   AnimationType? iconAnimationType,
+  PositionListener<T>? positionListener,
 });
 
 typedef SimpleSwitchBuilder<T> = AnimatedToggleSwitch<T> Function({
@@ -112,6 +113,7 @@ typedef SimpleSwitchBuilder<T> = AnimatedToggleSwitch<T> Function({
   Size? indicatorSize,
   AnimationType? indicatorAnimationType,
   AnimationType? iconAnimationType,
+  PositionListener<T>? positionListener,
 });
 
 /// Tests all AnimatedToggleSwitch constructors
@@ -157,6 +159,7 @@ void defaultTestAllSwitches(
         Size? indicatorSize,
         AnimationType? indicatorAnimationType,
         AnimationType? iconAnimationType,
+        PositionListener<int>? positionListener,
       }) =>
           buildSwitch(
         current: current,
@@ -179,6 +182,7 @@ void defaultTestAllSwitches(
         indicatorSize: indicatorSize,
         indicatorAnimationType: indicatorAnimationType,
         iconAnimationType: iconAnimationType,
+        positionListener: positionListener,
       ),
       type,
       defaultValues,
@@ -213,6 +217,7 @@ void defaultTestAllSwitches(
           Size? indicatorSize,
           AnimationType? indicatorAnimationType,
           AnimationType? iconAnimationType,
+          PositionListener<int>? positionListener,
         }) =>
             AnimatedToggleSwitch<int>.dual(
           current: current,
@@ -238,6 +243,7 @@ void defaultTestAllSwitches(
           indicatorSize: indicatorSize ?? const Size.fromWidth(46.0),
           indicatorAnimationType:
               indicatorAnimationType ?? AnimationType.onHover,
+          positionListener: positionListener,
         ),
         ConstructorType.dual,
         values,
@@ -282,6 +288,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.rolling(
                 current: current,
@@ -306,6 +313,7 @@ void testAllSwitches<T>(
                 indicatorSize: indicatorSize ?? const Size.fromWidth(46.0),
                 indicatorAnimationType:
                     indicatorAnimationType ?? AnimationType.onHover,
+                positionListener: positionListener,
               ),
               ConstructorType.rolling,
             ));
@@ -335,6 +343,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.rollingByHeight(
                 current: current,
@@ -364,6 +373,7 @@ void testAllSwitches<T>(
                             indicatorSize.height, 50.0, 2.0)),
                 indicatorAnimationType:
                     indicatorAnimationType ?? AnimationType.onHover,
+                positionListener: positionListener,
               ),
               ConstructorType.rolling,
             ));
@@ -394,6 +404,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.size(
                 current: current,
@@ -421,6 +432,7 @@ void testAllSwitches<T>(
                 indicatorSize: indicatorSize ?? const Size.fromWidth(46.0),
                 indicatorAnimationType:
                     indicatorAnimationType ?? AnimationType.onHover,
+                positionListener: positionListener,
               ),
               ConstructorType.size,
             ));
@@ -449,6 +461,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.sizeByHeight(
                 current: current,
@@ -481,6 +494,7 @@ void testAllSwitches<T>(
                             indicatorSize.height, 50.0, 2.0)),
                 indicatorAnimationType:
                     indicatorAnimationType ?? AnimationType.onHover,
+                positionListener: positionListener,
               ),
               ConstructorType.size,
             ));
@@ -511,6 +525,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.custom(
                 current: current,
@@ -539,6 +554,7 @@ void testAllSwitches<T>(
                     indicatorAnimationType ?? AnimationType.onHover,
                 iconAnimationType:
                     iconAnimationType ?? AnimationType.onSelected,
+                positionListener: positionListener,
               ),
               ConstructorType.custom,
             ));
@@ -567,6 +583,7 @@ void testAllSwitches<T>(
                 Size? indicatorSize,
                 AnimationType? indicatorAnimationType,
                 AnimationType? iconAnimationType,
+                PositionListener<T>? positionListener,
               }) =>
                   AnimatedToggleSwitch<T>.customByHeight(
                 current: current,
@@ -600,6 +617,7 @@ void testAllSwitches<T>(
                     indicatorAnimationType ?? AnimationType.onHover,
                 iconAnimationType:
                     iconAnimationType ?? AnimationType.onSelected,
+                positionListener: positionListener,
               ),
               ConstructorType.custom,
             ));
diff --git a/test/listener_test.dart b/test/listener_test.dart
new file mode 100644
index 0000000..90f31b6
--- /dev/null
+++ b/test/listener_test.dart
@@ -0,0 +1,35 @@
+import 'package:animated_toggle_switch/animated_toggle_switch.dart';
+import 'package:mocktail/mocktail.dart';
+
+import 'helper.dart';
+import 'mocks.dart';
+
+void main() {
+  defaultTestAllSwitches('AnimatedToggleSwitch triggers positionListener',
+      (tester, buildSwitch, type, values) async {
+    final current = values.first;
+    final nextIndex = values.length - 1;
+    final next = values[nextIndex];
+    final positionListener = MockPositionListenerFunction<int>();
+    await tester.pumpWidget(TestWrapper(
+      child: buildSwitch(
+        current: current,
+        iconBuilder: iconBuilder,
+        positionListener: positionListener,
+      ),
+    ));
+    await tester.pumpWidget(TestWrapper(
+      child: buildSwitch(
+        current: next,
+        iconBuilder: iconBuilder,
+        positionListener: positionListener,
+      ),
+    ));
+    await tester.pump(const Duration(seconds: 1));
+    verify(() => positionListener(PositionListenerInfo(
+        value: next,
+        index: nextIndex,
+        position: nextIndex.toDouble(),
+        mode: ToggleMode.none))).called(1);
+  });
+}
diff --git a/test/mocks.dart b/test/mocks.dart
index b56c802..dc0bc8c 100644
--- a/test/mocks.dart
+++ b/test/mocks.dart
@@ -9,7 +9,14 @@ abstract class TestOnChangedFunction<T> {
   void call(T value);
 }
 
+abstract class TestPositionListenerFunction<T> {
+  void call(PositionListenerInfo<T> positionListenerInfo);
+}
+
 class MockFunction<T> extends Mock implements TestOnTapFunction<T> {}
 
 class MockOnChangedFunction<T> extends Mock
     implements TestOnChangedFunction<T> {}
+
+class MockPositionListenerFunction<T> extends Mock
+    implements TestPositionListenerFunction<T> {}