blob: 1da03f53bc695795f5c59fdd5365c71cf25c4cb2 [file] [log] [blame]
// Copyright 2019 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/l10n/gallery_localizations.dart';
import 'package:gallery/layout/adaptive.dart';
import 'package:gallery/layout/text_scale.dart';
import 'package:gallery/pages/home.dart';
import 'package:gallery/layout/focus_traversal_policy.dart';
import 'package:gallery/studies/rally/tabs/accounts.dart';
import 'package:gallery/studies/rally/tabs/bills.dart';
import 'package:gallery/studies/rally/tabs/budgets.dart';
import 'package:gallery/studies/rally/tabs/overview.dart';
import 'package:gallery/studies/rally/tabs/settings.dart';
const int tabCount = 5;
const int turnsToRotateRight = 1;
const int turnsToRotateLeft = 3;
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with SingleTickerProviderStateMixin {
TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: tabCount, vsync: this)
..addListener(() {
// Set state to make sure that the [_RallyTab] widgets get updated when changing tabs.
setState(() {});
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final isDesktop = isDisplayDesktop(context);
Widget tabBarView;
if (isDesktop) {
final isTextDirectionRtl =
GalleryOptions.of(context).textDirection() == TextDirection.rtl;
final verticalRotation =
isTextDirectionRtl ? turnsToRotateLeft : turnsToRotateRight;
final revertVerticalRotation =
isTextDirectionRtl ? turnsToRotateRight : turnsToRotateLeft;
tabBarView = Row(
children: [
Container(
width: 150 + 50 * (cappedTextScale(context) - 1),
alignment: Alignment.topCenter,
padding: const EdgeInsets.symmetric(vertical: 32),
child: Column(
children: [
const SizedBox(height: 24),
ExcludeSemantics(
child: SizedBox(
height: 80,
child: Image.asset(
'logo.png',
package: 'rally_assets',
),
),
),
const SizedBox(height: 24),
// Rotate the tab bar, so the animation is vertical for desktops.
RotatedBox(
quarterTurns: verticalRotation,
child: _RallyTabBar(
tabs: _buildTabs(
context: context, theme: theme, isVertical: true)
.map(
(widget) {
// Revert the rotation on the tabs.
return RotatedBox(
quarterTurns: revertVerticalRotation,
child: widget,
);
},
).toList(),
tabController: _tabController,
),
),
],
),
),
Expanded(
// Rotate the tab views so we can swipe up and down.
child: RotatedBox(
quarterTurns: verticalRotation,
child: TabBarView(
controller: _tabController,
children: _buildTabViews().map(
(widget) {
// Revert the rotation on the tab views.
return RotatedBox(
quarterTurns: revertVerticalRotation,
child: widget,
);
},
).toList(),
),
),
),
],
);
} else {
tabBarView = Column(
children: [
_RallyTabBar(
tabs: _buildTabs(context: context, theme: theme),
tabController: _tabController,
),
Expanded(
child: TabBarView(
controller: _tabController,
children: _buildTabViews(),
),
),
],
);
}
final backButtonFocusNode =
InheritedFocusNodes.of(context).backButtonFocusNode;
return DefaultFocusTraversal(
policy: EdgeChildrenFocusTraversalPolicy(
firstFocusNodeOutsideScope: backButtonFocusNode,
lastFocusNodeOutsideScope: backButtonFocusNode,
focusScope: FocusScope.of(context),
),
child: ApplyTextOptions(
child: Scaffold(
body: SafeArea(
// For desktop layout we do not want to have SafeArea at the top and
// bottom to display 100% height content on the accounts view.
top: !isDesktop,
bottom: !isDesktop,
child: Theme(
// This theme effectively removes the default visual touch
// feedback for tapping a tab, which is replaced with a custom
// animation.
data: theme.copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: tabBarView,
),
),
),
),
);
}
List<Widget> _buildTabs(
{BuildContext context, ThemeData theme, bool isVertical = false}) {
return [
_RallyTab(
theme: theme,
iconData: Icons.pie_chart,
title: GalleryLocalizations.of(context).rallyTitleOverview,
tabIndex: 0,
tabController: _tabController,
isVertical: isVertical,
),
_RallyTab(
theme: theme,
iconData: Icons.attach_money,
title: GalleryLocalizations.of(context).rallyTitleAccounts,
tabIndex: 1,
tabController: _tabController,
isVertical: isVertical,
),
_RallyTab(
theme: theme,
iconData: Icons.money_off,
title: GalleryLocalizations.of(context).rallyTitleBills,
tabIndex: 2,
tabController: _tabController,
isVertical: isVertical,
),
_RallyTab(
theme: theme,
iconData: Icons.table_chart,
title: GalleryLocalizations.of(context).rallyTitleBudgets,
tabIndex: 3,
tabController: _tabController,
isVertical: isVertical,
),
_RallyTab(
theme: theme,
iconData: Icons.settings,
title: GalleryLocalizations.of(context).rallyTitleSettings,
tabIndex: 4,
tabController: _tabController,
isVertical: isVertical,
),
];
}
List<Widget> _buildTabViews() {
return [
OverviewView(),
AccountsView(),
BillsView(),
BudgetsView(),
SettingsView(),
];
}
}
class _RallyTabBar extends StatelessWidget {
const _RallyTabBar({Key key, this.tabs, this.tabController})
: super(key: key);
final List<Widget> tabs;
final TabController tabController;
@override
Widget build(BuildContext context) {
return TabBar(
// Setting isScrollable to true prevents the tabs from being
// wrapped in [Expanded] widgets, which allows for more
// flexible sizes and size animations among tabs.
isScrollable: true,
labelPadding: EdgeInsets.zero,
tabs: tabs,
controller: tabController,
// This hides the tab indicator.
indicatorColor: Colors.transparent,
);
}
}
class _RallyTab extends StatefulWidget {
_RallyTab({
ThemeData theme,
IconData iconData,
String title,
int tabIndex,
TabController tabController,
this.isVertical,
}) : titleText = Text(title, style: theme.textTheme.button),
isExpanded = tabController.index == tabIndex,
icon = Icon(iconData, semanticLabel: title);
final Text titleText;
final Icon icon;
final bool isExpanded;
final bool isVertical;
@override
_RallyTabState createState() => _RallyTabState();
}
class _RallyTabState extends State<_RallyTab>
with SingleTickerProviderStateMixin {
Animation<double> _titleSizeAnimation;
Animation<double> _titleFadeAnimation;
Animation<double> _iconFadeAnimation;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_titleSizeAnimation = _controller.view;
_titleFadeAnimation = _controller.drive(CurveTween(curve: Curves.easeOut));
_iconFadeAnimation = _controller.drive(Tween<double>(begin: 0.6, end: 1));
if (widget.isExpanded) {
_controller.value = 1;
}
}
@override
void didUpdateWidget(_RallyTab oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isExpanded) {
_controller.forward();
} else {
_controller.reverse();
}
}
@override
Widget build(BuildContext context) {
if (widget.isVertical) {
return Column(
children: [
const SizedBox(height: 18),
FadeTransition(
child: widget.icon,
opacity: _iconFadeAnimation,
),
const SizedBox(height: 12),
FadeTransition(
child: SizeTransition(
child: Center(child: ExcludeSemantics(child: widget.titleText)),
axis: Axis.vertical,
axisAlignment: -1,
sizeFactor: _titleSizeAnimation,
),
opacity: _titleFadeAnimation,
),
const SizedBox(height: 18),
],
);
}
// Calculate the width of each unexpanded tab by counting the number of
// units and dividing it into the screen width. Each unexpanded tab is 1
// unit, and there is always 1 expanded tab which is 1 unit + any extra
// space determined by the multiplier.
final width = MediaQuery.of(context).size.width;
const expandedTitleWidthMultiplier = 2;
final unitWidth = width / (tabCount + expandedTitleWidthMultiplier);
return ConstrainedBox(
constraints: BoxConstraints(minHeight: 56),
child: Row(
children: [
FadeTransition(
child: SizedBox(
width: unitWidth,
child: widget.icon,
),
opacity: _iconFadeAnimation,
),
FadeTransition(
child: SizeTransition(
child: SizedBox(
width: unitWidth * expandedTitleWidthMultiplier,
child: Center(
child: ExcludeSemantics(child: widget.titleText),
),
),
axis: Axis.horizontal,
axisAlignment: -1,
sizeFactor: _titleSizeAnimation,
),
opacity: _titleFadeAnimation,
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}