dev
dart 331 lines 13.2 KB
Raw
1 import 'dart:io';
2 import 'dart:ui';
3 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
4 import 'package:flutter/material.dart';
5 import 'package:flutter_mobx/flutter_mobx.dart';
6 import 'package:flutter_svg/flutter_svg.dart';
7 import 'package:cake_wallet/entities/new_main_actions.dart';
8 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
9
10 class NewMainNavBar extends StatefulWidget {
11 const NewMainNavBar({
12 super.key,
13 required this.dashboardViewModel,
14 required this.selectedIndex,
15 required this.onItemTap,
16 });
17
18 final DashboardViewModel dashboardViewModel;
19 final int selectedIndex;
20 final Function(int index) onItemTap;
21
22 static const barHeight = 68.0;
23 static final barBottomPadding = Platform.isIOS ? 22.0 : 12.0;
24
25 @override
26 State<NewMainNavBar> createState() => _NEWNewMainNavBarState();
27 }
28
29 class _NEWNewMainNavBarState extends State<NewMainNavBar> {
30 static const iconBoxWidth = 48.0;
31 static const iconWidth = 28.0;
32 static const iconHeight = 28.0;
33 static const iconHorizontalPadding = 18.0;
34
35 static const pillIconWidth = 24.0;
36 static const pillIconHeight = 24.0;
37 static const pillIconSpacing = 18.0;
38 static const pillHorizontalPadding = 20.0;
39
40 static const barBorderRadius = 50.0;
41 static const pillBorderRadius = 50.0;
42
43 static const barHorizontalPadding = 6.0;
44
45 static const barResizeDuration = Duration(milliseconds: 300);
46 static const inactiveIconMoveDuration = Duration(milliseconds: 300);
47 static const inactiveIconFadeDuration = Duration(milliseconds: 300);
48 static const inactiveIconAppearDuration = Duration(milliseconds: 300);
49 static const pillMoveDuration = Duration(milliseconds: 250);
50 static const pillResizeDuration = Duration(milliseconds: 250);
51 static const iconColorChangeDuration = Duration(milliseconds: 200);
52
53 static const pillTextStyle = TextStyle(
54 fontSize: 14,
55 fontWeight: FontWeight.w500,
56 );
57
58 bool _firstFrame = true;
59
60 @override
61 void initState() {
62 super.initState();
63
64 WidgetsBinding.instance.addPostFrameCallback((_) {
65 if (!mounted) return;
66 setState(() => _firstFrame = false);
67 });
68 }
69
70 void _onItemTap(int index) {
71 // if (index == widget.selectedIndex) return;
72 //
73 // setState(() {
74 // widget.selectedIndex = index;
75 // });
76
77 widget.onItemTap(index);
78
79 NewMainActions.all[index].onTap.call();
80 }
81
82 // Estimate pill width based on text length
83 double _estimatePillWidthForAction(BuildContext context, NewMainActions action, {Color? color}) {
84 final text = action.name(context);
85 final textPainter = TextPainter(
86 text: TextSpan(
87 text: text,
88 style: pillTextStyle.copyWith(color: color),
89 ),
90 maxLines: 1,
91 textDirection: TextDirection.ltr,
92 )..layout();
93 return pillIconWidth + pillIconSpacing + textPainter.width + pillHorizontalPadding;
94 }
95
96 double calcLeft(int index, double pillWidth) {
97 final baseOffset = iconBoxWidth * index;
98
99 double additionalSpacing;
100 if (index > widget.selectedIndex)
101 additionalSpacing = pillWidth - iconBoxWidth;
102 else
103 additionalSpacing = 0;
104
105 return baseOffset + additionalSpacing;
106 }
107
108 double calcBarWidth(double pillWidth, int visibleActionsLength) {
109 return (iconWidth + iconHorizontalPadding) * visibleActionsLength +
110 (pillWidth - iconWidth) +
111 barHorizontalPadding +
112 pillIconSpacing / double.infinity -
113 2;
114 }
115
116 @override
117 Widget build(BuildContext context) {
118 final theme = Theme.of(context);
119 final backgroundColor = theme.colorScheme.surfaceContainer.withAlpha(127);
120 final pillColor = theme.colorScheme.onSurface.withAlpha(25);
121 final activeColor = theme.colorScheme.onSurface;
122 final inactiveColor = theme.colorScheme.primary;
123
124 return Observer(
125 builder: (_) {
126 final visibleActions = NewMainActions.all
127 .where((action) => action.canShow?.call(widget.dashboardViewModel) ?? true)
128 .toList();
129
130 final pillWidth = _estimatePillWidthForAction(context, visibleActions[widget.selectedIndex],
131 color: activeColor);
132
133 final barWidth = calcBarWidth(pillWidth, visibleActions.length);
134
135 final currentAction = visibleActions[widget.selectedIndex];
136
137 return Align(
138 alignment: Alignment.bottomCenter,
139 child: SafeArea(
140 bottom: !(Platform.isIOS),
141 top: false,
142 child: Padding(
143 // tux PLEASE consult me (malik) before removing this padding.
144 padding: EdgeInsets.only(bottom: NewMainNavBar.barBottomPadding),
145 child: AnimatedContainer(
146 duration: barResizeDuration,
147 curve: Curves.easeOutCubic,
148 width: barWidth,
149 child: ClipRSuperellipse(
150 borderRadius: BorderRadius.circular(barBorderRadius),
151 child: BackdropFilter(
152 filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
153 child: Container(
154 height: NewMainNavBar.barHeight,
155 decoration: ShapeDecoration(
156 color: backgroundColor,
157 shape: RoundedSuperellipseBorder(
158 borderRadius: BorderRadius.circular(barBorderRadius),
159 side: const BorderSide(color: Color(0x14FFFFFF), width: 1),
160 ),
161 ),
162 child: Padding(
163 padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding),
164 child: Stack(
165 alignment: Alignment.center,
166 children: [
167 AnimatedPill(
168 left: calcLeft(widget.selectedIndex, pillWidth),
169 pillColor: pillColor,
170 currentAction: currentAction,
171 pillIconHeight: pillIconHeight,
172 pillIconWidth: pillIconWidth,
173 pillIconSpacing: pillIconSpacing,
174 pillBorderRadius: pillBorderRadius,
175 contentColor: activeColor,
176 estimateWidthForAction: pillWidth,
177 pillTextStyle: pillTextStyle,
178 pillMoveDuration: pillMoveDuration,
179 pillResizeDuration: pillResizeDuration,
180 ),
181 for (int i = 0; i < visibleActions.length; i++)
182 AnimatedPositioned(
183 duration: pillResizeDuration,
184 width: iconBoxWidth,
185 left: calcLeft(i, pillWidth) +
186 ((i == widget.selectedIndex)
187 ? iconHorizontalPadding / 100
188 : 0),
189 curve: Curves.easeOutCubic,
190 child: Semantics(
191 button: true,
192 selected: i == widget.selectedIndex,
193 inMutuallyExclusiveGroup: true,
194 label: visibleActions[i].name(context),
195 child: InkWell(
196 splashFactory: NoSplash.splashFactory,
197 splashColor: Colors.transparent,
198 borderRadius: BorderRadius.circular(pillBorderRadius),
199 onTap: () => _onItemTap(i),
200 child: AnimatedContainer(
201 duration:
202 _firstFrame ? Duration.zero : inactiveIconMoveDuration,
203 curve: Curves.easeOutCubic,
204 width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
205 alignment: Alignment.center,
206 child: AnimatedAlign(
207 duration: inactiveIconFadeDuration,
208 curve: Curves.easeOutCubic,
209 alignment: Alignment.center,
210 child: AnimatedScale(
211 duration: inactiveIconAppearDuration,
212 curve: Curves.easeOutCubic,
213 scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
214 child: TweenAnimationBuilder<Color?>(
215 tween: ColorTween(
216 begin: (i == widget.selectedIndex)
217 ? inactiveColor
218 : activeColor,
219 end: (i == widget.selectedIndex)
220 ? activeColor
221 : inactiveColor,
222 ),
223 duration: iconColorChangeDuration,
224 builder: (context, value, child) {
225 return Container(
226 height: NewMainNavBar.barHeight,
227 child: CakeImageWidget(
228 imageUrl: visibleActions[i].image,
229 width: iconWidth,
230 height: iconHeight,
231 //fit: BoxFit.scaleDown,
232 colorFilter: ColorFilter.mode(
233 value ?? inactiveColor,
234 BlendMode.srcIn,
235 ),
236 ),
237 );
238 }),
239 ),
240 ),
241 ),
242 ),
243 ),
244 ),
245 ],
246 ),
247 )),
248 ),
249 ),
250 ),
251 ),
252 ),
253 );
254 },
255 );
256 }
257 }
258
259 class AnimatedPill extends StatelessWidget {
260 const AnimatedPill({
261 super.key,
262 required this.left,
263 required this.pillColor,
264 required this.contentColor,
265 required this.pillIconHeight,
266 required this.pillIconWidth,
267 required this.pillBorderRadius,
268 required this.currentAction,
269 required this.estimateWidthForAction,
270 required this.pillIconSpacing,
271 required this.pillTextStyle,
272 required this.pillMoveDuration,
273 required this.pillResizeDuration,
274 });
275
276 final double left;
277 final Color pillColor;
278 final Color contentColor;
279 final double pillIconHeight;
280 final double pillIconWidth;
281 final double pillBorderRadius;
282 final NewMainActions currentAction;
283 final double estimateWidthForAction;
284 final double pillIconSpacing;
285 final TextStyle pillTextStyle;
286 final Duration pillMoveDuration;
287 final Duration pillResizeDuration;
288
289 @override
290 Widget build(BuildContext context) {
291 return AnimatedPositioned(
292 duration: pillMoveDuration,
293 curve: Curves.easeOutCubic,
294 left: left,
295 top: 6,
296 bottom: 6,
297 child: AnimatedContainer(
298 duration: pillResizeDuration,
299 curve: Curves.easeOutCubic,
300 width: estimateWidthForAction,
301 decoration: ShapeDecoration(
302 color: pillColor,
303 shape: RoundedSuperellipseBorder(borderRadius: BorderRadius.circular(pillBorderRadius)),
304 ),
305 clipBehavior: Clip.hardEdge,
306 alignment: Alignment.center,
307 child: FittedBox(
308 fit: BoxFit.scaleDown,
309 child: Row(
310 mainAxisSize: MainAxisSize.min,
311 mainAxisAlignment: MainAxisAlignment.center,
312 children: [
313 Padding(
314 padding: EdgeInsets.only(left: pillIconWidth + 2),
315 // The selected tab's InkWell already announces this name, so the
316 // pill text must not become a second stop for screen readers.
317 child: ExcludeSemantics(
318 child: Text(
319 currentAction.name(context),
320 style: pillTextStyle.copyWith(color: contentColor),
321 overflow: TextOverflow.fade,
322 softWrap: false,
323 ),
324 ),
325 ),
326 ],
327 ),
328 ),
329 ));
330 }
331 }