Skip to content

Instantly share code, notes, and snippets.

@wendellrocha
Created September 13, 2021 17:37
Show Gist options
  • Save wendellrocha/862eecd585207d09085ebfa2fa65070c to your computer and use it in GitHub Desktop.
Save wendellrocha/862eecd585207d09085ebfa2fa65070c to your computer and use it in GitHub Desktop.
Circular Bottom Bar
// 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