Instantly share code, notes, and snippets.
Forked from slightfoot/swipe_button.dart
Last active
March 18, 2019 12:01
-
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 bmatheus91/6310870b8d475e6b495b45c1f19e4e45 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) { | |
// Pega a posicao X, Y em que a tela foi tocada no Gesture Detector / Container | |
final pos = _positioned.globalToLocal(details.globalPosition); | |
// Utiliza apenas o X (Horizontal) para iniciar o gesto | |
_start = Offset(pos.dx, 0.0); | |
_controller.stop(canceled: true); | |
} | |
void _onDragUpdate(DragUpdateDetails details) { | |
// Pega a possicao que a tela foi tocada no Stack - A posicao inicial do Gesture Detector | |
final pos = _container.globalToLocal(details.globalPosition) - _start; | |
// Armazena a extensao do caminho a ser percorrido | |
// diminuindo o comprimento do Stack pelo do "Botao" | |
final extent = _container.size.width - _positioned.size.width; | |
// Calculo para variar a animacao (0.0 a 1.0) | |
_controller.value = (pos.dx.clamp(0.0, extent) / extent); | |
//print(_controller.value); | |
} | |
void _onDragEnd(DragEndDetails details) { | |
// Extensao = Comprimento do Stack - Comprimento do Gesture Detector | |
final extent = _container.size.width - _positioned.size.width; | |
// Velocidade do movimento de arrastar ate o final | |
var fractionalVelocity = (details.primaryVelocity / extent).abs(); | |
if (fractionalVelocity < 0.5) { | |
fractionalVelocity = 0.5; | |
} | |
SwipePosition result; | |
double acceleration, velocity; | |
// Verifica, ao liberar o toque de arrastar, se a animacao esta no meio do caminho | |
// Caso esteja finaliza o movimento pra direita, | |
// senao volta pra esquerda | |
if (_controller.value > 0.5) { | |
acceleration = 0.5; | |
velocity = fractionalVelocity; | |
result = SwipePosition.SwipeRight; | |
} else { | |
acceleration = -0.5; | |
velocity = -fractionalVelocity; | |
result = SwipePosition.SwipeLeft; | |
} | |
// Se nao arrastar ate o final volta para o meio | |
if (_controller.value > 0.0 || _controller.value < 1.0) { | |
_controller.value = 0.5; | |
} | |
// Cria uma "gravidade" do Botao | |
final simulation = _SwipeSimulation( | |
acceleration, | |
_controller.value, | |
1.0, | |
velocity, | |
); | |
// Aplica a "gravidade" a animacao | |
_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