Instantly share code, notes, and snippets.
Created
September 9, 2019 23:26
-
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 stargazing-dino/af5872100739fe66f8afa8f0195cacb1 to your computer and use it in GitHub Desktop.
Text Button Group
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'; | |
// https://pub.dev/packages/reorderables | |
class TextButtonGroup extends StatefulWidget { | |
TextButtonGroup({ | |
Key key, | |
this.initialIndex = 0, | |
@required this.titles, | |
@required this.onTap, | |
@required this.duration, | |
this.spacing = 20.0, | |
this.spacingFraction = 0.3, | |
this.curve = Curves.easeIn, | |
this.badgeRadius = 4, | |
this.badgeColor = Colors.blue, | |
this.selectedItemColor = Colors.black, | |
this.unselectedItemColor = Colors.grey, | |
this.alignment = Alignment.center, | |
}) : super(key: key); | |
final int initialIndex; | |
final List<TextSpan> titles; | |
final void Function(int selectedIndex) onTap; | |
final Duration duration; | |
final double spacing; | |
final double spacingFraction; | |
final Curve curve; | |
final double badgeRadius; | |
final Color badgeColor; | |
final Color selectedItemColor; | |
final Color unselectedItemColor; | |
final Alignment alignment; | |
@override | |
_TextButtonGroupState createState() => _TextButtonGroupState(); | |
} | |
class _TextButtonGroupState extends State<TextButtonGroup> | |
with SingleTickerProviderStateMixin { | |
double width = 0; | |
double leftWidth = 0; | |
int currentIndex = 0; | |
static const double BAR_HEIGHT = 60; | |
static const double INDICATOR_HEIGHT = 2; | |
List<double> widths; | |
@override | |
void initState() { | |
currentIndex = widget.initialIndex; | |
super.initState(); | |
} | |
_select(int index) { | |
double total = 0; | |
for (var i = index; i >= 0; i--) total += widths[i]; | |
leftWidth = total; | |
currentIndex = index; | |
widget.onTap(currentIndex); | |
setState(() {}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder( | |
builder: (context, constraints) { | |
final List<TextBox> lastBoxes = widget.titles | |
.map<TextBox>( | |
(textSpan) => _calcLastLineEnd(context, constraints, textSpan)) | |
.toList(); | |
widths = lastBoxes | |
.map<double>( | |
(lastBox) => (lastBox.right - lastBox.left) + widget.spacing) | |
.toList(); | |
// TODO: rename to sumWidth | |
width = widths.fold(0, (p, c) => p + c); | |
final rightSpacing = | |
(widget.spacing * widget.spacingFraction - widget.spacing) - | |
widget.badgeRadius; | |
final leftPosition = leftWidth == 0 ? widths[0] : leftWidth; | |
return Stack( | |
alignment: widget.alignment, | |
fit: StackFit.loose, | |
overflow: Overflow.visible, | |
children: <Widget>[ | |
IntrinsicWidth( | |
child: Row( | |
children: widget.titles | |
.asMap() | |
.map( | |
(int index, title) { | |
return MapEntry( | |
index, | |
Container( | |
margin: EdgeInsets.only( | |
right: index == widget.titles.length - 1 | |
? widget.spacing * widget.spacingFraction + | |
widget.badgeRadius | |
: widget.spacing, | |
top: widget.badgeRadius, | |
), | |
child: GestureDetector( | |
onTap: () { | |
_select(index); | |
}, | |
child: _buildTextWidget( | |
title, | |
index, | |
widths[index], | |
), | |
), | |
), | |
); | |
}, | |
) | |
.values | |
.toList(), | |
), | |
), | |
// Indicator | |
AnimatedPositioned( | |
left: leftPosition + rightSpacing, | |
top: 0, | |
duration: widget.duration, | |
curve: widget.curve, | |
child: CircleAvatar( | |
radius: widget.badgeRadius, | |
backgroundColor: widget.badgeColor, | |
), | |
), | |
], | |
); | |
}, | |
); | |
} | |
// Calculate the left, top, bottom position of the end of the last text | |
// line. | |
TextBox _calcLastLineEnd( | |
BuildContext context, | |
BoxConstraints constraints, | |
TextSpan textWidget, | |
) { | |
final richTextWidget = Text.rich(textWidget).build(context) as RichText; | |
final renderObject = richTextWidget.createRenderObject(context); | |
renderObject.layout(constraints); | |
final lastBox = renderObject | |
.getBoxesForSelection(TextSelection( | |
baseOffset: 0, extentOffset: textWidget.toPlainText().length)) | |
.last; | |
return lastBox; | |
} | |
Widget _buildTextWidget(TextSpan title, int index, double width) { | |
bool isSelected = index == currentIndex; | |
final color = | |
isSelected ? widget.selectedItemColor : widget.unselectedItemColor; | |
return Stack( | |
alignment: AlignmentDirectional.center, | |
children: <Widget>[ | |
AnimatedOpacity( | |
opacity: isSelected ? 1.0 : 0.5, | |
duration: widget.duration, | |
curve: widget.curve, | |
child: Text( | |
title.text, | |
style: title.style == null | |
? TextStyle(color: color) | |
: title.style.copyWith(color: color), | |
), | |
), | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment