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