Last active
September 4, 2022 15:09
-
-
Save kaboc/14c0fba024f187aeec9d4c0d2dce883b to your computer and use it in GitHub Desktop.
Floating bottom navigation bar (DartPad: https://dartpad.dev/14c0fba024f187aeec9d4c0d2dce883b)
This file contains hidden or 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
| // A floating bottom navigation bar for flutter inspired by a screen cast of | |
| // a similar one for React Native that I saw on Twitter. The original source | |
| // is probably this post: | |
| // https://hackernoon.com/how-we-created-tabbar-plugin-with-react-native-c00de2337f22 | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/material.dart'; | |
| void main() => runApp(const App()); | |
| class App extends StatefulWidget { | |
| const App(); | |
| @override | |
| _AppState createState() => _AppState(); | |
| } | |
| class _AppState extends State<App> { | |
| final _controller = PageController(); | |
| @override | |
| void dispose() { | |
| _controller.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| home: Scaffold( | |
| body: PageView( | |
| controller: _controller, | |
| children: <Widget>[ | |
| ColoredBox(color: Colors.blueGrey.shade100), | |
| _image('https://picsum.photos/id/788/400/800'), | |
| _image('https://picsum.photos/id/791/400/800'), | |
| ], | |
| ), | |
| extendBody: true, | |
| bottomNavigationBar: FloatingBottomBar( | |
| controller: _controller, | |
| items: [ | |
| FloatingBottomBarItem(Icons.library_books, label: 'Page1'), | |
| FloatingBottomBarItem(Icons.add_box, label: 'Page2'), | |
| FloatingBottomBarItem(Icons.business, label: 'Page3'), | |
| ], | |
| activeItemColor: Colors.green.shade700, | |
| enableIconRotation: true, | |
| onTap: (index) { | |
| print('Tapped: item $index'); | |
| _controller.animateToPage( | |
| index, | |
| duration: const Duration(milliseconds: 400), | |
| curve: Curves.easeOut, | |
| ); | |
| }, | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _image(String url) { | |
| return Image.network( | |
| url, | |
| loadingBuilder: (_, child, progress) => progress == null | |
| ? FittedBox(fit: BoxFit.cover, child: child) | |
| : const Center(child: CircularProgressIndicator()), | |
| ); | |
| } | |
| } | |
| //========================================================================= | |
| const _kMargin = 14.0; | |
| const _kHeight = 62.0; | |
| const _kCircleRadius = 30.0; | |
| const _kCircleMargin = 8.0; | |
| const _kTopRadius = 10.0; | |
| const _kBottomRadius = 28.0; | |
| const _kItemSize = 30.0; | |
| const _kPi = 3.1415926535897932; | |
| class FloatingBottomBarItem { | |
| const FloatingBottomBarItem(this.iconData, {this.label}); | |
| final IconData iconData; | |
| final String label; | |
| } | |
| class FloatingBottomBar extends StatefulWidget { | |
| const FloatingBottomBar({ | |
| @required this.controller, | |
| @required this.items, | |
| @required this.onTap, | |
| this.color, | |
| this.itemColor, | |
| this.activeItemColor, | |
| this.enableIconRotation, | |
| }); | |
| final PageController controller; | |
| final List<FloatingBottomBarItem> items; | |
| final ValueChanged<int> onTap; | |
| final Color color; | |
| final Color itemColor; | |
| final Color activeItemColor; | |
| final bool enableIconRotation; | |
| @override | |
| _FloatingBottomBarState createState() => _FloatingBottomBarState(); | |
| } | |
| class _FloatingBottomBarState extends State<FloatingBottomBar> { | |
| double _screenWidth; | |
| @override | |
| void didChangeDependencies() { | |
| super.didChangeDependencies(); | |
| _screenWidth = MediaQuery.of(context).size.width; | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final size = MediaQuery.of(context).size; | |
| final width = size.width; | |
| const height = _kHeight + _kMargin * 2; | |
| return AnimatedBuilder( | |
| animation: widget.controller, | |
| builder: (_, child) { | |
| var scrollPosition = 0.0; | |
| var currentIndex = 0; | |
| if (widget.controller?.hasClients ?? false) { | |
| scrollPosition = widget.controller.page; | |
| currentIndex = (widget.controller.page + 0.5).toInt(); | |
| } | |
| return Stack( | |
| overflow: Overflow.visible, | |
| children: <Widget>[ | |
| CustomPaint( | |
| size: Size(width, height), | |
| painter: _Painter( | |
| x: _itemXByScrollPosition(scrollPosition), | |
| color: widget.color, | |
| ), | |
| ), | |
| for (var i = 0; i < widget.items.length; i++) ...[ | |
| if (i == currentIndex) | |
| Positioned( | |
| top: _kMargin - _kCircleRadius + 8.0, | |
| left: _kCircleMargin + _itemXByScrollPosition(scrollPosition), | |
| child: _ActiveItem( | |
| i, | |
| iconData: widget.items[i].iconData, | |
| color: widget.activeItemColor, | |
| scrollPosition: scrollPosition, | |
| enableRotation: widget.enableIconRotation, | |
| onTap: widget.onTap, | |
| ), | |
| ), | |
| if (i != currentIndex) | |
| Positioned( | |
| top: _kMargin + (_kHeight - _kCircleRadius * 2) / 2, | |
| left: _kCircleMargin + _itemXByIndex(i), | |
| child: _Item( | |
| i, | |
| iconData: widget.items[i].iconData, | |
| label: widget.items[i].label, | |
| color: widget.itemColor, | |
| onTap: widget.onTap, | |
| ), | |
| ), | |
| ], | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| double _firstItemX() { | |
| return _kMargin + (_screenWidth - _kMargin * 2) * 0.1; | |
| } | |
| double _lastItemX() { | |
| return _screenWidth - | |
| _kMargin - | |
| (_screenWidth - _kMargin * 2) * 0.1 - | |
| (_kCircleRadius + _kCircleMargin) * 2; | |
| } | |
| double _itemDistance() { | |
| return (_lastItemX() - _firstItemX()) / (widget.items.length - 1); | |
| } | |
| double _itemXByScrollPosition(double scrollPosition) { | |
| return _firstItemX() + _itemDistance() * scrollPosition; | |
| } | |
| double _itemXByIndex(int index) { | |
| return _firstItemX() + _itemDistance() * index; | |
| } | |
| } | |
| class _Item extends StatelessWidget { | |
| const _Item(this.index, {this.iconData, this.label, this.color, this.onTap}); | |
| final int index; | |
| final IconData iconData; | |
| final String label; | |
| final Color color; | |
| final ValueChanged<int> onTap; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Padding( | |
| padding: const EdgeInsets.all(0.0), | |
| child: SizedBox.fromSize( | |
| size: const Size(_kCircleRadius * 2, _kCircleRadius * 2), | |
| child: RawMaterialButton( | |
| shape: const CircleBorder(), | |
| padding: const EdgeInsets.all(0.0), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: <Widget>[ | |
| Icon( | |
| iconData, | |
| size: _kItemSize - 4, | |
| color: color ?? Colors.grey.shade700, | |
| ), | |
| if (label != null) ...[ | |
| const SizedBox(height: 3.0), | |
| Text( | |
| label, | |
| style: TextStyle( | |
| color: color ?? Colors.grey.shade700, | |
| fontSize: 12.0, | |
| ), | |
| ), | |
| ], | |
| ], | |
| ), | |
| onPressed: () => onTap(index), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class _ActiveItem extends StatelessWidget { | |
| const _ActiveItem( | |
| this.index, { | |
| this.iconData, | |
| this.color, | |
| this.scrollPosition, | |
| this.enableRotation, | |
| this.onTap, | |
| }); | |
| final int index; | |
| final IconData iconData; | |
| final Color color; | |
| final double scrollPosition; | |
| final bool enableRotation; | |
| final ValueChanged<int> onTap; | |
| @override | |
| Widget build(BuildContext context) { | |
| final icon = Icon( | |
| iconData, | |
| size: _kItemSize, | |
| color: color ?? Colors.grey.shade700, | |
| ); | |
| return InkWell( | |
| child: SizedBox.fromSize( | |
| size: const Size(_kCircleRadius * 2, _kCircleRadius * 2), | |
| child: enableRotation ?? false | |
| ? Transform.rotate( | |
| angle: _kPi * 2 * (scrollPosition % 1), | |
| child: icon, | |
| ) | |
| : icon, | |
| ), | |
| onTap: () => onTap(index), | |
| ); | |
| } | |
| } | |
| class _Painter extends CustomPainter { | |
| _Painter({@required this.x, this.color}) | |
| : _paint = Paint() | |
| ..color = color ?? Colors.white | |
| ..isAntiAlias = true, | |
| _shadowColor = | |
| kIsWeb ? Colors.grey.shade600 : Colors.grey.withOpacity(0.4); | |
| final double x; | |
| final Color color; | |
| final Paint _paint; | |
| final Color _shadowColor; | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| _drawBar(canvas, size); | |
| _drawCircle(canvas); | |
| } | |
| @override | |
| bool shouldRepaint(_Painter oldDelegate) { | |
| return x != oldDelegate.x || color != oldDelegate.color; | |
| } | |
| void _drawBar(Canvas canvas, Size size) { | |
| const left = _kMargin; | |
| final right = size.width - _kMargin; | |
| const top = _kMargin; | |
| const bottom = top + _kHeight; | |
| final path = Path() | |
| ..moveTo(left + _kTopRadius, top) | |
| ..lineTo(x - _kTopRadius, top) | |
| ..relativeArcToPoint( | |
| const Offset(_kTopRadius, _kTopRadius), | |
| radius: const Radius.circular(_kTopRadius), | |
| ) | |
| ..relativeArcToPoint( | |
| const Offset((_kCircleRadius + _kCircleMargin) * 2, 0.0), | |
| radius: const Radius.circular(_kCircleRadius + _kCircleMargin), | |
| clockwise: false, | |
| ) | |
| ..relativeArcToPoint( | |
| const Offset(_kTopRadius, -_kTopRadius), | |
| radius: const Radius.circular(_kTopRadius), | |
| ) | |
| ..lineTo(right - _kTopRadius, top) | |
| ..relativeArcToPoint( | |
| const Offset(_kTopRadius, _kTopRadius), | |
| radius: const Radius.circular(_kTopRadius), | |
| ) | |
| ..lineTo(right, bottom - _kBottomRadius) | |
| ..relativeArcToPoint( | |
| const Offset(-_kBottomRadius, _kBottomRadius), | |
| radius: const Radius.circular(_kBottomRadius), | |
| ) | |
| ..lineTo(left + _kBottomRadius, bottom) | |
| ..relativeArcToPoint( | |
| const Offset(-_kBottomRadius, -_kBottomRadius), | |
| radius: const Radius.circular(_kBottomRadius), | |
| ) | |
| ..lineTo(left, top + _kTopRadius) | |
| ..relativeArcToPoint( | |
| const Offset(_kTopRadius, -_kTopRadius), | |
| radius: const Radius.circular(_kTopRadius), | |
| ); | |
| canvas | |
| ..drawShadow(path, _shadowColor, 5.0, false) | |
| ..drawPath(path, _paint); | |
| } | |
| void _drawCircle(Canvas canvas) { | |
| final path = Path() | |
| ..addArc( | |
| Rect.fromCircle( | |
| center: Offset( | |
| x + _kCircleMargin + _kCircleRadius, | |
| _kMargin + _kCircleMargin, | |
| ), | |
| radius: _kCircleRadius, | |
| ), | |
| 0, | |
| _kPi * 2, | |
| ); | |
| canvas | |
| ..drawShadow(path, _shadowColor, 3.0, false) | |
| ..drawPath(path, _paint); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment