├── motiontabbar.gif ├── motiontabbar_v2.gif ├── motiontabbar_v2.1.gif ├── lib ├── helpers │ ├── HalfClipper.dart │ └── HalfPainter.dart ├── MotionTabBarController.dart ├── MotionBadgeWidget.dart ├── MotionTabItem.dart └── MotionTabBar.dart ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── pubspec.yaml ├── .gitignore ├── pubspec.lock ├── README.md └── example └── main.dart /motiontabbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/therezacuet/Motion-Tab-Bar/HEAD/motiontabbar.gif -------------------------------------------------------------------------------- /motiontabbar_v2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/therezacuet/Motion-Tab-Bar/HEAD/motiontabbar_v2.gif -------------------------------------------------------------------------------- /motiontabbar_v2.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/therezacuet/Motion-Tab-Bar/HEAD/motiontabbar_v2.1.gif -------------------------------------------------------------------------------- /lib/helpers/HalfClipper.dart: -------------------------------------------------------------------------------- 1 | library motiontabbar; 2 | import 'package:flutter/material.dart'; 3 | 4 | class HalfClipper extends CustomClipper { 5 | @override 6 | Rect getClip(Size size) => Rect.fromLTWH(0, 0, size.width, size.height / 2); 7 | 8 | @override 9 | bool shouldReclip(CustomClipper oldClipper) => true; 10 | } -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /lib/MotionTabBarController.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MotionTabBarController extends TabController { 4 | MotionTabBarController({ 5 | int initialIndex = 0, 6 | Duration? animationDuration, 7 | required int length, 8 | required TickerProvider vsync, 9 | }) : super(initialIndex: initialIndex, animationDuration: animationDuration, length: length, vsync: vsync); 10 | 11 | // programmatic tab change 12 | set index(int index) { 13 | super.index = index; 14 | _changeIndex!(index); 15 | } 16 | 17 | // callback for tab change 18 | Function(int)? _changeIndex; 19 | set onTabChange(Function(int)? fx) { 20 | _changeIndex = fx; 21 | } 22 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.4] 2 | 3 | * Fix flutter run time issue 4 | 5 | ## [2.0.3] 6 | 7 | * Support RTL 8 | 9 | ## [2.0.2] 10 | 11 | * Fully compatible with the latest Flutter 3, ensuring a seamless development experience. 12 | * Addressed and squashed pesky bugs to enhance overall stability. 13 | * Added exciting features like badge support and improved customization options. 14 | 15 | ## [0.1.5] 16 | 17 | * Remove extra space from top of tab bar. 18 | 19 | ## [0.1.4] 20 | 21 | * Tab Item Dynamic. Now you can use multiple tab. 22 | * Tab Item Animation Delay fix 23 | 24 | ## [0.1.3] 25 | 26 | * Controller Issue fix. 27 | * Tab Item Animation Delay fix 28 | 29 | ## [0.1.2] 30 | 31 | * Fix Initial Index Issue. 32 | * Fix Initial Tab Selected Issue. 33 | 34 | ## [0.1.0] 35 | 36 | * Initial release. 37 | * Tab menu with interaction design. 38 | * An animated Bottom Navigation Bar for Flutter apps, icon animates into place, colors are customizable. 39 | -------------------------------------------------------------------------------- /lib/helpers/HalfPainter.dart: -------------------------------------------------------------------------------- 1 | library motiontabbar; 2 | import 'package:flutter/material.dart'; 3 | 4 | class HalfPainter extends CustomPainter { 5 | final Color? color; 6 | HalfPainter({this.color}); 7 | 8 | @override 9 | void paint(Canvas canvas, Size size) { 10 | final double curveSize = 10; 11 | final double xStartingPos = 0; 12 | final double yStartingPos = (size.height / 2); 13 | final double yMaxPos = yStartingPos - curveSize; 14 | final path = Path(); 15 | path.moveTo(xStartingPos, yStartingPos); 16 | path.lineTo(size.width - xStartingPos, yStartingPos); 17 | path.quadraticBezierTo(size.width - (curveSize), yStartingPos, size.width - (curveSize + 5), yMaxPos); 18 | path.lineTo(xStartingPos + (curveSize + 5), yMaxPos); 19 | path.quadraticBezierTo(xStartingPos + (curveSize), yStartingPos, xStartingPos, yStartingPos); 20 | path.close(); 21 | canvas.drawPath(path, Paint()..color = color ?? Colors.white); 22 | } 23 | 24 | @override 25 | bool shouldRepaint(CustomPainter oldDelegate) => true; 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rezaul Islam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: motion_tab_bar 2 | description: An animated Bottom Navigation Bar for Flutter apps, icon animates into place, colors are customizable. 3 | version: 2.0.4 4 | homepage: https://github.com/therezacuet/Motion-Tab-Bar 5 | 6 | environment: 7 | sdk: '>=2.12.0 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | vector_math: ^2.1.0 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.dev/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /lib/MotionBadgeWidget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MotionBadgeWidget extends StatelessWidget { 4 | const MotionBadgeWidget({ 5 | Key? key, 6 | bool? isIndicator, 7 | this.text, 8 | Color? textColor, 9 | double? size, 10 | Color? color, 11 | bool? disabled, 12 | bool? show, 13 | }) : this._isIndicator = isIndicator ?? false, 14 | this._color = color ?? Colors.red, 15 | this._textColor = textColor ?? Colors.white, 16 | this._size = size ?? (isIndicator == true ? 5 : 18), 17 | this._disabled = disabled ?? false, 18 | this._show = show != null ? show : true, 19 | assert(text != null ? text.length <= 3 : true), 20 | super(key: key); 21 | 22 | final bool? _isIndicator; 23 | final String? text; 24 | final Color? _color; 25 | final Color? _textColor; 26 | final double? _size; 27 | final bool? _disabled; 28 | final bool? _show; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return _show == true && _isIndicator == true 33 | ? Container( 34 | alignment: Alignment.center, 35 | padding: EdgeInsets.all(3), 36 | margin: EdgeInsets.all(7), 37 | decoration: new BoxDecoration( 38 | color: _disabled == false ? _color : _color!.withOpacity(0.6), 39 | borderRadius: BorderRadius.circular(_size! / 2), 40 | ), 41 | constraints: BoxConstraints( 42 | minWidth: _size!, 43 | minHeight: _size!, 44 | ), 45 | ) 46 | : _show == true && text != null && text != '' 47 | ? Container( 48 | alignment: Alignment.center, 49 | padding: EdgeInsets.all(3), 50 | decoration: new BoxDecoration( 51 | color: _disabled == false ? _color : _color!.withOpacity(0.6), 52 | borderRadius: BorderRadius.circular(_size! / 2), 53 | ), 54 | constraints: BoxConstraints( 55 | minWidth: _size!, 56 | minHeight: _size!, 57 | ), 58 | child: Text( 59 | '$text', 60 | style: TextStyle( 61 | color: _textColor, 62 | fontSize: _size != null ? (_size! / 2) : 9, 63 | fontWeight: FontWeight.w600, 64 | letterSpacing: 0.5, 65 | ), 66 | textAlign: TextAlign.center, 67 | ), 68 | ) 69 | : Container(); 70 | } 71 | } -------------------------------------------------------------------------------- /lib/MotionTabItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const double ICON_OFF = -3; 4 | const double ICON_ON = 0; 5 | const double TEXT_OFF = 3; 6 | const double TEXT_ON = 1; 7 | const double ALPHA_OFF = 0; 8 | const double ALPHA_ON = 1; 9 | const int ANIM_DURATION = 300; 10 | 11 | class MotionTabItem extends StatefulWidget { 12 | final String? title; 13 | final bool selected; 14 | final IconData? iconData; 15 | final TextStyle textStyle; 16 | final Function callbackFunction; 17 | final Color tabIconColor; 18 | final double? tabIconSize; 19 | final Widget? badge; 20 | 21 | MotionTabItem({ 22 | required this.title, 23 | required this.selected, 24 | required this.iconData, 25 | required this.textStyle, 26 | required this.tabIconColor, 27 | required this.callbackFunction, 28 | this.tabIconSize = 24, 29 | this.badge, 30 | }); 31 | 32 | @override 33 | _MotionTabItemState createState() => _MotionTabItemState(); 34 | } 35 | 36 | class _MotionTabItemState extends State { 37 | double iconYAlign = ICON_ON; 38 | double textYAlign = TEXT_OFF; 39 | double iconAlpha = ALPHA_ON; 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | _setIconTextAlpha(); 45 | } 46 | 47 | @override 48 | void didUpdateWidget(MotionTabItem oldWidget) { 49 | super.didUpdateWidget(oldWidget); 50 | _setIconTextAlpha(); 51 | } 52 | 53 | _setIconTextAlpha() { 54 | setState(() { 55 | iconYAlign = (widget.selected) ? ICON_OFF : ICON_ON; 56 | textYAlign = (widget.selected) ? TEXT_ON : TEXT_OFF; 57 | iconAlpha = (widget.selected) ? ALPHA_OFF : ALPHA_ON; 58 | }); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Expanded( 64 | child: Stack( 65 | fit: StackFit.expand, 66 | children: [ 67 | Container( 68 | height: double.infinity, 69 | width: double.infinity, 70 | alignment: Alignment.center, 71 | child: AnimatedAlign( 72 | duration: Duration(milliseconds: ANIM_DURATION), 73 | alignment: Alignment(0, textYAlign), 74 | child: Padding( 75 | padding: const EdgeInsets.all(8.0), 76 | child: widget.selected 77 | ? Text( 78 | widget.title!, 79 | style: widget.textStyle, 80 | softWrap: false, 81 | maxLines: 1, 82 | textAlign: TextAlign.center, 83 | ) 84 | : Text(''), 85 | ), 86 | ), 87 | ), 88 | InkWell( 89 | onTap: () => widget.callbackFunction(), 90 | child: Container( 91 | height: double.infinity, 92 | width: double.infinity, 93 | child: AnimatedAlign( 94 | duration: Duration(milliseconds: ANIM_DURATION), 95 | curve: Curves.easeIn, 96 | alignment: Alignment(0, iconYAlign), 97 | child: AnimatedOpacity( 98 | duration: Duration(milliseconds: ANIM_DURATION), 99 | opacity: iconAlpha, 100 | child: Stack( 101 | alignment: Alignment.center, 102 | children: [ 103 | IconButton( 104 | highlightColor: Colors.transparent, 105 | splashColor: Colors.transparent, 106 | padding: EdgeInsets.all(0), 107 | alignment: Alignment(0, 0), 108 | icon: Icon( 109 | widget.iconData, 110 | color: widget.tabIconColor, 111 | size: widget.tabIconSize, 112 | ), 113 | onPressed: () => widget.callbackFunction(), 114 | ), 115 | widget.badge != null 116 | ? Positioned( 117 | top: 0, 118 | right: 0, 119 | child: widget.badge!, 120 | ) 121 | : SizedBox(), 122 | ], 123 | ), 124 | ), 125 | ), 126 | ), 127 | ) 128 | ], 129 | ), 130 | ); 131 | } 132 | } -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.1" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | js: 63 | dependency: transitive 64 | description: 65 | name: js 66 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "0.6.7" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "0.12.15" 78 | material_color_utilities: 79 | dependency: transitive 80 | description: 81 | name: material_color_utilities 82 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "0.2.0" 86 | meta: 87 | dependency: transitive 88 | description: 89 | name: meta 90 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "1.9.1" 94 | path: 95 | dependency: transitive 96 | description: 97 | name: path 98 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "1.8.3" 102 | sky_engine: 103 | dependency: transitive 104 | description: flutter 105 | source: sdk 106 | version: "0.0.99" 107 | source_span: 108 | dependency: transitive 109 | description: 110 | name: source_span 111 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 112 | url: "https://pub.dev" 113 | source: hosted 114 | version: "1.9.1" 115 | stack_trace: 116 | dependency: transitive 117 | description: 118 | name: stack_trace 119 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 120 | url: "https://pub.dev" 121 | source: hosted 122 | version: "1.11.0" 123 | stream_channel: 124 | dependency: transitive 125 | description: 126 | name: stream_channel 127 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "2.1.1" 131 | string_scanner: 132 | dependency: transitive 133 | description: 134 | name: string_scanner 135 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.2.0" 139 | term_glyph: 140 | dependency: transitive 141 | description: 142 | name: term_glyph 143 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "1.2.1" 147 | test_api: 148 | dependency: transitive 149 | description: 150 | name: test_api 151 | sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "0.5.1" 155 | vector_math: 156 | dependency: "direct main" 157 | description: 158 | name: vector_math 159 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "2.1.4" 163 | sdks: 164 | dart: ">=3.0.0-0 <4.0.0" 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Motion Tab Bar compatible with Flutter 3 and RTL support 2 | 3 | [![pub package](https://img.shields.io/pub/v/motion_tab_bar)](https://pub.dev/packages/motion_tab_bar) 4 | [![likes](https://img.shields.io/pub/likes/motion_tab_bar)](https://pub.dev/packages/motion_tab_bar/score) 5 | [![popularity](https://img.shields.io/pub/popularity/motion_tab_bar)](https://pub.dev/packages/motion_tab_bar/score) 6 | [![pub points](https://img.shields.io/pub/points/motion_tab_bar)](https://pub.dev/packages/motion_tab_bar/score) 7 | 8 | A beautiful animated widget for your Flutter apps 9 | 10 | ![MotionTabBar Gif](https://github.com/therezacuet/Motion-Tab-Bar/blob/master/motiontabbar.gif?raw=true) 11 | 12 | | Without Badge | With Badge | 13 | | ------------- | ------------- | 14 | | ![MotionTabBar Gif](https://github.com/therezacuet/Motion-Tab-Bar/blob/master/motiontabbar_v2.gif?raw=true) | ![MotionTabBar Gif](https://github.com/therezacuet/Motion-Tab-Bar/blob/master/motiontabbar_v2.1.gif?raw=true) | 15 | 16 | ## Getting Started 17 | 18 | Add the plugin: 19 | 20 | ```yaml 21 | dependencies: 22 | motion_tab_bar: ^2.0.4 23 | ``` 24 | 25 | ## Basic Usage 26 | 27 | ### Use default TabController or MotionTabBarController: 28 | 29 | ```dart 30 | // TabController _tabController; 31 | MotionTabBarController? _motionTabBarController; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | 37 | //// Use normal tab controller 38 | // _tabController = TabController( 39 | // initialIndex: 1, 40 | // length: 4, 41 | // vsync: this, 42 | // ); 43 | 44 | //// use "MotionTabBarController" to replace with "TabController", if you need to programmatically change the tab 45 | _motionTabBarController = MotionTabBarController( 46 | initialIndex: 1, 47 | length: 4, 48 | vsync: this, 49 | ); 50 | } 51 | 52 | @override 53 | void dispose() { 54 | super.dispose(); 55 | 56 | // _tabController.dispose(); 57 | _motionTabBarController!.dispose(); 58 | } 59 | ``` 60 | 61 | ### Add Motion Tab Bar to Scaffold bottomNavigationBar: 62 | 63 | ```dart 64 | bottomNavigationBar: MotionTabBar( 65 | controller: _motionTabBarController, // ADD THIS if you need to change your tab programmatically 66 | initialSelectedTab: "Home", 67 | labels: const ["Dashboard", "Home", "Profile", "Settings"], 68 | icons: const [Icons.dashboard, Icons.home, Icons.people_alt, Icons.settings], 69 | 70 | // optional badges, length must be same with labels 71 | badges: [ 72 | // Default Motion Badge Widget 73 | const MotionBadgeWidget( 74 | text: '10+', 75 | textColor: Colors.white, // optional, default to Colors.white 76 | color: Colors.red, // optional, default to Colors.red 77 | size: 18, // optional, default to 18 78 | ), 79 | 80 | // custom badge Widget 81 | Container( 82 | color: Colors.black, 83 | padding: const EdgeInsets.all(2), 84 | child: const Text( 85 | '11', 86 | style: TextStyle( 87 | fontSize: 14, 88 | color: Colors.white, 89 | ), 90 | ), 91 | ), 92 | 93 | // allow null 94 | null, 95 | 96 | // Default Motion Badge Widget with indicator only 97 | const MotionBadgeWidget( 98 | isIndicator: true, 99 | color: Colors.blue, // optional, default to Colors.red 100 | size: 5, // optional, default to 5, 101 | show: true, // true / false 102 | ), 103 | ], 104 | tabSize: 50, 105 | tabBarHeight: 55, 106 | textStyle: const TextStyle( 107 | fontSize: 12, 108 | color: Colors.black, 109 | fontWeight: FontWeight.w500, 110 | ), 111 | tabIconColor: Colors.blue[600], 112 | tabIconSize: 28.0, 113 | tabIconSelectedSize: 26.0, 114 | tabSelectedColor: Colors.blue[900], 115 | tabIconSelectedColor: Colors.white, 116 | tabBarColor: Colors.white, 117 | onTabItemSelected: (int value) { 118 | setState(() { 119 | // _tabController!.index = value; 120 | _motionTabBarController!.index = value; 121 | }); 122 | }, 123 | ), 124 | ``` 125 | 126 | ### add TabBarView to Scaffold body 127 | 128 | ```dart 129 | body: TabBarView( 130 | physics: NeverScrollableScrollPhysics(), // swipe navigation handling is not supported 131 | // controller: _tabController, 132 | controller: _motionTabBarController, 133 | children: [ 134 | const Center( 135 | child: Text("Dashboard"), 136 | ), 137 | const Center( 138 | child: Text("Home"), 139 | ), 140 | const Center( 141 | child: Text("Profile"), 142 | ), 143 | const Center( 144 | child: Text("Settings"), 145 | ), 146 | ], 147 | ), 148 | ``` 149 | 150 | 151 | ### to change tabs programmatically 152 | ```dart 153 | ... 154 | 155 | ElevatedButton( 156 | // set MotionTabBarController index to new tab index 157 | onPressed: () => _motionTabBarController.index = 0, 158 | child: const Text('Dashboard Page'), 159 | ), 160 | ElevatedButton( 161 | // set MotionTabBarController index to new tab index 162 | onPressed: () => _motionTabBarController.index = 1, 163 | child: const Text('Home Page'), 164 | ), 165 | 166 | ... 167 | ``` 168 | 169 | 170 | Catch me up on **LinkedIn** @[Rezaul Islam](www.linkedin.com/in/therezacuet "Rezaul Islam") 171 | 172 | 💙 to Code👨🏽‍💻 Flutter Expert • Dart Kotlin Swift Node Js • Android • Full Stack Developer 173 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:motion_tab_bar/MotionBadgeWidget.dart'; 3 | import 'package:motion_tab_bar/MotionTabBar.dart'; 4 | import 'package:motion_tab_bar/MotionTabBarController.dart'; 5 | 6 | void main() => runApp(const MyApp()); 7 | 8 | class MyApp extends StatelessWidget { 9 | const MyApp({Key? key}) : super(key: key); 10 | 11 | // This widget is the root of your application. 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | title: 'Motion Tab Bar Sample', 16 | theme: ThemeData( 17 | primarySwatch: Colors.blue, 18 | ), 19 | home: const MyHomePage(title: 'Motion Tab Bar Sample'), 20 | ); 21 | } 22 | } 23 | 24 | class MyHomePage extends StatefulWidget { 25 | const MyHomePage({Key? key, this.title}) : super(key: key); 26 | 27 | final String? title; 28 | 29 | @override 30 | _MyHomePageState createState() => _MyHomePageState(); 31 | } 32 | 33 | class _MyHomePageState extends State with TickerProviderStateMixin { 34 | // TabController? _tabController; 35 | MotionTabBarController? _motionTabBarController; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | //// Use normal tab controller 41 | // _tabController = TabController( 42 | // initialIndex: 1, 43 | // length: 4, 44 | // vsync: this, 45 | // ); 46 | 47 | //// use "MotionTabBarController" to replace with "TabController", if you need to programmatically change the tab 48 | _motionTabBarController = MotionTabBarController( 49 | initialIndex: 1, 50 | length: 4, 51 | vsync: this, 52 | ); 53 | } 54 | 55 | @override 56 | void dispose() { 57 | super.dispose(); 58 | _motionTabBarController!.dispose(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return Scaffold( 64 | appBar: AppBar( 65 | title: Text(widget.title!), 66 | ), 67 | bottomNavigationBar: MotionTabBar( 68 | controller: _motionTabBarController, // Add this controller if you need to change your tab programmatically 69 | initialSelectedTab: "Home", 70 | useSafeArea: true, // default: true, apply safe area wrapper 71 | labels: const ["Dashboard", "Home", "Profile", "Settings"], 72 | icons: const [Icons.dashboard, Icons.home, Icons.people_alt, Icons.settings], 73 | 74 | // optional badges, length must be same with labels 75 | badges: [ 76 | // Default Motion Badge Widget 77 | const MotionBadgeWidget( 78 | text: '10+', 79 | textColor: Colors.white, // optional, default to Colors.white 80 | color: Colors.red, // optional, default to Colors.red 81 | size: 18, // optional, default to 18 82 | ), 83 | 84 | // custom badge Widget 85 | Container( 86 | color: Colors.black, 87 | padding: const EdgeInsets.all(2), 88 | child: const Text( 89 | '48', 90 | style: TextStyle( 91 | fontSize: 14, 92 | color: Colors.white, 93 | ), 94 | ), 95 | ), 96 | 97 | // allow null 98 | null, 99 | 100 | // Default Motion Badge Widget with indicator only 101 | const MotionBadgeWidget( 102 | isIndicator: true, 103 | color: Colors.red, // optional, default to Colors.red 104 | size: 5, // optional, default to 5, 105 | show: true, // true / false 106 | ), 107 | ], 108 | tabSize: 50, 109 | tabBarHeight: 55, 110 | textStyle: const TextStyle( 111 | fontSize: 12, 112 | color: Colors.black, 113 | fontWeight: FontWeight.w500, 114 | ), 115 | tabIconColor: Colors.blue[600], 116 | tabIconSize: 28.0, 117 | tabIconSelectedSize: 26.0, 118 | tabSelectedColor: Colors.blue[900], 119 | tabIconSelectedColor: Colors.white, 120 | tabBarColor: Colors.white, 121 | onTabItemSelected: (int value) { 122 | setState(() { 123 | _motionTabBarController!.index = value; 124 | }); 125 | }, 126 | ), 127 | body: TabBarView( 128 | physics: const NeverScrollableScrollPhysics(), // swipe navigation handling is not supported 129 | controller: _motionTabBarController, 130 | children: [ 131 | MainPageContentComponent(title: "Dashboard Page", controller: _motionTabBarController!), 132 | MainPageContentComponent(title: "Home Page", controller: _motionTabBarController!), 133 | MainPageContentComponent(title: "Profile Page", controller: _motionTabBarController!), 134 | MainPageContentComponent(title: "Settings Page", controller: _motionTabBarController!), 135 | ], 136 | ), 137 | ); 138 | } 139 | } 140 | 141 | class MainPageContentComponent extends StatelessWidget { 142 | const MainPageContentComponent({ 143 | required this.title, 144 | required this.controller, 145 | Key? key, 146 | }) : super(key: key); 147 | 148 | final String title; 149 | final MotionTabBarController controller; 150 | 151 | @override 152 | Widget build(BuildContext context) { 153 | return Center( 154 | child: Column( 155 | mainAxisAlignment: MainAxisAlignment.center, 156 | children: [ 157 | Text(title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), 158 | const SizedBox(height: 50), 159 | const Text('Go to "X" page programmatically'), 160 | const SizedBox(height: 10), 161 | ElevatedButton( 162 | onPressed: () => controller.index = 0, 163 | child: const Text('Dashboard Page'), 164 | ), 165 | ElevatedButton( 166 | onPressed: () => controller.index = 1, 167 | child: const Text('Home Page'), 168 | ), 169 | ElevatedButton( 170 | onPressed: () => controller.index = 2, 171 | child: const Text('Profile Page'), 172 | ), 173 | ElevatedButton( 174 | onPressed: () => controller.index = 3, 175 | child: const Text('Settings Page'), 176 | ), 177 | ], 178 | ), 179 | ); 180 | } 181 | } -------------------------------------------------------------------------------- /lib/MotionTabBar.dart: -------------------------------------------------------------------------------- 1 | library motiontabbar; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'MotionTabBarController.dart'; 5 | import 'MotionTabItem.dart'; 6 | import 'helpers/HalfClipper.dart'; 7 | import 'helpers/HalfPainter.dart'; 8 | 9 | typedef MotionTabBuilder = Widget Function(); 10 | 11 | class MotionTabBar extends StatefulWidget { 12 | final Color? tabIconColor, tabIconSelectedColor, tabSelectedColor, tabBarColor; 13 | final double? tabIconSize, tabIconSelectedSize, tabBarHeight, tabSize; 14 | final TextStyle? textStyle; 15 | final Function? onTabItemSelected; 16 | final String initialSelectedTab; 17 | 18 | final List labels; 19 | final List? icons; 20 | final bool useSafeArea; 21 | final MotionTabBarController? controller; 22 | 23 | // badge 24 | final List? badges; 25 | 26 | MotionTabBar({ 27 | this.textStyle, 28 | this.tabIconColor = Colors.black, 29 | this.tabIconSize = 24, 30 | this.tabIconSelectedColor = Colors.white, 31 | this.tabIconSelectedSize = 24, 32 | this.tabSelectedColor = Colors.black, 33 | this.tabBarColor = Colors.white, 34 | this.tabBarHeight = 65, 35 | this.tabSize = 60, 36 | this.onTabItemSelected, 37 | required this.initialSelectedTab, 38 | required this.labels, 39 | this.icons, 40 | this.useSafeArea = true, 41 | this.badges, 42 | this.controller, 43 | }) : assert(labels.contains(initialSelectedTab)), 44 | assert(icons != null && icons.length == labels.length), 45 | assert((badges != null && badges.length > 0) ? badges.length == labels.length : true); 46 | 47 | @override 48 | _MotionTabBarState createState() => _MotionTabBarState(); 49 | } 50 | 51 | class _MotionTabBarState extends State with TickerProviderStateMixin { 52 | late AnimationController _animationController; 53 | late Tween _positionTween; 54 | late Animation _positionAnimation; 55 | 56 | late AnimationController _fadeOutController; 57 | late Animation _fadeFabOutAnimation; 58 | late Animation _fadeFabInAnimation; 59 | 60 | late List labels; 61 | late Map icons; 62 | 63 | get tabAmount => icons.keys.length; 64 | get index => labels.indexOf(selectedTab); 65 | 66 | double fabIconAlpha = 1; 67 | IconData? activeIcon; 68 | String? selectedTab; 69 | 70 | bool isRtl = false; 71 | List? badges; 72 | Widget? activeBadge; 73 | 74 | double getPosition(bool isRTL) { 75 | double pace = 2 / (labels.length - 1); 76 | double position = (pace * index) - 1; 77 | 78 | if (isRTL) { 79 | // If RTL, reverse the position calculation 80 | position = 1 - (pace * index); 81 | } 82 | 83 | return position; 84 | } 85 | 86 | @override 87 | void initState() { 88 | super.initState(); 89 | 90 | WidgetsBinding.instance.addPostFrameCallback((_) async { 91 | isRtl = Directionality.of(context).index == 0; 92 | }); 93 | 94 | if(widget.controller != null) { 95 | widget.controller!.onTabChange= (index) { 96 | setState(() { 97 | activeIcon = widget.icons![index]; 98 | selectedTab = widget.labels[index]; 99 | }); 100 | _initAnimationAndStart(_positionAnimation.value, getPosition(isRtl)); 101 | }; 102 | } 103 | labels = widget.labels; 104 | icons = Map.fromIterable( 105 | labels, 106 | key: (label) => label, 107 | value: (label) => widget.icons![labels.indexOf(label)], 108 | ); 109 | 110 | selectedTab = widget.initialSelectedTab; 111 | activeIcon = icons[selectedTab]; 112 | 113 | // init badge text 114 | int selectedIndex = labels.indexWhere((element) => element == widget.initialSelectedTab); 115 | activeBadge = (widget.badges != null && widget.badges!.length > 0) ? widget.badges![selectedIndex] : null; 116 | 117 | _animationController = AnimationController( 118 | duration: Duration(milliseconds: ANIM_DURATION), 119 | vsync: this, 120 | ); 121 | 122 | _fadeOutController = AnimationController( 123 | duration: Duration(milliseconds: (ANIM_DURATION ~/ 5)), 124 | vsync: this, 125 | ); 126 | 127 | _positionTween = Tween(begin: getPosition(isRtl), end: 1); 128 | 129 | _positionAnimation = _positionTween.animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOut)) 130 | ..addListener(() { 131 | setState(() {}); 132 | }); 133 | 134 | _fadeFabOutAnimation = Tween(begin: 1, end: 0) 135 | .animate(CurvedAnimation(parent: _fadeOutController, curve: Curves.easeOut)) 136 | ..addListener(() { 137 | setState(() { 138 | fabIconAlpha = _fadeFabOutAnimation.value; 139 | }); 140 | }) 141 | ..addStatusListener((AnimationStatus status) { 142 | if (status == AnimationStatus.completed) { 143 | setState(() { 144 | activeIcon = icons[selectedTab]; 145 | int selectedIndex = labels.indexWhere((element) => element == selectedTab); 146 | activeBadge = (widget.badges != null && widget.badges!.length > 0) ? widget.badges![selectedIndex] : null; 147 | }); 148 | } 149 | }); 150 | 151 | _fadeFabInAnimation = Tween(begin: 0, end: 1) 152 | .animate(CurvedAnimation(parent: _animationController, curve: Interval(0.8, 1, curve: Curves.easeOut))) 153 | ..addListener(() { 154 | setState(() { 155 | fabIconAlpha = _fadeFabInAnimation.value; 156 | }); 157 | }); 158 | } 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | return Container( 163 | decoration: BoxDecoration( 164 | color: widget.tabBarColor, 165 | boxShadow: [ 166 | BoxShadow( 167 | color: Colors.black12, 168 | offset: Offset(0, -1), 169 | blurRadius: 5, 170 | ), 171 | ], 172 | ), 173 | child: SafeArea( 174 | bottom: widget.useSafeArea, 175 | child: Stack( 176 | alignment: Alignment.topCenter, 177 | children: [ 178 | Container( 179 | height: widget.tabBarHeight, 180 | decoration: BoxDecoration( 181 | color: widget.tabBarColor, 182 | ), 183 | child: Row( 184 | mainAxisSize: MainAxisSize.max, 185 | mainAxisAlignment: MainAxisAlignment.spaceAround, 186 | children: generateTabItems(), 187 | ), 188 | ), 189 | IgnorePointer( 190 | child: Container( 191 | decoration: BoxDecoration(color: Colors.transparent), 192 | child: Align( 193 | heightFactor: 0, 194 | alignment: Alignment(_positionAnimation.value, 0), 195 | child: FractionallySizedBox( 196 | widthFactor: 1 / tabAmount, 197 | child: Stack( 198 | alignment: Alignment.center, 199 | children: [ 200 | SizedBox( 201 | height: widget.tabSize! + 30, 202 | width: widget.tabSize! + 30, 203 | child: ClipRect( 204 | clipper: HalfClipper(), 205 | child: Container( 206 | child: Center( 207 | child: Container( 208 | width: widget.tabSize! + 10, 209 | height: widget.tabSize! + 10, 210 | decoration: BoxDecoration( 211 | color: widget.tabBarColor, 212 | shape: BoxShape.circle, 213 | boxShadow: [ 214 | BoxShadow( 215 | color: Colors.black12, 216 | blurRadius: 8, 217 | ) 218 | ], 219 | ), 220 | ), 221 | ), 222 | ), 223 | ), 224 | ), 225 | SizedBox( 226 | height: widget.tabSize! + 15, 227 | width: widget.tabSize! + 35, 228 | child: CustomPaint(painter: HalfPainter(color: widget.tabBarColor)), 229 | ), 230 | SizedBox( 231 | height: widget.tabSize, 232 | width: widget.tabSize, 233 | child: Container( 234 | decoration: BoxDecoration( 235 | shape: BoxShape.circle, 236 | color: widget.tabSelectedColor, 237 | ), 238 | child: Padding( 239 | padding: const EdgeInsets.all(0.0), 240 | child: Opacity( 241 | opacity: fabIconAlpha, 242 | child: Stack( 243 | alignment: Alignment.center, 244 | children: [ 245 | Icon( 246 | activeIcon, 247 | color: widget.tabIconSelectedColor, 248 | size: widget.tabIconSelectedSize, 249 | ), 250 | activeBadge != null 251 | ? Positioned( 252 | top: 0, 253 | right: 0, 254 | child: activeBadge!, 255 | ) 256 | : SizedBox(), 257 | ], 258 | ), 259 | ), 260 | ), 261 | ), 262 | ), 263 | ], 264 | ), 265 | ), 266 | ), 267 | ), 268 | ), 269 | ], 270 | ), 271 | ), 272 | ); 273 | } 274 | 275 | List generateTabItems() { 276 | bool isRtl = Directionality.of(context).index == 0; 277 | return labels.map((tabLabel) { 278 | IconData? icon = icons[tabLabel]; 279 | 280 | int selectedIndex = labels.indexWhere((element) => element == tabLabel); 281 | Widget? badge = (widget.badges != null && widget.badges!.length > 0) ? widget.badges![selectedIndex] : null; 282 | 283 | return MotionTabItem( 284 | selected: selectedTab == tabLabel, 285 | iconData: icon, 286 | title: tabLabel, 287 | textStyle: widget.textStyle ?? TextStyle(color: Colors.black), 288 | tabIconColor: widget.tabIconColor ?? Colors.black, 289 | tabIconSize: widget.tabIconSize, 290 | badge: badge, 291 | callbackFunction: () { 292 | setState(() { 293 | activeIcon = icon; 294 | selectedTab = tabLabel; 295 | widget.onTabItemSelected!(index); 296 | }); 297 | _initAnimationAndStart(_positionAnimation.value, getPosition(isRtl)); 298 | }, 299 | ); 300 | }).toList(); 301 | } 302 | 303 | _initAnimationAndStart(double from, double to) { 304 | _positionTween.begin = from; 305 | _positionTween.end = to; 306 | 307 | _animationController.reset(); 308 | _fadeOutController.reset(); 309 | _animationController.forward(); 310 | _fadeOutController.forward(); 311 | } 312 | } 313 | --------------------------------------------------------------------------------