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

Generated by: LCOV version 1.14