blob: f60a0421d996394a0e2f0890e242bfd592b857b1 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. 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 '../analytics/analytics.dart' as ga;
import '../shared/common_widgets.dart';
import '../shared/theme.dart';
import '../shared/utils.dart';
double get _tabHeight => scaleByFontFactor(46.0);
double get _textAndIconTabHeight => scaleByFontFactor(72.0);
class DevToolsTab extends Tab {
/// Creates a material design [TabBar] tab styled for DevTools.
///
/// The only difference is this tab makes more of an effort to reflect
/// changes in font and icon sizes.
DevToolsTab._({
required Key key,
String? text,
Icon? icon,
EdgeInsets iconMargin = const EdgeInsets.only(bottom: 10.0),
required this.gaId,
this.trailing,
Widget? child,
}) : assert(text != null || child != null || icon != null),
assert(text == null || child == null),
super(
key: key,
text: text,
icon: icon,
iconMargin: iconMargin,
height: calculateHeight(icon, text, child),
child: child,
);
factory DevToolsTab.create({
Key? key,
required String tabName,
required String gaPrefix,
Widget? trailing,
}) {
return DevToolsTab._(
key: key ?? ValueKey<String>(tabName),
gaId: '${gaPrefix}_$tabName',
trailing: trailing,
child: Text(
tabName,
overflow: TextOverflow.ellipsis,
),
);
}
static double calculateHeight(Icon? icon, String? text, Widget? child) {
if (icon == null || (text == null && child == null)) {
return _tabHeight;
} else {
return _textAndIconTabHeight;
}
}
/// Tab id for google analytics.
final String gaId;
final Widget? trailing;
}
/// A combined [TabBar] and [TabBarView] implementation that tracks tab changes
/// to our analytics.
///
/// When using this widget, ensure that the [AnalyticsTabbedView] is not being
/// rebuilt unnecessarily, as each call to [initState] and [didUpdateWidget]
/// will send an event to analytics for the default selected tab.
class AnalyticsTabbedView<T> extends StatefulWidget {
AnalyticsTabbedView({
Key? key,
required this.tabs,
required this.tabViews,
required this.gaScreen,
this.outlined = true,
this.sendAnalytics = true,
}) : trailingWidgets = List.generate(
tabs.length,
(index) => tabs[index].trailing ?? const SizedBox(),
),
super(key: key);
final List<DevToolsTab> tabs;
final List<Widget> tabViews;
final String gaScreen;
final List<Widget> trailingWidgets;
final bool outlined;
/// Whether to send analytics events to GA.
///
/// Only set this to false if [AnalyticsTabbedView] is being used for
/// experimental code we do not want to send GA events for yet.
final bool sendAnalytics;
@override
_AnalyticsTabbedViewState createState() => _AnalyticsTabbedViewState();
}
class _AnalyticsTabbedViewState extends State<AnalyticsTabbedView>
with TickerProviderStateMixin {
TabController? _tabController;
int _currentTabControllerIndex = 0;
void _initTabController() {
_tabController?.removeListener(_onTabChanged);
_tabController?.dispose();
_tabController = TabController(
length: widget.tabs.length,
vsync: this,
);
if (_currentTabControllerIndex >= _tabController!.length) {
_currentTabControllerIndex = 0;
}
_tabController!
..index = _currentTabControllerIndex
..addListener(_onTabChanged);
// Record a selection for the visible tab.
if (widget.sendAnalytics) {
ga.select(
widget.gaScreen,
widget.tabs[_currentTabControllerIndex].gaId,
nonInteraction: true,
);
}
}
void _onTabChanged() {
if (_currentTabControllerIndex != _tabController!.index) {
setState(() {
_currentTabControllerIndex = _tabController!.index;
});
if (widget.sendAnalytics) {
ga.select(
widget.gaScreen,
widget.tabs[_currentTabControllerIndex].gaId,
);
}
}
}
@override
void initState() {
super.initState();
_initTabController();
}
@override
void didUpdateWidget(AnalyticsTabbedView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.tabs != widget.tabs ||
oldWidget.gaScreen != widget.gaScreen) {
_initTabController();
}
}
@override
void dispose() {
_tabController?.removeListener(_onTabChanged);
_tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final borderSide = defaultBorderSide(theme);
Widget tabBar = Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TabBar(
labelColor: theme.textTheme.bodyText1?.color,
controller: _tabController,
tabs: widget.tabs,
isScrollable: true,
),
Expanded(
child: widget.trailingWidgets[_currentTabControllerIndex],
),
],
);
if (widget.outlined) {
tabBar = Container(
height: defaultButtonHeight +
(isDense() ? denseModeDenseSpacing : denseSpacing),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).focusColor),
),
child: tabBar,
);
}
Widget tabView = TabBarView(
physics: defaultTabBarViewPhysics,
controller: _tabController,
children: widget.tabViews,
);
if (widget.outlined) {
tabView = Container(
decoration: BoxDecoration(
border: Border(
left: borderSide,
bottom: borderSide,
right: borderSide,
),
),
child: tabView,
);
}
return Column(
children: [
tabBar,
Expanded(
child: tabView,
),
],
);
}
}