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 : /// 
35 1 : fixed,
36 :
37 : /// convex shape is fixed center with circle, see [FixedCircleTabStyle]
38 : ///
39 : /// 
40 1 : fixedCircle,
41 :
42 : /// convex shape is moved after selection, see [ReactTabStyle]
43 : ///
44 : /// 
45 1 : react,
46 :
47 : /// convex shape is moved with circle after selection, see [ReactCircleTabStyle]
48 : ///
49 : /// 
50 1 : reactCircle,
51 :
52 : /// tab icon, text animated with pop transition
53 : ///
54 : /// 
55 1 : textIn,
56 :
57 : /// similar to [TabStyle.textIn], text first
58 : ///
59 : /// 
60 1 : titled,
61 :
62 : /// tab item is flipped when selected, does not support [flutter web]
63 : ///
64 : /// 
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 : /// 
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 : /// 
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 : _State createState() {
273 1 : return _State();
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 _State extends State<ConvexAppBar> with TickerProviderStateMixin {
289 : int _currentIndex;
290 : Animation<double> _animation;
291 : AnimationController _controller;
292 : TabController _tabController;
293 :
294 1 : @override
295 : void initState() {
296 1 : super.initState();
297 1 : if (!isFixed()) {
298 1 : _initAnimation();
299 : }
300 : }
301 :
302 1 : void _handleTabControllerAnimationTick({bool force = false}) {
303 2 : if (!force && _tabController.indexIsChanging) {
304 : return;
305 : }
306 4 : if (_tabController.index != _currentIndex) {
307 3 : _warpToCurrentIndex(_tabController.index);
308 : }
309 : }
310 :
311 1 : Future<void> _warpToCurrentIndex(int index) async {
312 2 : _initAnimation(from: _currentIndex, to: index);
313 2 : _controller?.forward();
314 2 : setState(() {
315 1 : _currentIndex = index;
316 : });
317 : }
318 :
319 1 : Animation<double> _initAnimation({int from, int to}) {
320 1 : if (from != null && (from == to)) {
321 1 : return _animation;
322 : }
323 2 : from ??= widget.initialActiveIndex ?? 0;
324 : to ??= from;
325 6 : var lower = (2 * from + 1) / (2 * widget.count);
326 6 : var upper = (2 * to + 1) / (2 * widget.count);
327 2 : _controller = AnimationController(
328 1 : duration: Duration(milliseconds: 150),
329 : vsync: this,
330 : );
331 1 : final Animation curve = CurvedAnimation(
332 1 : parent: _controller,
333 2 : curve: widget.curve,
334 : );
335 3 : _animation = Tween(begin: lower, end: upper).animate(curve);
336 1 : return _animation;
337 : }
338 :
339 1 : @override
340 : void dispose() {
341 2 : _controller?.dispose();
342 1 : super.dispose();
343 : }
344 :
345 1 : _updateTabController() {
346 : final TabController newController =
347 4 : widget.controller ?? DefaultTabController.of(context);
348 3 : _tabController?.removeListener(_handleTabControllerAnimationTick);
349 1 : _tabController = newController;
350 3 : _tabController?.addListener(_handleTabControllerAnimationTick);
351 5 : _currentIndex = widget.initialActiveIndex ?? _tabController?.index ?? 0;
352 : }
353 :
354 1 : @override
355 : void didChangeDependencies() {
356 1 : super.didChangeDependencies();
357 1 : _updateTabController();
358 :
359 : /// When both ConvexAppBar and TabController are configured with initial index, there can be conflict;
360 : /// We use ConvexAppBar's value;
361 2 : if (widget.initialActiveIndex != null &&
362 1 : _tabController != null &&
363 5 : widget.initialActiveIndex != _tabController.index) {
364 3 : WidgetsBinding.instance.addPostFrameCallback((_) {
365 3 : _tabController.index = _currentIndex;
366 : });
367 : }
368 : }
369 :
370 1 : @override
371 : void didUpdateWidget(ConvexAppBar oldWidget) {
372 1 : super.didUpdateWidget(oldWidget);
373 4 : if (widget.controller != oldWidget.controller) {
374 0 : _updateTabController();
375 : }
376 : }
377 :
378 1 : @override
379 : Widget build(BuildContext context) {
380 : // take care of iPhoneX' safe area at bottom edge
381 : final double additionalBottomPadding =
382 4 : math.max(MediaQuery.of(context).padding.bottom, 0.0);
383 3 : var halfSize = widget.count ~/ 2;
384 2 : final convexIndex = isFixed() ? halfSize : _currentIndex;
385 3 : final active = isFixed() ? convexIndex == _currentIndex : true;
386 1 : return extend.Stack(
387 : overflow: Overflow.visible,
388 : alignment: Alignment.bottomCenter,
389 1 : children: <Widget>[
390 1 : Container(
391 3 : height: widget.height ?? BAR_HEIGHT + additionalBottomPadding,
392 3 : width: MediaQuery.of(context).size.width,
393 1 : child: CustomPaint(
394 1 : painter: ConvexPainter(
395 2 : top: widget.top ?? CURVE_TOP,
396 2 : width: widget.curveSize ?? CONVEX_SIZE,
397 2 : height: widget.curveSize ?? CONVEX_SIZE,
398 2 : color: widget.backgroundColor ?? Colors.blue,
399 2 : gradient: widget.gradient,
400 2 : sigma: widget.elevation ?? ELEVATION,
401 1 : leftPercent: isFixed()
402 : ? const AlwaysStoppedAnimation<double>(0.5)
403 1 : : _animation ?? _initAnimation(),
404 : ),
405 : ),
406 : ),
407 1 : _barContent(additionalBottomPadding),
408 1 : Positioned.fill(
409 2 : top: widget.top,
410 : bottom: additionalBottomPadding,
411 1 : child: FractionallySizedBox(
412 3 : widthFactor: 1 / widget.count,
413 3 : alignment: Alignment((convexIndex - halfSize) / (halfSize), 0),
414 1 : child: GestureDetector(
415 1 : child: _newTab(convexIndex, active),
416 2 : onTap: () => _onTabClick(convexIndex),
417 : )),
418 : ),
419 : ],
420 : );
421 : }
422 :
423 4 : bool isFixed() => widget.itemBuilder.fixed();
424 :
425 1 : Widget _barContent(double paddingBottom) {
426 1 : List<Widget> children = [];
427 : // add placeholder Widget
428 5 : var curveTabIndex = isFixed() ? widget.count ~/ 2 : _currentIndex;
429 4 : for (var i = 0; i < widget.count; i++) {
430 1 : if (i == curveTabIndex) {
431 3 : children.add(Expanded(child: Container()));
432 : continue;
433 : }
434 2 : var active = _currentIndex == i;
435 1 : Widget child = _newTab(i, active);
436 2 : children.add(Expanded(
437 1 : child: GestureDetector(
438 : behavior: HitTestBehavior.opaque,
439 : child: child,
440 2 : onTap: () => _onTabClick(i),
441 : )));
442 : }
443 :
444 1 : return Container(
445 3 : height: widget.height ?? BAR_HEIGHT + paddingBottom,
446 1 : padding: EdgeInsets.only(bottom: paddingBottom),
447 1 : child: Row(
448 : mainAxisSize: MainAxisSize.max,
449 : crossAxisAlignment: CrossAxisAlignment.center,
450 : children: children,
451 : ),
452 : );
453 : }
454 :
455 1 : Widget _newTab(int i, bool active) {
456 4 : var child = widget.itemBuilder.build(context, i, active);
457 2 : if (widget.chipBuilder != null) {
458 4 : child = widget.chipBuilder.build(context, child, i, active);
459 : }
460 : return child;
461 : }
462 :
463 1 : void _onTabClick(int i) {
464 1 : _warpToCurrentIndex(i);
465 2 : _tabController?.index = i;
466 2 : if (widget.onTap != null) {
467 2 : widget.onTap(i);
468 : }
469 : }
470 : }
471 :
472 : typedef GestureTapIndexCallback = void Function(int index);
473 : typedef CustomTabBuilder = Widget Function(
474 : BuildContext context, int index, bool active);
|