Instantly share code, notes, and snippets.
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save peerwaya/6e45f1ec1f91ddb992f82815e6ca1036 to your computer and use it in GitHub Desktop.
Flutter Swipe Button Demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter/physics.dart'; | |
void main() => runApp(SwipeDemoApp()); | |
class SwipeDemoApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
primaryColor: const Color(0xFFF2BF3F), | |
primaryColorLight: const Color(0xFFF7E0AA), | |
), | |
home: Scaffold( | |
body: Align( | |
alignment: Alignment(0.0, 0.8), | |
child: Padding( | |
padding: const EdgeInsets.all(24.0), | |
child: SwipeButton( | |
thumb: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Align(widthFactor: 0.33, child: Icon(Icons.chevron_right)), | |
Align(widthFactor: 0.33, child: Icon(Icons.chevron_right)), | |
Align(widthFactor: 0.33, child: Icon(Icons.chevron_right)), | |
], | |
), | |
content: Center( | |
child: Text('Swipe to buy now'), | |
), | |
onChanged: (result) { | |
print('onChanged $result'); | |
}, | |
), | |
), | |
), | |
), | |
); | |
} | |
} | |
enum SwipePosition { | |
SwipeLeft, | |
SwipeRight, | |
} | |
class SwipeButton extends StatefulWidget { | |
const SwipeButton({ | |
Key key, | |
this.thumb, | |
this.content, | |
BorderRadius borderRadius, | |
this.initialPosition = SwipePosition.SwipeLeft, | |
@required this.onChanged, | |
this.height = 56.0, | |
}) : assert(initialPosition != null && onChanged != null && height != null), | |
this.borderRadius = borderRadius ?? BorderRadius.zero, | |
super(key: key); | |
final Widget thumb; | |
final Widget content; | |
final BorderRadius borderRadius; | |
final double height; | |
final SwipePosition initialPosition; | |
final ValueChanged<SwipePosition> onChanged; | |
@override | |
SwipeButtonState createState() => SwipeButtonState(); | |
} | |
class SwipeButtonState extends State<SwipeButton> | |
with SingleTickerProviderStateMixin { | |
final GlobalKey _containerKey = GlobalKey(); | |
final GlobalKey _positionedKey = GlobalKey(); | |
AnimationController _controller; | |
Animation<double> _contentAnimation; | |
Offset _start = Offset.zero; | |
RenderBox get _positioned => _positionedKey.currentContext.findRenderObject(); | |
RenderBox get _container => _containerKey.currentContext.findRenderObject(); | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController.unbounded(vsync: this); | |
_contentAnimation = Tween<double>(begin: 1.0, end: 0.0) | |
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); | |
if (widget.initialPosition == SwipePosition.SwipeRight) { | |
_controller.value = 1.0; | |
} | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final theme = Theme.of(context); | |
return SizedBox( | |
width: double.infinity, | |
height: widget.height, | |
child: Stack( | |
key: _containerKey, | |
children: <Widget>[ | |
DecoratedBox( | |
decoration: BoxDecoration( | |
color: theme.primaryColorLight, | |
borderRadius: widget.borderRadius, | |
), | |
child: ClipRRect( | |
clipper: _SwipeButtonClipper( | |
animation: _controller, | |
borderRadius: widget.borderRadius, | |
), | |
borderRadius: widget.borderRadius, | |
child: SizedBox.expand( | |
child: Padding( | |
padding: EdgeInsets.only(left: widget.height), | |
child: FadeTransition( | |
opacity: _contentAnimation, | |
child: widget.content, | |
), | |
), | |
), | |
), | |
), | |
AnimatedBuilder( | |
animation: _controller, | |
builder: (BuildContext context, Widget child) { | |
return Align( | |
alignment: Alignment((_controller.value * 2.0) - 1.0, 0.0), | |
child: child, | |
); | |
}, | |
child: GestureDetector( | |
onHorizontalDragStart: _onDragStart, | |
onHorizontalDragUpdate: _onDragUpdate, | |
onHorizontalDragEnd: _onDragEnd, | |
child: Container( | |
key: _positionedKey, | |
width: widget.height, | |
height: widget.height, | |
decoration: BoxDecoration( | |
color: theme.primaryColor, | |
borderRadius: widget.borderRadius, | |
), | |
child: widget.thumb, | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
void _onDragStart(DragStartDetails details) { | |
final pos = _positioned.globalToLocal(details.globalPosition); | |
_start = Offset(pos.dx, 0.0); | |
_controller.stop(canceled: true); | |
} | |
void _onDragUpdate(DragUpdateDetails details) { | |
final pos = _container.globalToLocal(details.globalPosition) - _start; | |
final extent = _container.size.width - _positioned.size.width; | |
_controller.value = (pos.dx.clamp(0.0, extent) / extent); | |
} | |
void _onDragEnd(DragEndDetails details) { | |
final extent = _container.size.width - _positioned.size.width; | |
var fractionalVelocity = (details.primaryVelocity / extent).abs(); | |
if (fractionalVelocity < 0.5) { | |
fractionalVelocity = 0.5; | |
} | |
SwipePosition result; | |
double acceleration, velocity; | |
if (_controller.value > 0.5) { | |
acceleration = 0.5; | |
velocity = fractionalVelocity; | |
result = SwipePosition.SwipeRight; | |
} else { | |
acceleration = -0.5; | |
velocity = -fractionalVelocity; | |
result = SwipePosition.SwipeLeft; | |
} | |
final simulation = _SwipeSimulation( | |
acceleration, | |
_controller.value, | |
1.0, | |
velocity, | |
); | |
_controller.animateWith(simulation).then((_) { | |
if (widget.onChanged != null) { | |
widget.onChanged(result); | |
} | |
}); | |
} | |
} | |
class _SwipeSimulation extends GravitySimulation { | |
_SwipeSimulation( | |
double acceleration, double distance, double endDistance, double velocity) | |
: super(acceleration, distance, endDistance, velocity); | |
@override | |
double x(double time) => super.x(time).clamp(0.0, 1.0); | |
@override | |
bool isDone(double time) { | |
final _x = x(time).abs(); | |
return _x <= 0.0 || _x >= 1.0; | |
} | |
} | |
class _SwipeButtonClipper extends CustomClipper<RRect> { | |
const _SwipeButtonClipper({ | |
@required this.animation, | |
@required this.borderRadius, | |
}) : assert(animation != null && borderRadius != null), | |
super(reclip: animation); | |
final Animation<double> animation; | |
final BorderRadius borderRadius; | |
@override | |
RRect getClip(Size size) { | |
return borderRadius.toRRect( | |
Rect.fromLTRB( | |
size.width * animation.value, | |
0.0, | |
size.width, | |
size.height, | |
), | |
); | |
} | |
@override | |
bool shouldReclip(_SwipeButtonClipper oldClipper) => true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment