Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created December 9, 2018 01:02
Show Gist options
  • Save slightfoot/d2a98cadaaeb7f2df63b6a3996e4dc5e to your computer and use it in GitHub Desktop.
Save slightfoot/d2a98cadaaeb7f2df63b6a3996e4dc5e 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) {
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;
}
@imaNNeo
Copy link

imaNNeo commented Dec 9, 2018

Perfect!

@enricodvn
Copy link

You should add license, or made into a package.

@sr-albert
Copy link

Good widget, it solves my problem, i add your name as �the author when i use your widget, you should make it into a package and add the license for this. Thanks again!

@kishan2612
Copy link

I have changed the code to support reverse swipe content !

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,
    this.thumbColor,
    this.rectColor,
  })  : 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 Color thumbColor;
  final Color rectColor;
  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: widget.rectColor,
              borderRadius: widget.borderRadius,
            ),
            child: ClipRRect(
              clipper: _SwipeButtonClipper(
                  animation: _controller,
                  borderRadius: widget.borderRadius,
                  position: widget.initialPosition),
              borderRadius: widget.borderRadius,
              child: SizedBox.expand(
                child: Padding(
                  padding: EdgeInsets.only(left: widget.height),
                  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: widget.thumbColor,
                  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,
    this.position,
  })  : assert(animation != null && borderRadius != null),
        super(reclip: animation);

  final Animation<double> animation;
  final BorderRadius borderRadius;
  final SwipePosition position;

  @override
  RRect getClip(Size size) {
    return borderRadius.toRRect(
      Rect.fromLTRB(
        position == SwipePosition.SwipeRight
            ? size.width * animation.value
            : size.width,
        0.0,
        position == SwipePosition.SwipeLeft
            ? size.width * animation.value
            : 0.0,
        size.height,
      ),
    );
  }

  @override
  bool shouldReclip(_SwipeButtonClipper oldClipper) => true;
}

@AshishInnovantes
Copy link

AshishInnovantes commented Mar 23, 2021

How to change the state of the icons and text when SwipePosition.SwipeRight action is done?

I have changed the code to support reverse swipe content !

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,
    this.thumbColor,
    this.rectColor,
  })  : 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 Color thumbColor;
  final Color rectColor;
  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: widget.rectColor,
              borderRadius: widget.borderRadius,
            ),
            child: ClipRRect(
              clipper: _SwipeButtonClipper(
                  animation: _controller,
                  borderRadius: widget.borderRadius,
                  position: widget.initialPosition),
              borderRadius: widget.borderRadius,
              child: SizedBox.expand(
                child: Padding(
                  padding: EdgeInsets.only(left: widget.height),
                  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: widget.thumbColor,
                  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,
    this.position,
  })  : assert(animation != null && borderRadius != null),
        super(reclip: animation);

  final Animation<double> animation;
  final BorderRadius borderRadius;
  final SwipePosition position;

  @override
  RRect getClip(Size size) {
    return borderRadius.toRRect(
      Rect.fromLTRB(
        position == SwipePosition.SwipeRight
            ? size.width * animation.value
            : size.width,
        0.0,
        position == SwipePosition.SwipeLeft
            ? size.width * animation.value
            : 0.0,
        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