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 : 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);
|