From dc75debee32876a65968ca45317db8130ab8e988 Mon Sep 17 00:00:00 2001 From: Vasu Nageshri Date: Sun, 26 Apr 2026 15:43:06 +0530 Subject: [PATCH] feat: add enableSwipe parameter for gesture-based selection (#71) ## Problem Users requested the ability to swipe between toggle options (issue #71), especially useful for 2-option switches. ## Solution Added an `enableSwipe` parameter (default: `false`). When enabled, a swipe right/down advances selection by one step and swipe left/up retreats selection by one step. Swipes beyond the first or last switch are ignored. ## Changes - lib/toggle_switch.dart: added `enableSwipe` field, constructor param, and `_handleSwipe` method; wrapped RowToColumn in GestureDetector - test/toggle_switch_test.dart: three new tests covering swipe right, swipe left, and swipe disabled (default) Fixes #71 --- example/lib/main.dart | 18 ++++ lib/toggle_switch.dart | 43 ++++++++-- test/toggle_switch_test.dart | 154 +++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index c12c446..1a837b3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -591,6 +591,24 @@ class MyApp extends StatelessWidget { }, ), const SizedBox(height: 20.0), + Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + 'Swipe gesture (enableSwipe: true):', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ToggleSwitch( + initialLabelIndex: 0, + totalSwitches: 3, + labels: ['One', 'Two', 'Three'], + enableSwipe: true, + onToggle: (index) { + print('swiped to: $index'); + }, + ), + const SizedBox(height: 20.0), ], ), )), diff --git a/lib/toggle_switch.dart b/lib/toggle_switch.dart index 5529efb..ebf21b8 100644 --- a/lib/toggle_switch.dart +++ b/lib/toggle_switch.dart @@ -122,6 +122,9 @@ class ToggleSwitch extends StatefulWidget { /// Set custom widget final List? customWidgets; + /// Enable swipe gesture to change selection + final bool enableSwipe; + ToggleSwitch({ Key? key, this.totalSwitches, @@ -161,6 +164,7 @@ class ToggleSwitch extends StatefulWidget { this.centerText = false, this.multiLineText = false, this.customWidgets, + this.enableSwipe = false, }) : super(key: key); @override @@ -247,10 +251,17 @@ class _ToggleSwitchState extends State ), height: !widget.isVertical ? widget.minHeight + _borderWidth : null, width: widget.isVertical ? widget.minWidth + _borderWidth : null, - child: RowToColumn( - isVertical: widget.isVertical, - mainAxisSize: MainAxisSize.min, - children: List.generate(_totalSwitches * 2 - 1, (index) { + child: GestureDetector( + onHorizontalDragEnd: (!widget.enableSwipe || widget.isVertical) + ? null + : (details) => _handleSwipe(details.primaryVelocity), + onVerticalDragEnd: (!widget.enableSwipe || !widget.isVertical) + ? null + : (details) => _handleSwipe(details.primaryVelocity), + child: RowToColumn( + isVertical: widget.isVertical, + mainAxisSize: MainAxisSize.min, + children: List.generate(_totalSwitches * 2 - 1, (index) { /// Active if index matches current final active = index ~/ 2 == widget.initialLabelIndex && states[index ~/ 2]; @@ -266,7 +277,8 @@ class _ToggleSwitchState extends State states: states, ); } - }), + }), + ), ), ), ); @@ -436,6 +448,27 @@ class _ToggleSwitchState extends State widget.onToggle?.call(newIndex); } + /// Handles swipe gesture to advance or retreat selection by one step. + void _handleSwipe(double? velocity) { + if (velocity == null || velocity == 0) return; + final List states = + widget.states ?? List.filled(_totalSwitches, true); + final int next; + if (widget.initialLabelIndex == null) { + // From "no selection", swiping right/down selects the first enabled item + // and swiping left/up selects the last enabled item. + next = velocity > 0 ? 0 : _totalSwitches - 1; + } else { + final current = widget.initialLabelIndex!; + // positive velocity = swipe right/down → advance; negative = swipe left/up → retreat + next = velocity > 0 ? current + 1 : current - 1; + } + if (next < 0 || next >= _totalSwitches) return; + // Do not select a disabled switch. + if (!states[next]) return; + _handleOnTap(next); + } + /// Icon widget Widget _icon( {required int index, diff --git a/test/toggle_switch_test.dart b/test/toggle_switch_test.dart index ac41ff5..573b445 100644 --- a/test/toggle_switch_test.dart +++ b/test/toggle_switch_test.dart @@ -283,4 +283,158 @@ void main() { expect(helloTextFinder, findsOneWidget); expect(flutterTextFinder, findsOneWidget); }); + + // Swiping right on a horizontal ToggleSwitch with enableSwipe:true advances selection. + testWidgets('swipe right advances selection when enableSwipe is true', + (WidgetTester tester) async { + int? lastIndex; + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: MaterialApp( + home: Scaffold( + body: Center( + child: ToggleSwitch( + totalSwitches: 3, + labels: ['A', 'B', 'C'], + initialLabelIndex: 0, + enableSwipe: true, + onToggle: (index) => lastIndex = index, + ), + ), + ), + ), + ), + ); + + // Fling right (positive velocity) on the switch row. + await tester.fling(find.text('A'), const Offset(100, 0), 500); + await tester.pumpAndSettle(); + + expect(lastIndex, equals(1)); + }); + + // Swiping left on a horizontal ToggleSwitch with enableSwipe:true retreats selection. + testWidgets('swipe left retreats selection when enableSwipe is true', + (WidgetTester tester) async { + int? lastIndex; + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: MaterialApp( + home: Scaffold( + body: Center( + child: ToggleSwitch( + totalSwitches: 3, + labels: ['A', 'B', 'C'], + initialLabelIndex: 2, + enableSwipe: true, + onToggle: (index) => lastIndex = index, + ), + ), + ), + ), + ), + ); + + // Fling left (negative velocity) on the switch row. + await tester.fling(find.text('C'), const Offset(-100, 0), 500); + await tester.pumpAndSettle(); + + expect(lastIndex, equals(1)); + }); + + // Swiping does NOT change selection when enableSwipe is false (default). + testWidgets('swipe does not change selection when enableSwipe is false', + (WidgetTester tester) async { + int toggleCallCount = 0; + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: MaterialApp( + home: Scaffold( + body: Center( + child: ToggleSwitch( + totalSwitches: 3, + labels: ['A', 'B', 'C'], + initialLabelIndex: 0, + onToggle: (index) => toggleCallCount++, + ), + ), + ), + ), + ), + ); + + await tester.fling(find.text('A'), const Offset(100, 0), 500); + await tester.pumpAndSettle(); + + expect(toggleCallCount, equals(0)); + }); + + // Swiping beyond the last switch should be ignored. + testWidgets('swipe beyond last index is ignored', + (WidgetTester tester) async { + int toggleCallCount = 0; + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: MaterialApp( + home: Scaffold( + body: Center( + child: ToggleSwitch( + totalSwitches: 3, + labels: ['A', 'B', 'C'], + initialLabelIndex: 2, // already at last + enableSwipe: true, + onToggle: (index) => toggleCallCount++, + ), + ), + ), + ), + ), + ); + + // Fling right — already at the last switch, should be ignored. + await tester.fling(find.text('C'), const Offset(100, 0), 500); + await tester.pumpAndSettle(); + + expect(toggleCallCount, equals(0)); + }); + + // Swiping into a disabled switch should be ignored. + testWidgets('swipe into disabled switch is ignored', + (WidgetTester tester) async { + int toggleCallCount = 0; + + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(size: Size(800, 600)), + child: MaterialApp( + home: Scaffold( + body: Center( + child: ToggleSwitch( + totalSwitches: 3, + labels: ['A', 'B', 'C'], + states: [true, false, true], // index 1 disabled + initialLabelIndex: 0, + enableSwipe: true, + onToggle: (index) => toggleCallCount++, + ), + ), + ), + ), + ), + ); + + // Fling right — next index (1) is disabled, should be ignored. + await tester.fling(find.text('A'), const Offset(100, 0), 500); + await tester.pumpAndSettle(); + + expect(toggleCallCount, equals(0)); + }); }