Skip to content

Instantly share code, notes, and snippets.

@bmatheus91
Forked from slightfoot/swipe_button.dart
Last active March 18, 2019 12:01
Show Gist options
  • Save bmatheus91/6310870b8d475e6b495b45c1f19e4e45 to your computer and use it in GitHub Desktop.
Save bmatheus91/6310870b8d475e6b495b45c1f19e4e45 to your computer and use it in GitHub Desktop.
Flutter Swipe Button Demo
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