-
-
Save afonsocraposo/23928895d61508e1347bc4bfdf686383 to your computer and use it in GitHub Desktop.
Button Tabs (IOS style)
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'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: MyHomePage(), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin { | |
// TickerProviderStateMixin allows the fade out/fade in animation when changing the active button | |
// this will control the button clicks and tab changing | |
TabController _controller; | |
// this will control the animation when a button changes from an off state to an on state | |
AnimationController _animationControllerOn; | |
// this will control the animation when a button changes from an on state to an off state | |
AnimationController _animationControllerOff; | |
// this will give the background color values of a button when it changes to an on state | |
Animation _colorTweenBackgroundOn; | |
Animation _colorTweenBackgroundOff; | |
// this will give the foreground color values of a button when it changes to an on state | |
Animation _colorTweenForegroundOn; | |
Animation _colorTweenForegroundOff; | |
// when swiping, the _controller.index value only changes after the animation, therefore, we need this to trigger the animations and save the current index | |
int _currentIndex = 0; | |
// saves the previous active tab | |
int _prevControllerIndex = 0; | |
// saves the value of the tab animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5 | |
double _aniValue = 0.0; | |
// saves the previous value of the tab animation. It's used to figure the direction of the animation | |
double _prevAniValue = 0.0; | |
// these will be our tab icons. You can use whatever you like for the content of your buttons | |
List _icons = [ | |
Icons.star, | |
Icons.whatshot, | |
Icons.call, | |
Icons.contacts, | |
Icons.email, | |
Icons.donut_large | |
]; | |
// active button's foreground color | |
Color _foregroundOn = Colors.white; | |
Color _foregroundOff = Colors.black; | |
// active button's background color | |
Color _backgroundOn = Colors.blue; | |
Color _backgroundOff = Colors.grey[300]; | |
// scroll controller for the TabBar | |
ScrollController _scrollController = new ScrollController(); | |
// this will save the keys for each Tab in the Tab Bar, so we can retrieve their position and size for the scroll controller | |
List _keys = []; | |
// regist if the the button was tapped | |
bool _buttonTap = false; | |
@override | |
void initState() { | |
super.initState(); | |
for (int index = 0; index < _icons.length; index++) { | |
// create a GlobalKey for each Tab | |
_keys.add(new GlobalKey()); | |
} | |
// this creates the controller with 6 tabs (in our case) | |
_controller = TabController(vsync: this, length: _icons.length); | |
// this will execute the function every time there's a swipe animation | |
_controller.animation.addListener(_handleTabAnimation); | |
// this will execute the function every time the _controller.index value changes | |
_controller.addListener(_handleTabChange); | |
_animationControllerOff = | |
AnimationController(vsync: this, duration: Duration(milliseconds: 75)); | |
// so the inactive buttons start in their "final" state (color) | |
_animationControllerOff.value = 1.0; | |
_colorTweenBackgroundOff = | |
ColorTween(begin: _backgroundOn, end: _backgroundOff) | |
.animate(_animationControllerOff); | |
_colorTweenForegroundOff = | |
ColorTween(begin: _foregroundOn, end: _foregroundOff) | |
.animate(_animationControllerOff); | |
_animationControllerOn = | |
AnimationController(vsync: this, duration: Duration(milliseconds: 150)); | |
// so the inactive buttons start in their "final" state (color) | |
_animationControllerOn.value = 1.0; | |
_colorTweenBackgroundOn = | |
ColorTween(begin: _backgroundOff, end: _backgroundOn) | |
.animate(_animationControllerOn); | |
_colorTweenForegroundOn = | |
ColorTween(begin: _foregroundOff, end: _foregroundOn) | |
.animate(_animationControllerOn); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('Button Tabs (IOS Style)'), | |
), | |
backgroundColor: Colors.white, | |
body: Column(children: <Widget>[ | |
// this is the TabBar | |
Container( | |
height: 49.0, | |
// this generates our tabs buttons | |
child: ListView.builder( | |
// this gives the TabBar a bounce effect when scrolling farther than it's size | |
physics: BouncingScrollPhysics(), | |
controller: _scrollController, | |
// make the list horizontal | |
scrollDirection: Axis.horizontal, | |
// number of tabs | |
itemCount: _icons.length, | |
itemBuilder: (BuildContext context, int index) { | |
return Padding( | |
// each button's key | |
key: _keys[index], | |
// padding for the buttons | |
padding: EdgeInsets.all(6.0), | |
child: ButtonTheme( | |
child: AnimatedBuilder( | |
animation: _colorTweenBackgroundOn, | |
builder: (context, child) => FlatButton( | |
// get the color of the button's background (dependent of its state) | |
color: _getBackgroundColor(index), | |
// make the button a rectangle with round corners | |
shape: RoundedRectangleBorder( | |
borderRadius: new BorderRadius.circular(7.0)), | |
onPressed: () { | |
setState(() { | |
_buttonTap = true; | |
// trigger the controller to change between Tab Views | |
_controller.animateTo(index); | |
// set the current index | |
_setCurrentIndex(index); | |
// scroll to the tapped button (needed if we tap the active button and it's not on its position) | |
_scrollTo(index); | |
}); | |
}, | |
child: Icon( | |
// get the icon | |
_icons[index], | |
// get the color of the icon (dependent of its state) | |
color: _getForegroundColor(index), | |
)), | |
))); | |
})), | |
Flexible( | |
// this will host our Tab Views | |
child: TabBarView( | |
// and it is controlled by the controller | |
controller: _controller, | |
children: <Widget>[ | |
// our Tab Views | |
Icon(_icons[0]), | |
Icon(_icons[1]), | |
Icon(_icons[2]), | |
Icon(_icons[3]), | |
Icon(_icons[4]), | |
Icon(_icons[5]) | |
], | |
)), | |
])); | |
} | |
// runs during the switching tabs animation | |
_handleTabAnimation() { | |
// gets the value of the animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5 | |
_aniValue = _controller.animation.value; | |
// if the button wasn't pressed, which means the user is swiping, and the amount swipped is less than 1 (this means that we're swiping through neighbor Tab Views) | |
if (!_buttonTap && ((_aniValue - _prevAniValue).abs() < 1)) { | |
// set the current tab index | |
_setCurrentIndex(_aniValue.round()); | |
} | |
// save the previous Animation Value | |
_prevAniValue = _aniValue; | |
} | |
// runs when the displayed tab changes | |
_handleTabChange() { | |
// if a button was tapped, change the current index | |
if (_buttonTap) _setCurrentIndex(_controller.index); | |
// this resets the button tap | |
if ((_controller.index == _prevControllerIndex) || | |
(_controller.index == _aniValue.round())) _buttonTap = false; | |
// save the previous controller index | |
_prevControllerIndex = _controller.index; | |
} | |
_setCurrentIndex(int index) { | |
// if we're actually changing the index | |
if (index != _currentIndex) { | |
setState(() { | |
// change the index | |
_currentIndex = index; | |
}); | |
// trigger the button animation | |
_triggerAnimation(); | |
// scroll the TabBar to the correct position (if we have a scrollable bar) | |
_scrollTo(index); | |
} | |
} | |
_triggerAnimation() { | |
// reset the animations so they're ready to go | |
_animationControllerOn.reset(); | |
_animationControllerOff.reset(); | |
// run the animations! | |
_animationControllerOn.forward(); | |
_animationControllerOff.forward(); | |
} | |
_scrollTo(int index) { | |
// get the screen width. This is used to check if we have an element off screen | |
double screenWidth = MediaQuery.of(context).size.width; | |
// get the button we want to scroll to | |
RenderBox renderBox = _keys[index].currentContext.findRenderObject(); | |
// get its size | |
double size = renderBox.size.width; | |
// and position | |
double position = renderBox.localToGlobal(Offset.zero).dx; | |
// this is how much the button is away from the center of the screen and how much we must scroll to get it into place | |
double offset = (position + size / 2) - screenWidth / 2; | |
// if the button is to the left of the middle | |
if (offset < 0) { | |
// get the first button | |
renderBox = _keys[0].currentContext.findRenderObject(); | |
// get the position of the first button of the TabBar | |
position = renderBox.localToGlobal(Offset.zero).dx; | |
// if the offset pulls the first button away from the left side, we limit that movement so the first button is stuck to the left side | |
if (position > offset) offset = position; | |
} else { | |
// if the button is to the right of the middle | |
// get the last button | |
renderBox = _keys[_icons.length - 1].currentContext.findRenderObject(); | |
// get its position | |
position = renderBox.localToGlobal(Offset.zero).dx; | |
// and size | |
size = renderBox.size.width; | |
// if the last button doesn't reach the right side, use it's right side as the limit of the screen for the TabBar | |
if (position + size < screenWidth) screenWidth = position + size; | |
// if the offset pulls the last button away from the right side limit, we reduce that movement so the last button is stuck to the right side limit | |
if (position + size - offset < screenWidth) { | |
offset = position + size - screenWidth; | |
} | |
} | |
// scroll the calculated ammount | |
_scrollController.animateTo(offset + _scrollController.offset, | |
duration: new Duration(milliseconds: 150), curve: Curves.easeInOut); | |
} | |
_getBackgroundColor(int index) { | |
if (index == _currentIndex) { | |
// if it's active button | |
return _colorTweenBackgroundOn.value; | |
} else if (index == _prevControllerIndex) { | |
// if it's the previous active button | |
return _colorTweenBackgroundOff.value; | |
} else { | |
// if the button is inactive | |
return _backgroundOff; | |
} | |
} | |
_getForegroundColor(int index) { | |
// the same as the above | |
if (index == _currentIndex) { | |
return _colorTweenForegroundOn.value; | |
} else if (index == _prevControllerIndex) { | |
return _colorTweenForegroundOff.value; | |
} else { | |
return _foregroundOff; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment