Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
],
),
)),
Expand Down
43 changes: 38 additions & 5 deletions lib/toggle_switch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ class ToggleSwitch extends StatefulWidget {
/// Set custom widget
final List<Widget>? customWidgets;

/// Enable swipe gesture to change selection
final bool enableSwipe;

ToggleSwitch({
Key? key,
this.totalSwitches,
Expand Down Expand Up @@ -161,6 +164,7 @@ class ToggleSwitch extends StatefulWidget {
this.centerText = false,
this.multiLineText = false,
this.customWidgets,
this.enableSwipe = false,
}) : super(key: key);

@override
Expand Down Expand Up @@ -247,10 +251,17 @@ class _ToggleSwitchState extends State<ToggleSwitch>
),
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];
Expand All @@ -266,7 +277,8 @@ class _ToggleSwitchState extends State<ToggleSwitch>
states: states,
);
}
}),
}),
),
),
),
);
Expand Down Expand Up @@ -436,6 +448,27 @@ class _ToggleSwitchState extends State<ToggleSwitch>
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<bool> states =
widget.states ?? List<bool>.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);
Comment thread
Vasusn marked this conversation as resolved.
}
Comment thread
Vasusn marked this conversation as resolved.

/// Icon widget
Widget _icon(
{required int index,
Expand Down
154 changes: 154 additions & 0 deletions test/toggle_switch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
Comment thread
Vasusn marked this conversation as resolved.

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