Created
November 2, 2021 11:28
-
-
Save amitkot/3c7fc5375b6626b1e939879b55a9482d to your computer and use it in GitHub Desktop.
Flipping pill-shaped Switch in Flutter
This file contains hidden or 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 'dart:math'; | |
import 'package:flutter/material.dart'; | |
class FlippingSwitch extends StatefulWidget { | |
const FlippingSwitch( | |
{required this.color, | |
required this.backgroundColor, | |
required this.firstLabel, | |
required this.secondLabel, | |
this.onChange, | |
Key? key}) | |
: super(key: key); | |
final Color color; | |
final Color backgroundColor; | |
final String firstLabel; | |
final String secondLabel; | |
final void Function(bool)? onChange; | |
@override | |
State<FlippingSwitch> createState() => _FlippingSwitchState(); | |
} | |
class _FlippingSwitchState extends State<FlippingSwitch> | |
with TickerProviderStateMixin { | |
final double _maxTiltAngle = pi / 6; | |
late final AnimationController _tiltController; | |
late Animation _tiltAnimation; | |
double _directionMultiplier = 1; | |
late final AnimationController _flipController; | |
@override | |
void initState() { | |
super.initState(); | |
_tiltController = AnimationController( | |
vsync: this, | |
duration: const Duration(milliseconds: 200), | |
reverseDuration: const Duration(milliseconds: 1500), | |
)..addStatusListener((status) { | |
if (status == AnimationStatus.completed) { | |
_tiltController.reverse(); | |
} | |
}); | |
_tiltAnimation = CurvedAnimation( | |
parent: _tiltController, | |
curve: Curves.easeOut, | |
reverseCurve: Curves.elasticOut.flipped, | |
); | |
_flipController = AnimationController( | |
vsync: this, | |
duration: const Duration(milliseconds: 500), | |
); | |
_jumpToMode(leftEnabled: true); | |
} | |
@override | |
void dispose() { | |
_tiltController.dispose(); | |
_flipController.dispose(); | |
super.dispose(); | |
} | |
void _jumpToMode({required bool leftEnabled}) { | |
_flipController.value = leftEnabled ? 1.0 : 0.0; | |
} | |
void _flipSwitch() { | |
if (_flipController.isCompleted) { | |
_directionMultiplier = -1; | |
_tiltController.forward(); | |
_flipController.reverse(); | |
widget.onChange?.call(false); | |
} else { | |
_directionMultiplier = 1; | |
_tiltController.forward(); | |
_flipController.forward(); | |
widget.onChange?.call(true); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return DefaultTextStyle( | |
style: TextStyle( | |
color: widget.color, | |
fontSize: 20, | |
fontWeight: FontWeight.bold, | |
), | |
child: AnimatedBuilder( | |
animation: _tiltController, | |
builder: (context, child) { | |
return Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.001) | |
..rotateY( | |
_tiltAnimation.value * _maxTiltAngle * _directionMultiplier), | |
alignment: const FractionalOffset(0.5, 1.0), | |
child: child, | |
); | |
}, | |
child: Stack( | |
children: [ | |
_background(), | |
AnimatedBuilder( | |
animation: _flipController, | |
builder: (context, child) { | |
return _flippingSwitch(angle: pi * _flipController.value); | |
}, | |
), | |
], | |
), | |
), | |
); | |
} | |
Widget _background() { | |
return GestureDetector( | |
onTap: _flipSwitch, | |
child: Container( | |
width: 250, | |
height: 64, | |
decoration: BoxDecoration( | |
color: widget.backgroundColor, | |
borderRadius: BorderRadius.circular(32), | |
border: Border.all( | |
width: 5, | |
color: widget.color, | |
), | |
), | |
child: Row( | |
children: [ | |
Expanded( | |
child: Center( | |
child: Text( | |
widget.firstLabel, | |
), | |
), | |
), | |
Expanded( | |
child: Center( | |
child: Text( | |
widget.secondLabel, | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
Widget _flippingSwitch({required double angle}) { | |
final isLeft = angle > (pi / 2); | |
final transformAngle = isLeft ? angle - pi : angle; | |
final label = isLeft ? widget.firstLabel : widget.secondLabel; | |
return Positioned( | |
top: 0, | |
bottom: 0, | |
right: isLeft ? null : 0, | |
left: isLeft ? 0 : null, | |
child: Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.002) | |
..rotateY(transformAngle), | |
alignment: isLeft | |
? const FractionalOffset(1.0, 1.0) | |
: const FractionalOffset(0, 1.0), | |
child: Container( | |
decoration: BoxDecoration( | |
color: widget.color, | |
borderRadius: BorderRadius.only( | |
topRight: isLeft ? Radius.zero : const Radius.circular(32), | |
bottomRight: isLeft ? Radius.zero : const Radius.circular(32), | |
topLeft: isLeft ? const Radius.circular(32) : Radius.zero, | |
bottomLeft: isLeft ? const Radius.circular(32) : Radius.zero, | |
), | |
), | |
width: 125, | |
child: Center( | |
child: Text( | |
label, | |
style: TextStyle( | |
color: widget.backgroundColor, | |
), | |
), | |
), | |
), | |
), | |
); | |
} | |
} |
Author
amitkot
commented
Nov 2, 2021
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment