LCOV - code coverage report
Current view: top level - src - bar.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 123 124 99.2 %
Date: 2020-03-09 19:58:44 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:math' as math;
       2             : 
       3             : import 'package:flutter/cupertino.dart';
       4             : import 'package:flutter/material.dart';
       5             : import 'package:flutter/widgets.dart';
       6             : 
       7             : import 'chip_builder.dart';
       8             : import 'interface.dart';
       9             : import 'item.dart';
      10             : import 'painter.dart';
      11             : import 'stack.dart' as extend;
      12             : import 'style/fixed_circle_tab_style.dart';
      13             : import 'style/fixed_tab_style.dart';
      14             : import 'style/react_circle_tab_style.dart';
      15             : import 'style/react_tab_style.dart';
      16             : import 'style/styles.dart';
      17             : 
      18             : /// Default size of the curve line.
      19             : const double CONVEX_SIZE = 80;
      20             : 
      21             : /// Default height of the AppBar.
      22             : const double BAR_HEIGHT = 50;
      23             : 
      24             : /// Default distance that the child's top edge is inset from the top of the stack.
      25             : const double CURVE_TOP = -25;
      26             : 
      27             : /// Default size for active tab.
      28             : const double ACTION_LAYOUT_SIZE = 60;
      29             : 
      30             : /// Default size for active icon in tab.
      31             : const double ACTION_INNER_BUTTON_SIZE = 40;
      32             : 
      33             : /// default elevation of [ConvexAppBar].
      34             : const double ELEVATION = 2;
      35             : 
      36             : /// Tab style which supported internal.
      37           1 : enum TabStyle {
      38             :   /// Convex shape fixed center, see [FixedTabStyle].
      39             :   ///
      40             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-fixed.gif)
      41           1 :   fixed,
      42             : 
      43             :   /// Convex shape is fixed center with circle, see [FixedCircleTabStyle].
      44             :   ///
      45             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-fixed-circle.gif)
      46           1 :   fixedCircle,
      47             : 
      48             :   /// Convex shape is moved after selection, see [ReactTabStyle].
      49             :   ///
      50             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-react.gif)
      51           1 :   react,
      52             : 
      53             :   /// Convex shape is moved with circle after selection, see [ReactCircleTabStyle].
      54             :   ///
      55             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-react-circle.gif)
      56           1 :   reactCircle,
      57             : 
      58             :   /// Tab icon, text animated with pop transition.
      59             :   ///
      60             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-textIn.gif)
      61           1 :   textIn,
      62             : 
      63             :   /// Similar to [TabStyle.textIn], text first.
      64             :   ///
      65             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-titled.gif)
      66           1 :   titled,
      67             : 
      68             :   /// Tab item is flipped when selected, does not support [flutter web].
      69             :   ///
      70             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-flip.gif)
      71           1 :   flip,
      72             : 
      73             :   /// User defined style
      74           1 :   custom,
      75             : }
      76             : 
      77             : /// Online example can be found at http://hacktons.cn/convex_bottom_bar.
      78             : ///
      79             : /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-theming.png)
      80             : class ConvexAppBar extends StatefulWidget {
      81             :   /// TAB item builder.
      82             :   final DelegateBuilder itemBuilder;
      83             : 
      84             :   /// Badge chip builder.
      85             :   final ChipBuilder chipBuilder;
      86             : 
      87             :   /// Tab Click handler.
      88             :   final GestureTapIndexCallback onTap;
      89             : 
      90             :   /// Tab controller to work with [TabBarView] or [PageView].
      91             :   final TabController controller;
      92             : 
      93             :   /// Color of the AppBar.
      94             :   final Color backgroundColor;
      95             : 
      96             :   /// If provided, backgroundColor for tab app will be ignored.
      97             :   ///
      98             :   /// ![](https://github.com/hacktons/convex_bottom_bar/raw/master/doc/appbar-gradient.gif)
      99             :   final Gradient gradient;
     100             : 
     101             :   /// The initial active index, you can config initialIndex of [TabController] if work with [TabBarView] or [PageView].
     102             :   final int initialActiveIndex;
     103             : 
     104             :   /// Tab count.
     105             :   final int count;
     106             : 
     107             :   /// Height of the AppBar.
     108             :   final double height;
     109             : 
     110             :   /// Size of the curve line.
     111             :   final double curveSize;
     112             : 
     113             :   /// The distance that the [actionButton] top edge is inset from the top of the AppBar.
     114             :   final double top;
     115             : 
     116             :   /// Elevation for the bar top edge.
     117             :   final double elevation;
     118             : 
     119             :   /// Style to describe the convex shape.
     120             :   final TabStyle style;
     121             : 
     122             :   /// The curve to use in the forward direction. Only works when tab style is not fixed.
     123             :   final Curve curve;
     124             : 
     125             :   /// Construct a new appbar with internal style.
     126             :   ///
     127             :   /// ```dart
     128             :   /// ConvexAppBar(
     129             :   ///   items: [
     130             :   ///     TabItem(title: 'Tab A', icon: Icons.add),
     131             :   ///     TabItem(title: 'Tab B', icon: Icons.near_me),
     132             :   ///     TabItem(title: 'Tab C', icon: Icons.web),
     133             :   ///   ],
     134             :   /// )
     135             :   /// ```
     136             :   ///
     137             :   /// You can also define a custom chipBuilder class.
     138             :   /// ```dart
     139             :   /// class _ChipBuilder extends ChipBuilder {
     140             :   ///  @override
     141             :   ///  Widget build(BuildContext context, Widget child, int index, bool active) {
     142             :   ///    return Stack(
     143             :   ///      alignment: Alignment.center,
     144             :   ///      children: <Widget>[
     145             :   ///        child,
     146             :   ///        Positioned.fill(
     147             :   ///          child: Align(
     148             :   ///            alignment: Alignment.topRight,
     149             :   ///            child: Container(
     150             :   ///              margin: EdgeInsets.only(top: 10, right: 10),
     151             :   ///              padding: EdgeInsets.only(left: 4, right: 4),
     152             :   ///              child: Icon(Icons.access_alarm, color: Colors.redAccent),
     153             :   ///            ),
     154             :   ///          ),
     155             :   ///        )
     156             :   ///      ],
     157             :   ///    );
     158             :   ///    ;
     159             :   ///  }
     160             :   /// }
     161             :   ///```
     162             :   /// See also:
     163             :   ///
     164             :   ///  * [ConvexAppBar.builder], define a custom tab style by implement a [DelegateBuilder].
     165             :   ///  * [ConvexAppBar.badge], construct a new appbar with styled badge.
     166           1 :   ConvexAppBar({
     167             :     Key key,
     168             :     @required List<TabItem> items,
     169             :     int initialActiveIndex,
     170             :     GestureTapIndexCallback onTap,
     171             :     TabController controller,
     172             :     Color color,
     173             :     Color activeColor,
     174             :     Color backgroundColor,
     175             :     Gradient gradient,
     176             :     double height,
     177             :     double curveSize,
     178             :     double top,
     179             :     double elevation,
     180             :     TabStyle style = TabStyle.reactCircle,
     181             :     Curve curve = Curves.easeInOut,
     182             :     ChipBuilder chipBuilder,
     183           1 :   }) : this.builder(
     184             :           key: key,
     185           1 :           itemBuilder: supportedStyle(
     186             :             style,
     187             :             items: items,
     188             :             color: color ?? Colors.white60,
     189             :             activeColor: activeColor ?? Colors.white,
     190             :             backgroundColor: backgroundColor ?? Colors.blue,
     191             :             curve: curve ?? Curves.easeInOut,
     192             :           ),
     193             :           onTap: onTap,
     194             :           controller: controller,
     195             :           backgroundColor: backgroundColor ?? Colors.blue,
     196           1 :           count: items.length,
     197             :           initialActiveIndex: initialActiveIndex,
     198             :           gradient: gradient,
     199             :           height: height,
     200             :           curveSize: curveSize,
     201             :           top: top,
     202             :           elevation: elevation,
     203             :           style: style,
     204             :           curve: curve ?? Curves.easeInOut,
     205             :           chipBuilder: chipBuilder,
     206             :         );
     207             : 
     208             :   /// Define a custom tab style by implement a [DelegateBuilder].
     209             :   ///
     210             :   /// ```dart
     211             :   /// ConvexAppBar(
     212             :   ///   count: 5,
     213             :   ///   itemBuilder: Builder(),
     214             :   /// )
     215             :   ///
     216             :   /// class Builder extends DelegateBuilder {
     217             :   ///   @override
     218             :   ///   Widget build(BuildContext context, int index, bool active) {
     219             :   ///     return Text('TAB $index');
     220             :   ///   }
     221             :   /// }
     222             :   /// ```
     223           1 :   const ConvexAppBar.builder({
     224             :     Key key,
     225             :     @required this.itemBuilder,
     226             :     @required this.count,
     227             :     this.initialActiveIndex,
     228             :     this.onTap,
     229             :     this.controller,
     230             :     this.backgroundColor,
     231             :     this.gradient,
     232             :     this.height,
     233             :     this.curveSize,
     234             :     this.top,
     235             :     this.elevation,
     236             :     this.style = TabStyle.reactCircle,
     237             :     this.curve = Curves.easeInOut,
     238             :     this.chipBuilder,
     239           1 :   })  : assert(top == null || top <= 0, 'top should be negative'),
     240           1 :         assert(itemBuilder != null, 'provide custom buidler'),
     241           2 :         assert(initialActiveIndex == null || initialActiveIndex < count,
     242           1 :             'initial index should < $count'),
     243           1 :         super(key: key);
     244             : 
     245             :   /// Construct a new appbar with badge.
     246             :   ///
     247             :   /// {@animation 1010 598 https://github.com/hacktons/convex_bottom_bar/raw/master/doc/badge-demo.mp4}
     248             :   ///
     249             :   /// [badge] is map with tab items, the value of entry can be either [String],
     250             :   /// [IconData], [Color] or [Widget].
     251             :   ///
     252             :   /// ```dart
     253             :   /// ConvexAppBar.badge(
     254             :   ///   {3: '99+'},
     255             :   ///   items: [
     256             :   ///     TabItem(title: 'Tab A', icon: Icons.add),
     257             :   ///     TabItem(title: 'Tab B', icon: Icons.near_me),
     258             :   ///     TabItem(title: 'Tab C', icon: Icons.web),
     259             :   ///   ],
     260             :   /// )
     261             :   /// ```
     262           1 :   factory ConvexAppBar.badge(
     263             :     Map<int, dynamic> badge, {
     264             :     Key key,
     265             :     // config for badge
     266             :     Color badgeTextColor,
     267             :     Color badgeColor,
     268             :     EdgeInsets badgePadding,
     269             :     double badgeBorderRadius,
     270             :     // parameter for appbar
     271             :     List<TabItem> items,
     272             :     int initialActiveIndex,
     273             :     GestureTapIndexCallback onTap,
     274             :     TabController controller,
     275             :     Color color,
     276             :     Color activeColor,
     277             :     Color backgroundColor,
     278             :     Gradient gradient,
     279             :     double height,
     280             :     double curveSize,
     281             :     double top,
     282             :     double elevation,
     283             :     TabStyle style,
     284             :     Curve curve,
     285             :   }) {
     286             :     DefaultChipBuilder chipBuilder;
     287           1 :     if (badge != null && badge.isNotEmpty) {
     288           1 :       chipBuilder = DefaultChipBuilder(
     289             :         badge,
     290             :         textColor: badgeTextColor,
     291             :         badgeColor: badgeColor,
     292             :         padding: badgePadding,
     293             :         borderRadius: badgeBorderRadius,
     294             :       );
     295             :     }
     296           1 :     return ConvexAppBar(
     297             :       key: key,
     298             :       items: items,
     299             :       initialActiveIndex: initialActiveIndex,
     300             :       onTap: onTap,
     301             :       controller: controller,
     302             :       color: color,
     303             :       activeColor: activeColor,
     304             :       backgroundColor: backgroundColor,
     305             :       gradient: gradient,
     306             :       height: height,
     307             :       curveSize: curveSize,
     308             :       top: top,
     309             :       elevation: elevation,
     310             :       style: style,
     311             :       curve: curve,
     312             :       chipBuilder: chipBuilder,
     313             :     );
     314             :   }
     315             : 
     316           1 :   @override
     317             :   ConvexAppBarState createState() {
     318           1 :     return ConvexAppBarState();
     319             :   }
     320             : }
     321             : 
     322             : /// State of [ConvexAppBar].
     323             : class ConvexAppBarState extends State<ConvexAppBar>
     324             :     with TickerProviderStateMixin {
     325             :   int _currentIndex;
     326             :   Animation<double> _animation;
     327             :   AnimationController _controller;
     328             :   TabController _tabController;
     329             : 
     330           1 :   @override
     331             :   void initState() {
     332           1 :     super.initState();
     333           1 :     if (!isFixed()) {
     334           1 :       _initAnimation();
     335             :     }
     336             :   }
     337             : 
     338           1 :   void _handleTabControllerAnimationTick({bool force = false}) {
     339           2 :     if (!force && _tabController.indexIsChanging) {
     340             :       return;
     341             :     }
     342           4 :     if (_tabController.index != _currentIndex) {
     343           3 :       animateTo(_tabController.index);
     344             :     }
     345             :   }
     346             : 
     347             :   /// change active tab index; can be used with [PageView].
     348           1 :   Future<void> animateTo(int index) async {
     349           2 :     _initAnimation(from: _currentIndex, to: index);
     350           2 :     _controller?.forward();
     351           2 :     setState(() {
     352           1 :       _currentIndex = index;
     353             :     });
     354             :   }
     355             : 
     356           1 :   Animation<double> _initAnimation({int from, int to}) {
     357           1 :     if (from != null && (from == to)) {
     358           1 :       return _animation;
     359             :     }
     360           2 :     from ??= widget.initialActiveIndex ?? 0;
     361             :     to ??= from;
     362           6 :     var lower = (2 * from + 1) / (2 * widget.count);
     363           6 :     var upper = (2 * to + 1) / (2 * widget.count);
     364           2 :     _controller = AnimationController(
     365           1 :       duration: Duration(milliseconds: 150),
     366             :       vsync: this,
     367             :     );
     368           1 :     final Animation curve = CurvedAnimation(
     369           1 :       parent: _controller,
     370           2 :       curve: widget.curve,
     371             :     );
     372           3 :     _animation = Tween(begin: lower, end: upper).animate(curve);
     373           1 :     return _animation;
     374             :   }
     375             : 
     376           1 :   @override
     377             :   void dispose() {
     378           2 :     _controller?.dispose();
     379           1 :     super.dispose();
     380             :   }
     381             : 
     382           1 :   _updateTabController() {
     383             :     final TabController newController =
     384           4 :         widget.controller ?? DefaultTabController.of(context);
     385           3 :     _tabController?.removeListener(_handleTabControllerAnimationTick);
     386           1 :     _tabController = newController;
     387           3 :     _tabController?.addListener(_handleTabControllerAnimationTick);
     388           5 :     _currentIndex = widget.initialActiveIndex ?? _tabController?.index ?? 0;
     389             :   }
     390             : 
     391           1 :   @override
     392             :   void didChangeDependencies() {
     393           1 :     super.didChangeDependencies();
     394           1 :     _updateTabController();
     395             : 
     396             :     /// When both ConvexAppBar and TabController are configured with initial index, there can be conflict;
     397             :     /// We use ConvexAppBar's value;
     398           2 :     if (widget.initialActiveIndex != null &&
     399           1 :         _tabController != null &&
     400           5 :         widget.initialActiveIndex != _tabController.index) {
     401           3 :       WidgetsBinding.instance.addPostFrameCallback((_) {
     402           3 :         _tabController.index = _currentIndex;
     403             :       });
     404             :     }
     405             :   }
     406             : 
     407           1 :   @override
     408             :   void didUpdateWidget(ConvexAppBar oldWidget) {
     409           1 :     super.didUpdateWidget(oldWidget);
     410           4 :     if (widget.controller != oldWidget.controller) {
     411           0 :       _updateTabController();
     412             :     }
     413             :   }
     414             : 
     415           1 :   @override
     416             :   Widget build(BuildContext context) {
     417             :     // take care of iPhoneX' safe area at bottom edge
     418             :     final double additionalBottomPadding =
     419           4 :         math.max(MediaQuery.of(context).padding.bottom, 0.0);
     420           5 :     final convexIndex = isFixed() ? (widget.count ~/ 2) : _currentIndex;
     421           3 :     final active = isFixed() ? convexIndex == _currentIndex : true;
     422             : 
     423           3 :     final height = widget.height ?? BAR_HEIGHT + additionalBottomPadding;
     424           3 :     final width = MediaQuery.of(context).size.width;
     425           1 :     var percent = isFixed()
     426             :         ? const AlwaysStoppedAnimation<double>(0.5)
     427           1 :         : _animation ?? _initAnimation();
     428           3 :     var factor = 1 / widget.count;
     429           1 :     var offset = FractionalOffset(
     430           8 :       widget.count > 1 ? 1 / (widget.count - 1) * convexIndex : 0.0,
     431             :       0,
     432             :     );
     433           1 :     return extend.Stack(
     434             :       overflow: Overflow.visible,
     435             :       alignment: Alignment.bottomCenter,
     436           1 :       children: <Widget>[
     437           1 :         Container(
     438             :           height: height,
     439             :           width: width,
     440           1 :           child: CustomPaint(
     441           1 :             painter: ConvexPainter(
     442           2 :               top: widget.top ?? CURVE_TOP,
     443           2 :               width: widget.curveSize ?? CONVEX_SIZE,
     444           2 :               height: widget.curveSize ?? CONVEX_SIZE,
     445           2 :               color: widget.backgroundColor ?? Colors.blue,
     446           2 :               gradient: widget.gradient,
     447           2 :               sigma: widget.elevation ?? ELEVATION,
     448             :               leftPercent: percent,
     449             :             ),
     450             :           ),
     451             :         ),
     452           1 :         _barContent(height, additionalBottomPadding, convexIndex),
     453           1 :         Positioned.fill(
     454           2 :           top: widget.top,
     455             :           bottom: additionalBottomPadding,
     456           1 :           child: FractionallySizedBox(
     457             :               widthFactor: factor,
     458             :               alignment: offset,
     459           1 :               child: GestureDetector(
     460           1 :                 child: _newTab(convexIndex, active),
     461           2 :                 onTap: () => _onTabClick(convexIndex),
     462             :               )),
     463             :         ),
     464             :       ],
     465             :     );
     466             :   }
     467             : 
     468             :   /// Whether the tab shape are fixed or not.
     469           4 :   bool isFixed() => widget.itemBuilder.fixed();
     470             : 
     471           1 :   Widget _barContent(double height, double paddingBottom, int curveTabIndex) {
     472           1 :     List<Widget> children = [];
     473           4 :     for (var i = 0; i < widget.count; i++) {
     474           1 :       if (i == curveTabIndex) {
     475           3 :         children.add(Expanded(child: Container()));
     476             :         continue;
     477             :       }
     478           2 :       var active = _currentIndex == i;
     479           2 :       children.add(Expanded(
     480           1 :         child: GestureDetector(
     481             :           behavior: HitTestBehavior.opaque,
     482           1 :           child: _newTab(i, active),
     483           2 :           onTap: () => _onTabClick(i),
     484             :         ),
     485             :       ));
     486             :     }
     487             : 
     488           1 :     return Container(
     489             :       height: height,
     490           1 :       padding: EdgeInsets.only(bottom: paddingBottom),
     491           1 :       child: Row(
     492             :         mainAxisSize: MainAxisSize.max,
     493             :         crossAxisAlignment: CrossAxisAlignment.center,
     494             :         children: children,
     495             :       ),
     496             :     );
     497             :   }
     498             : 
     499           1 :   Widget _newTab(int i, bool active) {
     500           4 :     var child = widget.itemBuilder.build(context, i, active);
     501           2 :     if (widget.chipBuilder != null) {
     502           4 :       child = widget.chipBuilder.build(context, child, i, active);
     503             :     }
     504             :     return child;
     505             :   }
     506             : 
     507           1 :   void _onTabClick(int i) {
     508           1 :     animateTo(i);
     509           2 :     _tabController?.index = i;
     510           2 :     if (widget.onTap != null) {
     511           2 :       widget.onTap(i);
     512             :     }
     513             :   }
     514             : }

Generated by: LCOV version 1.14