Created
September 13, 2021 17:37
-
-
Save wendellrocha/862eecd585207d09085ebfa2fa65070c to your computer and use it in GitHub Desktop.
Circular Bottom Bar
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
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file | |
// for details. All rights reserved. Use of this source code is governed by a | |
// BSD-style license that can be found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: const MyHomePage(title: 'Flutter Demo Home Page'), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
final String title; | |
const MyHomePage({ | |
Key key, | |
@required this.title, | |
}) : super(key: key); | |
@override | |
_MyHomePageState createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
int selectedPos = 0; | |
double bottomNavBarHeight = 60; | |
List<TabItem> tabItems = List.of([ | |
new TabItem(Icons.home, "Home", Colors.blue, labelStyle: TextStyle(fontWeight: FontWeight.normal)), | |
new TabItem(Icons.search, "Search", Colors.orange, labelStyle: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)), | |
new TabItem(Icons.layers, "Reports", Colors.red), | |
new TabItem(Icons.notifications, "Notifications", Colors.cyan), | |
new TabItem(Icons.email, "Notifications", Colors.cyan), | |
new TabItem(Icons.ac_unit, "Notifications", Colors.cyan), | |
]); | |
CircularBottomNavigationController _navigationController; | |
@override | |
void initState() { | |
super.initState(); | |
_navigationController = new CircularBottomNavigationController(selectedPos); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Stack( | |
children: <Widget>[ | |
Padding(child: bodyContainer(), padding: EdgeInsets.only(bottom: bottomNavBarHeight),), | |
Align(alignment: Alignment.bottomCenter, child: bottomNav()) | |
], | |
), | |
); | |
} | |
Widget bodyContainer() { | |
Color selectedColor = tabItems[selectedPos].circleColor; | |
String slogan; | |
switch (selectedPos) { | |
case 0: | |
slogan = "Familly, Happiness, Food"; | |
break; | |
case 1: | |
slogan = "Find, Check, Use"; | |
break; | |
case 2: | |
slogan = "Receive, Review, Rip"; | |
break; | |
case 3: | |
slogan = "Noise, Panic, Ignore"; | |
break; | |
} | |
return GestureDetector( | |
child: Container( | |
width: double.infinity, | |
height: double.infinity, | |
color: selectedColor, | |
child: Center( | |
child: Text( | |
slogan, | |
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20), | |
), | |
), | |
), | |
onTap: () { | |
if (_navigationController.value == tabItems.length - 1) { | |
_navigationController.value = 0; | |
} else { | |
_navigationController.value++; | |
} | |
}, | |
); | |
} | |
Widget bottomNav() { | |
return CircularBottomNavigation( | |
tabItems, | |
controller: _navigationController, | |
barHeight: bottomNavBarHeight, | |
barBackgroundColor: Colors.white, | |
animationDuration: Duration(milliseconds: 300), | |
selectedCallback: (int selectedPos) { | |
setState(() { | |
this.selectedPos = selectedPos; | |
print(_navigationController.value); | |
}); | |
}, | |
); | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
_navigationController.dispose(); | |
} | |
} | |
typedef CircularBottomNavSelectedCallback =Function(int selectedPos); | |
class CircularBottomNavigation extends StatefulWidget { | |
final List<TabItem> tabItems; | |
final int selectedPos; | |
final double barHeight; | |
final Color barBackgroundColor; | |
final double circleSize; | |
final double circleStrokeWidth; | |
final double iconsSize; | |
final Color selectedIconColor; | |
final Color normalIconColor; | |
final Duration animationDuration; | |
final CircularBottomNavSelectedCallback selectedCallback; | |
final CircularBottomNavigationController controller; | |
CircularBottomNavigation(this.tabItems, | |
{this.selectedPos = 0, | |
this.barHeight = 60, | |
this.barBackgroundColor = Colors.white, | |
this.circleSize = 58, | |
this.circleStrokeWidth = 4, | |
this.iconsSize = 32, | |
this.selectedIconColor = Colors.white, | |
this.normalIconColor = Colors.grey, | |
this.animationDuration = const Duration(milliseconds: 300), | |
this.selectedCallback, | |
this.controller}) | |
: assert(tabItems != null && tabItems.length != 0, "tabItems is required"); | |
@override | |
State<StatefulWidget> createState() => _CircularBottomNavigationState(); | |
} | |
class _CircularBottomNavigationState extends State<CircularBottomNavigation> | |
with TickerProviderStateMixin { | |
Curve _animationsCurve = Cubic(0.27, 1.21, .77, 1.09); | |
AnimationController itemsController; | |
Animation<double> selectedPosAnimation; | |
Animation<double> itemsAnimation; | |
List<double> _itemsSelectedState; | |
int selectedPos; | |
int previousSelectedPos; | |
CircularBottomNavigationController _controller; | |
@override | |
void initState() { | |
super.initState(); | |
if (widget.controller != null) { | |
_controller = widget.controller; | |
previousSelectedPos = selectedPos = _controller.value; | |
} else { | |
previousSelectedPos = selectedPos = widget.selectedPos; | |
_controller = CircularBottomNavigationController(selectedPos); | |
} | |
_controller.addListener(_newSelectedPosNotify); | |
_itemsSelectedState = List.generate(widget.tabItems.length, (index) { | |
return selectedPos == index ? 1.0 : 0.0; | |
}); | |
itemsController = new AnimationController(vsync: this, duration: widget.animationDuration); | |
itemsController.addListener(() { | |
setState(() { | |
_itemsSelectedState.asMap().forEach((i, value) { | |
if (i == previousSelectedPos) { | |
_itemsSelectedState[previousSelectedPos] = 1.0 - itemsAnimation.value; | |
} else if (i == selectedPos) { | |
_itemsSelectedState[selectedPos] = itemsAnimation.value; | |
} else { | |
_itemsSelectedState[i] = 0.0; | |
} | |
}); | |
}); | |
}); | |
selectedPosAnimation = | |
makeSelectedPosAnimation(selectedPos.toDouble(), selectedPos.toDouble()); | |
itemsAnimation = Tween(begin: 0.0, end: 1.0) | |
.animate(CurvedAnimation(parent: itemsController, curve: _animationsCurve)); | |
} | |
Animation<double> makeSelectedPosAnimation(double begin, double end) { | |
return Tween(begin: begin, end: end) | |
.animate(CurvedAnimation(parent: itemsController, curve: _animationsCurve)); | |
} | |
void onSelectedPosAnimate() { | |
setState(() {}); | |
} | |
void _newSelectedPosNotify() { | |
_setSelectedPos(widget.controller.value); | |
} | |
@override | |
Widget build(BuildContext context) { | |
double fullWidth = MediaQuery | |
.of(context) | |
.size | |
.width; | |
double fullHeight = widget.barHeight + (widget.circleSize / 2) + widget.circleStrokeWidth; | |
double sectionsWidth = fullWidth / widget.tabItems.length; | |
//Create the boxes Rect | |
List<Rect> boxes = List(); | |
widget.tabItems.asMap().forEach((i, tabItem) { | |
double left = i * sectionsWidth; | |
double top = fullHeight - widget.barHeight; | |
double right = left + sectionsWidth; | |
double bottom = fullHeight; | |
boxes.add(Rect.fromLTRB(left, top, right, bottom)); | |
}); | |
List<Widget> children = List(); | |
// This is the full view transparent background (have free space for circle) | |
children.add(Container( | |
width: fullWidth, | |
height: fullHeight, | |
)); | |
// This is the bar background (bottom section of our view) | |
children.add(Positioned( | |
child: Container( | |
width: fullWidth, | |
height: widget.barHeight, | |
decoration: BoxDecoration( | |
shape: BoxShape.rectangle, | |
color: widget.barBackgroundColor, | |
boxShadow: [new BoxShadow(color: Colors.grey, blurRadius: 2.0)]), | |
), | |
top: fullHeight - widget.barHeight, | |
left: 0, | |
)); | |
// This is the circle handle | |
children.add(new Positioned( | |
child: Container( | |
width: widget.circleSize, | |
height: widget.circleSize, | |
child: Stack( | |
children: <Widget>[ | |
Container( | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: widget.barBackgroundColor), | |
), | |
Container( | |
margin: EdgeInsets.all(widget.circleStrokeWidth), | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: widget.tabItems[selectedPos].circleColor), | |
), | |
], | |
), | |
), | |
left: (selectedPosAnimation.value * sectionsWidth) + | |
(sectionsWidth / 2) - | |
(widget.circleSize / 2), | |
top: 0, | |
)); | |
//Here are the Icons and texts of items | |
boxes.asMap().forEach((int pos, Rect r) { | |
// Icon | |
Color iconColor = | |
pos == selectedPos ? widget.selectedIconColor : widget.normalIconColor; | |
double scaleFactor = pos == selectedPos ? 1.2 : 1.0; | |
children.add( | |
Positioned( | |
child: Transform.scale( | |
scale: scaleFactor, | |
child: Icon( | |
widget.tabItems[pos].icon, | |
size: widget.iconsSize, | |
color: iconColor, | |
), | |
), | |
left: r.center.dx - (widget.iconsSize / 2), | |
top: r.center.dy - | |
(widget.iconsSize / 2) - | |
(_itemsSelectedState[pos] * ((widget.barHeight / 2) + widget.circleStrokeWidth)), | |
), | |
); | |
// Text | |
double textHeight = fullHeight - widget.circleSize; | |
double opacity = _itemsSelectedState[pos]; | |
if (opacity < 0.0) { | |
opacity = 0.0; | |
} else if (opacity > 1.0) { | |
opacity = 1.0; | |
} | |
children.add(Positioned( | |
child: Container( | |
width: r.width, | |
height: textHeight, | |
child: Center( | |
child: Opacity( | |
opacity: opacity, | |
child: Text( | |
widget.tabItems[pos].title, | |
textAlign: TextAlign.center, | |
style: widget.tabItems[pos].labelStyle, | |
), | |
)), | |
), | |
left: r.left, | |
top: r.top + | |
(widget.circleSize / 2) - | |
(widget.circleStrokeWidth * 2) + | |
((1.0 - _itemsSelectedState[pos]) * textHeight), | |
)); | |
if (pos != selectedPos) { | |
children.add(Positioned.fromRect( | |
child: GestureDetector( | |
onTap: () { | |
_controller.value = pos; | |
}, | |
), | |
rect: r, | |
)); | |
} | |
}); | |
return Stack( | |
children: children, | |
); | |
} | |
void _setSelectedPos(int pos) { | |
previousSelectedPos = selectedPos; | |
selectedPos = pos; | |
itemsController.forward(from: 0.0); | |
selectedPosAnimation = makeSelectedPosAnimation( | |
previousSelectedPos.toDouble(), selectedPos.toDouble()); | |
selectedPosAnimation.addListener(onSelectedPosAnimate); | |
if (widget.selectedCallback != null) { | |
widget.selectedCallback(selectedPos); | |
} | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
itemsController.dispose(); | |
_controller.removeListener(_newSelectedPosNotify); | |
} | |
} | |
class CircularBottomNavigationController extends ValueNotifier<int> { | |
CircularBottomNavigationController(int value) : super(value); | |
} | |
class TabItem { | |
IconData icon; | |
String title; | |
Color circleColor; | |
TextStyle labelStyle; | |
TabItem(this.icon, this.title, this.circleColor, {this.labelStyle = const TextStyle(fontWeight: FontWeight.bold)}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment