Last active
May 31, 2021 03:48
-
-
Save Nash0x7E2/16c9d5eac8c8278e8771fb359fef323c to your computer and use it in GitHub Desktop.
Horizontal item selector inspired by this design https://dribbble.com/shots/6591397-Travel-Application/attachments.
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'; | |
class ItemTextStyle { | |
const ItemTextStyle({ | |
@required this.initialStyle, | |
@required this.animatedStyle, | |
}); | |
final TextStyle initialStyle; | |
final TextStyle animatedStyle; | |
} | |
class ItemSelectorBar extends StatelessWidget { | |
const ItemSelectorBar({ | |
Key key, | |
@required this.activeIndex, | |
@required this.onTap, | |
@required this.items, | |
this.animatedTextStyle = const ItemTextStyle( | |
initialStyle: TextStyle(color: Colors.grey, fontSize: 14), | |
animatedStyle: TextStyle(color: Colors.white, fontSize: 14)), | |
this.itemPadding = const EdgeInsets.all(8.0), | |
}) : super(key: key); | |
final int activeIndex; | |
final ValueChanged<int> onTap; | |
final List<String> items; | |
final EdgeInsetsGeometry itemPadding; | |
final ItemTextStyle animatedTextStyle; | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: Theme.of(context).primaryColor, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: [ | |
for (int index = 0; index < items.length; index++) | |
Padding( | |
padding: itemPadding, | |
child: ItemSelector( | |
index: index, | |
name: items[index], | |
activeIndex: activeIndex, | |
style: animatedTextStyle, | |
onTap: onTap, | |
), | |
) | |
], | |
), | |
); | |
} | |
} | |
class ItemSelector extends StatefulWidget { | |
const ItemSelector({ | |
Key key, | |
this.name, | |
this.index, | |
this.activeIndex, | |
this.style, | |
this.onTap, | |
}) : super(key: key); | |
final String name; | |
final int index; | |
final int activeIndex; | |
final ItemTextStyle style; | |
final ValueChanged<int> onTap; | |
@override | |
_ItemSelectorState createState() => _ItemSelectorState(); | |
} | |
class _ItemSelectorState extends State<ItemSelector> with SingleTickerProviderStateMixin { | |
bool _isSelected; | |
Animation<TextStyle> _textStyle; | |
AnimationController _controller; | |
@override | |
void initState() { | |
super.initState(); | |
_isSelected = widget.activeIndex == widget.index; | |
_controller = AnimationController( | |
vsync: this, | |
duration: const Duration(milliseconds: 250), | |
value: _isSelected ? 1.0 : 0.0, | |
); | |
_textStyle = TextStyleTween(begin: widget.style.initialStyle, end: widget.style.animatedStyle).animate(_controller); | |
} | |
@override | |
void didUpdateWidget(ItemSelector oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_isSelected = widget.activeIndex == widget.index; | |
animateSelected(); | |
} | |
void animateSelected() { | |
if (_isSelected) { | |
_controller.forward(); | |
} else { | |
_controller.reverse(); | |
} | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
_controller.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return GestureDetector( | |
onTap: () => widget.onTap(widget.index), | |
child: AnimatedBuilder( | |
animation: _controller, | |
builder: (context, _) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Text( | |
widget.name, | |
style: _textStyle.value, | |
), | |
CustomPaint( | |
painter: _CirclePainter( | |
radius: 4.0, | |
animation: _controller, | |
verticalSpacing: 10, | |
circleColor: Colors.redAccent, | |
), | |
), | |
], | |
); | |
}, | |
), | |
); | |
} | |
} | |
class _CirclePainter extends CustomPainter { | |
_CirclePainter({ | |
@required this.radius, | |
@required this.animation, | |
this.verticalSpacing = 5.0, | |
this.strokeWidth = 10.0, | |
this.circleColor = Colors.redAccent, | |
}) : super(repaint: animation); | |
final double radius; | |
final Animation<double> animation; | |
final double verticalSpacing; | |
final double strokeWidth; | |
final Color circleColor; | |
@override | |
void paint(Canvas canvas, Size size) { | |
final Paint paint = Paint() | |
..color = circleColor | |
..strokeWidth = strokeWidth; | |
canvas.drawCircle( | |
Offset(size.width / 2, size.height / 2 + verticalSpacing), | |
radius * animation.value, | |
paint, | |
); | |
} | |
@override | |
bool shouldRepaint(_CirclePainter oldDelegate) => oldDelegate.animation != animation; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment