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