Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active December 9, 2025 15:54
Show Gist options
  • Select an option

  • Save PlugFox/39c051d807efb04759498033ce8452dc to your computer and use it in GitHub Desktop.

Select an option

Save PlugFox/39c051d807efb04759498033ce8452dc to your computer and use it in GitHub Desktop.
Swipeable controller demo.
/*
* Swipeable controller demo.
* https://gist.github.com/PlugFox/39c051d807efb04759498033ce8452dc
* https://dartpad.dev?id=39c051d807efb04759498033ce8452dc
* Mike Matiunin <[email protected]>, 09 December 2025
*/
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
void main() => runZonedGuarded<void>(
() => runApp(const App()),
(error, stackTrace) => print('Top level exception'), // ignore: avoid_print
);
/// {@template app}
/// App widget.
/// {@endtemplate}
class App extends StatelessWidget {
/// {@macro app}
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Swipeable Demo',
home: Scaffold(
appBar: AppBar(title: const Text('Swipeable Demo')),
body: const SafeArea(
child: Center(
child: SizedBox(
width: 1024,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Swipeable(),
),
),
),
),
),
);
}
/// {@template swipeable}
/// Swipeable widget.
/// {@endtemplate}
class Swipeable extends StatefulWidget {
/// {@macro swipeable}
const Swipeable({
super.key, // ignore: unused_element_parameter
});
@override
State<Swipeable> createState() => _SwipeableState();
}
/// State for widget Swipeable.
class _SwipeableState extends State<Swipeable> {
late final SwipeableController _controller;
/* #region Lifecycle */
@override
void initState() {
super.initState();
_controller = SwipeableController()..addListener(_onChange);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
/* #endregion */
void _onChange() {
// TODO(plugfox): Add your callback logic here.
// e.g.
// if (_controller.progress == 1.0)
// widget.onDismissed?.call();
// Mike Matiunin <[email protected]>, 09 December 2025
}
@override
Widget build(BuildContext context) => Center(
child: SizedBox(
height: 64,
child: Material(
type: MaterialType.canvas,
borderRadius: BorderRadius.circular(32),
color: Colors.grey.shade600,
clipBehavior: Clip.hardEdge,
child: LayoutBuilder(
builder: (context, constraints) => Padding(
padding: const EdgeInsets.all(8),
child: Flow(
delegate: _SwipeableDelegate(controller: _controller),
clipBehavior: Clip.none,
children: <Widget>[
// The draggable handle.
GestureDetector(
onHorizontalDragUpdate: (details) {
// Update the progress based on drag delta.
final delta =
details.delta.dx / (constraints.maxWidth - 64);
if (delta == 0) return;
_controller.progress += delta;
},
onHorizontalDragEnd: (details) {
// User has released the drag, start the animation.
_controller.startAnimation();
HapticFeedback.lightImpact().ignore();
},
onHorizontalDragStart: (details) {
// User has started dragging, stop any ongoing animation.
_controller.stopAnimation();
HapticFeedback.lightImpact().ignore();
},
child: Material(
type: MaterialType.circle,
color: Colors.blue,
elevation: 4,
clipBehavior: Clip.none,
child: SizedBox.square(
dimension: 48,
// Show different icons based on the swipe progress.
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => Icon(switch (_controller) {
SwipeableController(
isAnimating: true,
progress: > .5,
) =>
Icons.arrow_forward,
SwipeableController(
isAnimating: true,
) =>
Icons.arrow_back,
_ => Icons.drag_indicator,
}, color: Colors.white),
),
),
),
),
],
),
),
),
),
),
);
}
class _SwipeableDelegate extends FlowDelegate {
_SwipeableDelegate({required this.controller}) : super(repaint: controller);
final SwipeableController controller;
@override
void paintChildren(FlowPaintingContext context) {
if (context.childCount == 0) return;
final progress = controller.progress;
final size = context.size;
final childSize = context.getChildSize(0);
if (childSize == null) return;
final dx = (size.width - childSize.width) * progress;
context.paintChild(
0,
transform: Matrix4.translationValues(
dx,
(size.height - childSize.height) / 2,
0,
),
);
}
@override
bool shouldRepaint(covariant _SwipeableDelegate oldDelegate) =>
!identical(controller, oldDelegate.controller);
}
/// {@template swipeable}
/// Controller for swipeable widget.
/// {@endtemplate}
class SwipeableController with ChangeNotifier {
/// {@macro swipeable}
SwipeableController({double value = 0.0}) : _progress = value {
_ticker = Ticker(_onTick);
}
/// The ticker for driving the animation.
late final Ticker _ticker;
double _progress;
/// The current progress of the swipeable widget.
double get progress => _progress;
/// Starts the swipe animation.
double _elapsed = 0;
/// Sets the current progress of the swipeable widget.
set progress(double value) {
_progress = value.clamp(0.0, 1.0);
notifyListeners();
}
/// Whether the swipe animation is running.
bool get isAnimating => _ticker.isActive;
/// Starts the swipe animation.
void startAnimation() {
if (_progress == 0.0 || _progress == 1.0) return;
if (_ticker.isActive) return;
_elapsed = 0;
_ticker.start();
}
/// Stops the swipe animation.
void stopAnimation() {
if (!_ticker.isActive) return;
_ticker.stop();
_elapsed = 0;
}
/// Reacts to a swipe gesture update.
void _onTick(Duration elapsed) {
final ms = elapsed.inMilliseconds.toDouble();
final delta = (ms - _elapsed) / 750;
_elapsed = ms;
_progress = switch (_progress) {
> 0.5 => math.min(1, _progress + delta),
_ => math.max(0, _progress - delta),
};
if (_progress >= 1.0 || _progress <= 0.0) stopAnimation();
notifyListeners();
}
@override
void dispose() {
_ticker.dispose();
super.dispose();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment