1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/material.dart';
6
7import '../../data/gallery_options.dart';
8import '../../gallery_localizations.dart';
9import '../../layout/adaptive.dart';
10import '../../layout/text_scale.dart';
11import 'tabs/accounts.dart';
12import 'tabs/bills.dart';
13import 'tabs/budgets.dart';
14import 'tabs/overview.dart';
15import 'tabs/settings.dart';
16
17const int tabCount = 5;
18const int turnsToRotateRight = 1;
19const int turnsToRotateLeft = 3;
20
21class HomePage extends StatefulWidget {
22 const HomePage({super.key});
23
24 @override
25 State<HomePage> createState() => _HomePageState();
26}
27
28class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin, RestorationMixin {
29 late TabController _tabController;
30 RestorableInt tabIndex = RestorableInt(0);
31
32 @override
33 String get restorationId => 'home_page';
34
35 @override
36 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
37 registerForRestoration(tabIndex, 'tab_index');
38 _tabController.index = tabIndex.value;
39 }
40
41 @override
42 void initState() {
43 super.initState();
44 _tabController = TabController(length: tabCount, vsync: this)..addListener(() {
45 // Set state to make sure that the [_RallyTab] widgets get updated when changing tabs.
46 setState(() {
47 tabIndex.value = _tabController.index;
48 });
49 });
50 }
51
52 @override
53 void dispose() {
54 _tabController.dispose();
55 tabIndex.dispose();
56 super.dispose();
57 }
58
59 @override
60 Widget build(BuildContext context) {
61 final ThemeData theme = Theme.of(context);
62 final bool isDesktop = isDisplayDesktop(context);
63 Widget tabBarView;
64 if (isDesktop) {
65 final bool isTextDirectionRtl =
66 GalleryOptions.of(context).resolvedTextDirection() == TextDirection.rtl;
67 final int verticalRotation = isTextDirectionRtl ? turnsToRotateLeft : turnsToRotateRight;
68 final int revertVerticalRotation =
69 isTextDirectionRtl ? turnsToRotateRight : turnsToRotateLeft;
70 tabBarView = Row(
71 children: <Widget>[
72 Container(
73 width: 150 + 50 * (cappedTextScale(context) - 1),
74 alignment: Alignment.topCenter,
75 padding: const EdgeInsets.symmetric(vertical: 32),
76 child: Column(
77 children: <Widget>[
78 const SizedBox(height: 24),
79 ExcludeSemantics(
80 child: SizedBox(
81 height: 80,
82 child: Image.asset('logo.png', package: 'rally_assets'),
83 ),
84 ),
85 const SizedBox(height: 24),
86 // Rotate the tab bar, so the animation is vertical for desktops.
87 RotatedBox(
88 quarterTurns: verticalRotation,
89 child: _RallyTabBar(
90 tabs:
91 _buildTabs(context: context, theme: theme, isVertical: true).map((
92 Widget widget,
93 ) {
94 // Revert the rotation on the tabs.
95 return RotatedBox(quarterTurns: revertVerticalRotation, child: widget);
96 }).toList(),
97 tabController: _tabController,
98 ),
99 ),
100 ],
101 ),
102 ),
103 Expanded(
104 // Rotate the tab views so we can swipe up and down.
105 child: RotatedBox(
106 quarterTurns: verticalRotation,
107 child: TabBarView(
108 controller: _tabController,
109 children:
110 _buildTabViews().map((Widget widget) {
111 // Revert the rotation on the tab views.
112 return RotatedBox(quarterTurns: revertVerticalRotation, child: widget);
113 }).toList(),
114 ),
115 ),
116 ),
117 ],
118 );
119 } else {
120 tabBarView = Column(
121 children: <Widget>[
122 _RallyTabBar(
123 tabs: _buildTabs(context: context, theme: theme),
124 tabController: _tabController,
125 ),
126 Expanded(child: TabBarView(controller: _tabController, children: _buildTabViews())),
127 ],
128 );
129 }
130 return ApplyTextOptions(
131 child: Scaffold(
132 body: SafeArea(
133 // For desktop layout we do not want to have SafeArea at the top and
134 // bottom to display 100% height content on the accounts view.
135 top: !isDesktop,
136 bottom: !isDesktop,
137 child: Theme(
138 // This theme effectively removes the default visual touch
139 // feedback for tapping a tab, which is replaced with a custom
140 // animation.
141 data: theme.copyWith(
142 splashColor: Colors.transparent,
143 highlightColor: Colors.transparent,
144 ),
145 child: FocusTraversalGroup(policy: OrderedTraversalPolicy(), child: tabBarView),
146 ),
147 ),
148 ),
149 );
150 }
151
152 List<Widget> _buildTabs({
153 required BuildContext context,
154 required ThemeData theme,
155 bool isVertical = false,
156 }) {
157 final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
158 return <Widget>[
159 _RallyTab(
160 theme: theme,
161 iconData: Icons.pie_chart,
162 title: localizations.rallyTitleOverview,
163 tabIndex: 0,
164 tabController: _tabController,
165 isVertical: isVertical,
166 ),
167 _RallyTab(
168 theme: theme,
169 iconData: Icons.attach_money,
170 title: localizations.rallyTitleAccounts,
171 tabIndex: 1,
172 tabController: _tabController,
173 isVertical: isVertical,
174 ),
175 _RallyTab(
176 theme: theme,
177 iconData: Icons.money_off,
178 title: localizations.rallyTitleBills,
179 tabIndex: 2,
180 tabController: _tabController,
181 isVertical: isVertical,
182 ),
183 _RallyTab(
184 theme: theme,
185 iconData: Icons.table_chart,
186 title: localizations.rallyTitleBudgets,
187 tabIndex: 3,
188 tabController: _tabController,
189 isVertical: isVertical,
190 ),
191 _RallyTab(
192 theme: theme,
193 iconData: Icons.settings,
194 title: localizations.rallyTitleSettings,
195 tabIndex: 4,
196 tabController: _tabController,
197 isVertical: isVertical,
198 ),
199 ];
200 }
201
202 List<Widget> _buildTabViews() {
203 return const <Widget>[
204 OverviewView(),
205 AccountsView(),
206 BillsView(),
207 BudgetsView(),
208 SettingsView(),
209 ];
210 }
211}
212
213class _RallyTabBar extends StatelessWidget {
214 const _RallyTabBar({required this.tabs, this.tabController});
215
216 final List<Widget> tabs;
217 final TabController? tabController;
218
219 @override
220 Widget build(BuildContext context) {
221 return FocusTraversalOrder(
222 order: const NumericFocusOrder(0),
223 child: TabBar(
224 // Setting isScrollable to true prevents the tabs from being
225 // wrapped in [Expanded] widgets, which allows for more
226 // flexible sizes and size animations among tabs.
227 isScrollable: true,
228 labelPadding: EdgeInsets.zero,
229 tabs: tabs,
230 controller: tabController,
231 // This hides the tab indicator.
232 indicatorColor: Colors.transparent,
233 ),
234 );
235 }
236}
237
238class _RallyTab extends StatefulWidget {
239 _RallyTab({
240 required ThemeData theme,
241 IconData? iconData,
242 required String title,
243 int? tabIndex,
244 required TabController tabController,
245 required this.isVertical,
246 }) : titleText = Text(title, style: theme.textTheme.labelLarge),
247 isExpanded = tabController.index == tabIndex,
248 icon = Icon(iconData, semanticLabel: title);
249
250 final Text titleText;
251 final Icon icon;
252 final bool isExpanded;
253 final bool isVertical;
254
255 @override
256 _RallyTabState createState() => _RallyTabState();
257}
258
259class _RallyTabState extends State<_RallyTab> with SingleTickerProviderStateMixin {
260 late Animation<double> _titleSizeAnimation;
261 late Animation<double> _titleFadeAnimation;
262 late Animation<double> _iconFadeAnimation;
263 late AnimationController _controller;
264
265 @override
266 void initState() {
267 super.initState();
268 _controller = AnimationController(duration: const Duration(milliseconds: 200), vsync: this);
269 _titleSizeAnimation = _controller.view;
270 _titleFadeAnimation = _controller.drive(CurveTween(curve: Curves.easeOut));
271 _iconFadeAnimation = _controller.drive(Tween<double>(begin: 0.6, end: 1));
272 if (widget.isExpanded) {
273 _controller.value = 1;
274 }
275 }
276
277 @override
278 void didUpdateWidget(_RallyTab oldWidget) {
279 super.didUpdateWidget(oldWidget);
280 if (widget.isExpanded) {
281 _controller.forward();
282 } else {
283 _controller.reverse();
284 }
285 }
286
287 @override
288 Widget build(BuildContext context) {
289 if (widget.isVertical) {
290 return Column(
291 children: <Widget>[
292 const SizedBox(height: 18),
293 FadeTransition(opacity: _iconFadeAnimation, child: widget.icon),
294 const SizedBox(height: 12),
295 FadeTransition(
296 opacity: _titleFadeAnimation,
297 child: SizeTransition(
298 axisAlignment: -1,
299 sizeFactor: _titleSizeAnimation,
300 child: Center(child: ExcludeSemantics(child: widget.titleText)),
301 ),
302 ),
303 const SizedBox(height: 18),
304 ],
305 );
306 }
307
308 // Calculate the width of each unexpanded tab by counting the number of
309 // units and dividing it into the screen width. Each unexpanded tab is 1
310 // unit, and there is always 1 expanded tab which is 1 unit + any extra
311 // space determined by the multiplier.
312 final double width = MediaQuery.of(context).size.width;
313 const int expandedTitleWidthMultiplier = 2;
314 final double unitWidth = width / (tabCount + expandedTitleWidthMultiplier);
315
316 return ConstrainedBox(
317 constraints: const BoxConstraints(minHeight: 56),
318 child: Row(
319 children: <Widget>[
320 FadeTransition(
321 opacity: _iconFadeAnimation,
322 child: SizedBox(width: unitWidth, child: widget.icon),
323 ),
324 FadeTransition(
325 opacity: _titleFadeAnimation,
326 child: SizeTransition(
327 axis: Axis.horizontal,
328 axisAlignment: -1,
329 sizeFactor: _titleSizeAnimation,
330 child: SizedBox(
331 width: unitWidth * expandedTitleWidthMultiplier,
332 child: Center(child: ExcludeSemantics(child: widget.titleText)),
333 ),
334 ),
335 ),
336 ],
337 ),
338 );
339 }
340
341 @override
342 void dispose() {
343 _controller.dispose();
344 super.dispose();
345 }
346}
347

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com