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 : /// Color of the AppBar
84 : final Color backgroundColor;
85 :
86 : /// If provided, backgroundColor for tab app will be ignored
87 : ///
88 : /// 
89 : final Gradient gradient;
90 :
91 : /// The initial active index, default as 0 if not provided;
92 : final int initialActiveIndex;
93 :
94 : /// Tab count
95 : final int count;
96 :
97 : /// Height of the AppBar
98 : final double height;
99 :
100 : /// Size of the curve line
101 : final double curveSize;
102 :
103 : /// The distance that the [actionButton] top edge is inset from the top of the AppBar.
104 : final double top;
105 :
106 : /// Elevation for the bar top edge
107 : final double elevation;
108 :
109 : /// Style to describe the convex shape
110 : final TabStyle style;
111 :
112 : /// The curve to use in the forward direction. Only works when tab style is not fixed.
113 : final Curve curve;
114 :
115 : /// Construct a new appbar with internal style
116 : ///
117 : /// {@tool sample}
118 : ///
119 : /// ```dart
120 : /// ConvexAppBar(
121 : /// items: [
122 : /// TabItem(title: 'Tab A', icon: Icons.add),
123 : /// TabItem(title: 'Tab B', icon: Icons.near_me),
124 : /// TabItem(title: 'Tab C', icon: Icons.web),
125 : /// ],
126 : /// )
127 : /// ```
128 : /// {@end-tool}
129 1 : ConvexAppBar({
130 : Key key,
131 : @required List<TabItem> items,
132 : int initialActiveIndex,
133 : GestureTapIndexCallback onTap,
134 : Color color,
135 : Color activeColor,
136 : Color backgroundColor,
137 : Gradient gradient,
138 : double height,
139 : double curveSize,
140 : double top,
141 : double elevation,
142 : TabStyle style = TabStyle.reactCircle,
143 : Curve curve = Curves.easeInOut,
144 : ChipBuilder chipBuilder,
145 1 : }) : this.builder(
146 : key: key,
147 1 : itemBuilder: supportedStyle(
148 : style,
149 : items: items,
150 : color: color ?? Colors.white60,
151 : activeColor: activeColor ?? Colors.white,
152 : backgroundColor: backgroundColor ?? Colors.blue,
153 : curve: curve ?? Curves.easeInOut,
154 : ),
155 : onTap: onTap,
156 : backgroundColor: backgroundColor ?? Colors.blue,
157 1 : count: items.length,
158 : initialActiveIndex: initialActiveIndex,
159 : gradient: gradient,
160 : height: height,
161 : curveSize: curveSize,
162 : top: top,
163 : elevation: elevation,
164 : style: style,
165 : curve: curve ?? Curves.easeInOut,
166 : chipBuilder: chipBuilder,
167 : );
168 :
169 : /// define a custom tab style by implement a [DelegateBuilder]
170 1 : const ConvexAppBar.builder({
171 : Key key,
172 : @required this.itemBuilder,
173 : @required this.count,
174 : this.initialActiveIndex,
175 : this.onTap,
176 : this.backgroundColor,
177 : this.gradient,
178 : this.height,
179 : this.curveSize,
180 : this.top,
181 : this.elevation,
182 : this.style = TabStyle.reactCircle,
183 : this.curve = Curves.easeInOut,
184 : this.chipBuilder,
185 1 : }) : assert(top == null || top <= 0, 'top should be negative'),
186 1 : assert(itemBuilder != null, 'provide custom buidler'),
187 1 : assert(initialActiveIndex == null || initialActiveIndex < count,
188 0 : 'initial index should < $count'),
189 1 : super(key: key);
190 :
191 : /// Construct a new appbar with badge
192 : ///
193 : /// {@animation 1010 598 https://github.com/hacktons/convex_bottom_bar/raw/master/doc/badge-demo.mp4}
194 : ///
195 : /// [badge] is map with tab items, the value of entry can be either [String],
196 : /// [IconData], [Color] or [Widget].
197 : ///
198 : /// {@tool sample}
199 : ///
200 : /// ```dart
201 : /// ConvexAppBar.badge(
202 : /// {3: '99+'},
203 : /// items: [
204 : /// TabItem(title: 'Tab A', icon: Icons.add),
205 : /// TabItem(title: 'Tab B', icon: Icons.near_me),
206 : /// TabItem(title: 'Tab C', icon: Icons.web),
207 : /// ],
208 : /// )
209 : /// ```
210 : /// {@end-tool}
211 1 : factory ConvexAppBar.badge(
212 : Map<int, dynamic> badge, {
213 : Key key,
214 : // config for badge
215 : Color badgeTextColor,
216 : Color badgeColor,
217 : EdgeInsets badgePadding,
218 : double badgeBorderRadius,
219 : // parameter for appbar
220 : List<TabItem> items,
221 : int initialActiveIndex,
222 : GestureTapIndexCallback onTap,
223 : Color color,
224 : Color activeColor,
225 : Color backgroundColor,
226 : Gradient gradient,
227 : double height,
228 : double curveSize,
229 : double top,
230 : double elevation,
231 : TabStyle style,
232 : Curve curve,
233 : }) {
234 : DefaultChipBuilder chipBuilder;
235 1 : if (badge != null && badge.isNotEmpty) {
236 1 : chipBuilder = DefaultChipBuilder(
237 : badge,
238 : textColor: badgeTextColor,
239 : badgeColor: badgeColor,
240 : padding: badgePadding,
241 : borderRadius: badgeBorderRadius,
242 : );
243 : }
244 1 : return ConvexAppBar(
245 : key: key,
246 : items: items,
247 : initialActiveIndex: initialActiveIndex,
248 : onTap: onTap,
249 : color: color,
250 : activeColor: activeColor,
251 : backgroundColor: backgroundColor,
252 : gradient: gradient,
253 : height: height,
254 : curveSize: curveSize,
255 : top: top,
256 : elevation: elevation,
257 : style: style,
258 : curve: curve,
259 : chipBuilder: chipBuilder,
260 : );
261 : }
262 :
263 1 : @override
264 : _State createState() {
265 1 : return _State();
266 : }
267 : }
268 :
269 : /// Item builder
270 : abstract class DelegateBuilder {
271 : /// called when the tab item is build
272 : Widget build(BuildContext context, int index, bool active);
273 :
274 : /// whether the convex shape is fixed center or positioned according to selection
275 1 : bool fixed() {
276 : return false;
277 : }
278 : }
279 :
280 : class _State extends State<ConvexAppBar> with TickerProviderStateMixin {
281 : int _currentSelectedIndex;
282 : Animation<double> _animation;
283 : AnimationController _controller;
284 :
285 1 : @override
286 : void initState() {
287 3 : _currentSelectedIndex = widget.initialActiveIndex ?? 0;
288 1 : if (!isFixed()) {
289 1 : _initAnimation();
290 : }
291 1 : super.initState();
292 : }
293 :
294 1 : Animation<double> _initAnimation({int from, int to}) {
295 1 : if (from != null && (from == to)) {
296 1 : return _animation;
297 : }
298 2 : from ??= widget.initialActiveIndex ?? 0;
299 : to ??= from;
300 6 : var lower = (2 * from + 1) / (2 * widget.count);
301 6 : var upper = (2 * to + 1) / (2 * widget.count);
302 2 : _controller = AnimationController(
303 1 : duration: Duration(milliseconds: 150),
304 : vsync: this,
305 : );
306 1 : final Animation curve = CurvedAnimation(
307 1 : parent: _controller,
308 2 : curve: widget.curve,
309 : );
310 3 : _animation = Tween(begin: lower, end: upper).animate(curve);
311 1 : return _animation;
312 : }
313 :
314 1 : @override
315 : void dispose() {
316 2 : _controller?.dispose();
317 1 : super.dispose();
318 : }
319 :
320 1 : @override
321 : Widget build(BuildContext context) {
322 : // take care of iPhoneX' safe area at bottom edge
323 : final double additionalBottomPadding =
324 4 : math.max(MediaQuery.of(context).padding.bottom, 0.0);
325 3 : var halfSize = widget.count ~/ 2;
326 2 : final convexIndex = isFixed() ? halfSize : _currentSelectedIndex;
327 3 : final active = isFixed() ? convexIndex == _currentSelectedIndex : true;
328 1 : return extend.Stack(
329 : overflow: Overflow.visible,
330 : alignment: Alignment.bottomCenter,
331 1 : children: <Widget>[
332 1 : Container(
333 3 : height: widget.height ?? BAR_HEIGHT + additionalBottomPadding,
334 3 : width: MediaQuery.of(context).size.width,
335 1 : child: CustomPaint(
336 1 : painter: ConvexPainter(
337 2 : top: widget.top ?? CURVE_TOP,
338 2 : width: widget.curveSize ?? CONVEX_SIZE,
339 2 : height: widget.curveSize ?? CONVEX_SIZE,
340 2 : color: widget.backgroundColor ?? Colors.blue,
341 2 : gradient: widget.gradient,
342 2 : sigma: widget.elevation ?? ELEVATION,
343 1 : leftPercent: isFixed()
344 : ? const AlwaysStoppedAnimation<double>(0.5)
345 1 : : _animation ?? _initAnimation(),
346 : ),
347 : ),
348 : ),
349 1 : barContent(additionalBottomPadding),
350 1 : Positioned.fill(
351 2 : top: widget.top,
352 : bottom: additionalBottomPadding,
353 1 : child: FractionallySizedBox(
354 3 : widthFactor: 1 / widget.count,
355 3 : alignment: Alignment((convexIndex - halfSize) / (halfSize), 0),
356 1 : child: GestureDetector(
357 1 : child: _newTab(convexIndex, active),
358 1 : onTap: () {
359 1 : _onTabClick(convexIndex);
360 2 : setState(() {
361 1 : _currentSelectedIndex = convexIndex;
362 : });
363 : },
364 : )),
365 : ),
366 : ],
367 : );
368 : }
369 :
370 4 : bool isFixed() => widget.itemBuilder.fixed();
371 :
372 1 : Widget barContent(double paddingBottom) {
373 1 : List<Widget> children = [];
374 : // add placeholder Widget
375 5 : var curveTabIndex = isFixed() ? widget.count ~/ 2 : _currentSelectedIndex;
376 4 : for (var i = 0; i < widget.count; i++) {
377 1 : if (i == curveTabIndex) {
378 3 : children.add(Expanded(child: Container()));
379 : continue;
380 : }
381 2 : var active = _currentSelectedIndex == i;
382 1 : Widget child = _newTab(i, active);
383 2 : children.add(Expanded(
384 1 : child: GestureDetector(
385 : behavior: HitTestBehavior.opaque,
386 : child: child,
387 1 : onTap: () {
388 1 : _onTabClick(i);
389 2 : setState(() {
390 1 : _currentSelectedIndex = i;
391 : });
392 : },
393 : )));
394 : }
395 :
396 1 : return Container(
397 3 : height: widget.height ?? BAR_HEIGHT + paddingBottom,
398 1 : padding: EdgeInsets.only(bottom: paddingBottom),
399 1 : child: Row(
400 : mainAxisSize: MainAxisSize.max,
401 : crossAxisAlignment: CrossAxisAlignment.center,
402 : children: children,
403 : ),
404 : );
405 : }
406 :
407 1 : Widget _newTab(int i, bool active) {
408 4 : var child = widget.itemBuilder.build(context, i, active);
409 2 : if (widget.chipBuilder != null) {
410 4 : child = widget.chipBuilder.build(context, child, i, active);
411 : }
412 : return child;
413 : }
414 :
415 1 : void _onTabClick(int i) {
416 2 : _initAnimation(from: _currentSelectedIndex, to: i);
417 2 : _controller?.forward();
418 2 : if (widget.onTap != null) {
419 2 : widget.onTap(i);
420 : }
421 : }
422 : }
423 :
424 : typedef GestureTapIndexCallback = void Function(int index);
425 : typedef CustomTabBuilder = Widget Function(
426 : BuildContext context, int index, bool active);
|