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 : /// 
41 1 : fixed,
42 :
43 : /// Convex shape is fixed center with circle, see [FixedCircleTabStyle].
44 : ///
45 : /// 
46 1 : fixedCircle,
47 :
48 : /// Convex shape is moved after selection, see [ReactTabStyle].
49 : ///
50 : /// 
51 1 : react,
52 :
53 : /// Convex shape is moved with circle after selection, see [ReactCircleTabStyle].
54 : ///
55 : /// 
56 1 : reactCircle,
57 :
58 : /// Tab icon, text animated with pop transition.
59 : ///
60 : /// 
61 1 : textIn,
62 :
63 : /// Similar to [TabStyle.textIn], text first.
64 : ///
65 : /// 
66 1 : titled,
67 :
68 : /// Tab item is flipped when selected, does not support [flutter web].
69 : ///
70 : /// 
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 : /// 
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 : /// 
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 : }
|