mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add support for secondary tab bar (#122756)
Add support for secondary tab bar
This commit is contained in:
parent
ce68d97983
commit
02d5c7595b
@ -12,8 +12,8 @@ class TabsTemplate extends TokenTemplate {
|
||||
|
||||
@override
|
||||
String generate() => '''
|
||||
class _${blockName}DefaultsM3 extends TabBarTheme {
|
||||
_${blockName}DefaultsM3(this.context)
|
||||
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
|
||||
_${blockName}PrimaryDefaultsM3(this.context)
|
||||
: super(indicatorSize: TabBarIndicatorSize.label);
|
||||
|
||||
final BuildContext context;
|
||||
@ -69,5 +69,64 @@ class _${blockName}DefaultsM3 extends TabBarTheme {
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
''';
|
||||
|
||||
class _${blockName}SecondaryDefaultsM3 extends TabBarTheme {
|
||||
_${blockName}SecondaryDefaultsM3(this.context)
|
||||
: super(indicatorSize: TabBarIndicatorSize.tab);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
late final TextTheme _textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@override
|
||||
Color? get dividerColor => ${componentColor("md.comp.secondary-navigation-tab.divider")};
|
||||
|
||||
@override
|
||||
Color? get indicatorColor => ${componentColor("md.comp.primary-navigation-tab.active-indicator")};
|
||||
|
||||
@override
|
||||
Color? get labelColor => ${componentColor("md.comp.secondary-navigation-tab.active.label-text")};
|
||||
|
||||
@override
|
||||
TextStyle? get labelStyle => ${textStyle("md.comp.secondary-navigation-tab.label-text")};
|
||||
|
||||
@override
|
||||
Color? get unselectedLabelColor => ${componentColor("md.comp.secondary-navigation-tab.inactive.label-text")};
|
||||
|
||||
@override
|
||||
TextStyle? get unselectedLabelStyle => ${textStyle("md.comp.secondary-navigation-tab.label-text")};
|
||||
|
||||
@override
|
||||
MaterialStateProperty<Color?> get overlayColor {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.hover.state-layer')};
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.focus.state-layer')};
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.hover.state-layer')};
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.focus.state-layer')};
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return ${componentColor('md.comp.secondary-navigation-tab.pressed.state-layer')};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
''';
|
||||
|
||||
}
|
||||
|
@ -6,24 +6,22 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const MyApp());
|
||||
void main() => runApp(const TabBarApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
static const String _title = 'Flutter Code Sample';
|
||||
class TabBarApp extends StatelessWidget {
|
||||
const TabBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: _title,
|
||||
home: MyStatelessWidget(),
|
||||
return MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: const TabBarExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyStatelessWidget extends StatelessWidget {
|
||||
const MyStatelessWidget({super.key});
|
||||
class TabBarExample extends StatelessWidget {
|
||||
const TabBarExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -32,7 +30,7 @@ class MyStatelessWidget extends StatelessWidget {
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TabBar Widget'),
|
||||
title: const Text('TabBar Sample'),
|
||||
bottom: const TabBar(
|
||||
tabs: <Widget>[
|
||||
Tab(
|
||||
|
@ -6,34 +6,31 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const MyApp());
|
||||
void main() => runApp(const TabBarApp());
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
static const String _title = 'Flutter Code Sample';
|
||||
class TabBarApp extends StatelessWidget {
|
||||
const TabBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: _title,
|
||||
home: MyStatefulWidget(),
|
||||
return MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: const TabBarExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyStatefulWidget extends StatefulWidget {
|
||||
const MyStatefulWidget({super.key});
|
||||
class TabBarExample extends StatefulWidget {
|
||||
const TabBarExample({super.key});
|
||||
|
||||
@override
|
||||
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
|
||||
State<TabBarExample> createState() => _TabBarExampleState();
|
||||
}
|
||||
|
||||
/// [AnimationController]s can be created with `vsync: this` because of
|
||||
/// [TickerProviderStateMixin].
|
||||
class _MyStatefulWidgetState extends State<MyStatefulWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -41,11 +38,17 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget>
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('TabBar Widget'),
|
||||
title: const Text('TabBar Sample'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const <Widget>[
|
||||
|
117
examples/api/lib/material/tabs/tab_bar.2.dart
Normal file
117
examples/api/lib/material/tabs/tab_bar.2.dart
Normal file
@ -0,0 +1,117 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Flutter code sample for [TabBar].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const TabBarApp());
|
||||
|
||||
class TabBarApp extends StatelessWidget {
|
||||
const TabBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(useMaterial3: true),
|
||||
home: const TabBarExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarExample extends StatelessWidget {
|
||||
const TabBarExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
initialIndex: 1,
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Primary and secondary TabBar'),
|
||||
bottom: const TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: <Widget>[
|
||||
Tab(
|
||||
text: 'Flights',
|
||||
icon: Icon(Icons.flight),
|
||||
),
|
||||
Tab(
|
||||
text: 'Trips',
|
||||
icon: Icon(Icons.luggage),
|
||||
),
|
||||
Tab(
|
||||
text: 'Explore',
|
||||
icon: Icon(Icons.explore),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: const TabBarView(
|
||||
children: <Widget>[
|
||||
NestedTabBar('Flights'),
|
||||
NestedTabBar('Trips'),
|
||||
NestedTabBar('Explore'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NestedTabBar extends StatefulWidget {
|
||||
const NestedTabBar(this.outerTab, {super.key});
|
||||
|
||||
final String outerTab;
|
||||
|
||||
@override
|
||||
State<NestedTabBar> createState() => _NestedTabBarState();
|
||||
}
|
||||
|
||||
class _NestedTabBarState extends State<NestedTabBar> with TickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
TabBar.secondary(
|
||||
controller: _tabController,
|
||||
tabs: const <Widget>[
|
||||
Tab(text: 'Overview'),
|
||||
Tab(text: 'Specifications'),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: <Widget>[
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
child: Center(child: Text('${widget.outerTab}: Overview tab')),
|
||||
),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
child: Center(child: Text('${widget.outerTab}: Specifications tab')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
51
examples/api/test/material/tabs/tab_bar.0_test.dart
Normal file
51
examples/api/test/material/tabs/tab_bar.0_test.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2014 The Flutter 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 'package:flutter_api_samples/material/tabs/tab_bar.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TabBarApp(),
|
||||
);
|
||||
|
||||
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
|
||||
expect(tabBar.tabs.length, 3);
|
||||
|
||||
final Finder tab1 = find.widgetWithIcon(Tab, Icons.cloud_outlined);
|
||||
final Finder tab2 = find.widgetWithIcon(Tab, Icons.beach_access_sharp);
|
||||
final Finder tab3 = find.widgetWithIcon(Tab, Icons.brightness_5_sharp);
|
||||
|
||||
const String tabBarViewText1 = "It's cloudy here";
|
||||
const String tabBarViewText2 = "It's rainy here";
|
||||
const String tabBarViewText3 = "It's sunny here";
|
||||
|
||||
expect(find.text(tabBarViewText1), findsNothing);
|
||||
expect(find.text(tabBarViewText2), findsOneWidget);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab1);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsOneWidget);
|
||||
expect(find.text(tabBarViewText2), findsNothing);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsNothing);
|
||||
expect(find.text(tabBarViewText2), findsOneWidget);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab3);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsNothing);
|
||||
expect(find.text(tabBarViewText2), findsNothing);
|
||||
expect(find.text(tabBarViewText3), findsOneWidget);
|
||||
});
|
||||
}
|
51
examples/api/test/material/tabs/tab_bar.1_test.dart
Normal file
51
examples/api/test/material/tabs/tab_bar.1_test.dart
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2014 The Flutter 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 'package:flutter_api_samples/material/tabs/tab_bar.1.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.TabBarApp(),
|
||||
);
|
||||
|
||||
final TabBar tabBar = tester.widget<TabBar>(find.byType(TabBar));
|
||||
expect(tabBar.tabs.length, 3);
|
||||
|
||||
final Finder tab1 = find.widgetWithIcon(Tab, Icons.cloud_outlined);
|
||||
final Finder tab2 = find.widgetWithIcon(Tab, Icons.beach_access_sharp);
|
||||
final Finder tab3 = find.widgetWithIcon(Tab, Icons.brightness_5_sharp);
|
||||
|
||||
const String tabBarViewText1 = "It's cloudy here";
|
||||
const String tabBarViewText2 = "It's rainy here";
|
||||
const String tabBarViewText3 = "It's sunny here";
|
||||
|
||||
expect(find.text(tabBarViewText1), findsOneWidget);
|
||||
expect(find.text(tabBarViewText2), findsNothing);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab1);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsOneWidget);
|
||||
expect(find.text(tabBarViewText2), findsNothing);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsNothing);
|
||||
expect(find.text(tabBarViewText2), findsOneWidget);
|
||||
expect(find.text(tabBarViewText3), findsNothing);
|
||||
|
||||
await tester.tap(tab3);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(tabBarViewText1), findsNothing);
|
||||
expect(find.text(tabBarViewText2), findsNothing);
|
||||
expect(find.text(tabBarViewText3), findsOneWidget);
|
||||
});
|
||||
}
|
71
examples/api/test/material/tabs/tab_bar.2_test.dart
Normal file
71
examples/api/test/material/tabs/tab_bar.2_test.dart
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2014 The Flutter 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 'package:flutter_api_samples/material/tabs/tab_bar.2.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Switch tabs in the TabBar', (WidgetTester tester) async {
|
||||
const String primaryTabLabel1 = 'Flights';
|
||||
const String primaryTabLabel2 = 'Trips';
|
||||
const String primaryTabLabel3 = 'Explore';
|
||||
const String secondaryTabLabel1 = 'Overview';
|
||||
const String secondaryTabLabel2 = 'Specifications';
|
||||
|
||||
await tester.pumpWidget(
|
||||
const example.TabBarApp(),
|
||||
);
|
||||
|
||||
final TabBar primaryTabBar = tester.widget<TabBar>(find.byType(TabBar).last);
|
||||
expect(primaryTabBar.tabs.length, 3);
|
||||
|
||||
final TabBar secondaryTabBar = tester.widget<TabBar>(find.byType(TabBar).first);
|
||||
expect(secondaryTabBar.tabs.length, 2);
|
||||
|
||||
final Finder primaryTab1 = find.widgetWithText(Tab, primaryTabLabel1);
|
||||
final Finder primaryTab2 = find.widgetWithText(Tab, primaryTabLabel2);
|
||||
final Finder primaryTab3 = find.widgetWithText(Tab, primaryTabLabel3);
|
||||
final Finder secondaryTab2 = find.widgetWithText(Tab, secondaryTabLabel2);
|
||||
|
||||
String tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel1 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(primaryTab1);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel1: $secondaryTabLabel1 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(secondaryTab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel1: $secondaryTabLabel2 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(primaryTab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel1 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(secondaryTab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel2: $secondaryTabLabel2 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(primaryTab3);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel3: $secondaryTabLabel1 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
|
||||
await tester.tap(secondaryTab2);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tabBarViewText = '$primaryTabLabel3: $secondaryTabLabel2 tab';
|
||||
expect(find.text(tabBarViewText), findsOneWidget);
|
||||
});
|
||||
}
|
@ -167,24 +167,27 @@ class _TabStyle extends AnimatedWidget {
|
||||
const _TabStyle({
|
||||
required Animation<double> animation,
|
||||
required this.isSelected,
|
||||
required this.isPrimary,
|
||||
required this.labelColor,
|
||||
required this.unselectedLabelColor,
|
||||
required this.labelStyle,
|
||||
required this.unselectedLabelStyle,
|
||||
required this.defaults,
|
||||
required this.child,
|
||||
}) : super(listenable: animation);
|
||||
|
||||
final TextStyle? labelStyle;
|
||||
final TextStyle? unselectedLabelStyle;
|
||||
final bool isSelected;
|
||||
final bool isPrimary;
|
||||
final Color? labelColor;
|
||||
final Color? unselectedLabelColor;
|
||||
final TabBarTheme defaults;
|
||||
final Widget child;
|
||||
|
||||
MaterialStateColor _resolveWithLabelColor(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
||||
final Animation<double> animation = listenable as Animation<double>;
|
||||
|
||||
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
|
||||
@ -219,9 +222,7 @@ class _TabStyle extends AnimatedWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
final TabBarTheme defaults = themeData.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
||||
final Animation<double> animation = listenable as Animation<double>;
|
||||
|
||||
final Set<MaterialState> states = isSelected
|
||||
@ -604,7 +605,10 @@ class _TabBarScrollController extends ScrollController {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Material Design widget that displays a horizontal row of tabs.
|
||||
/// A Material Design primary tab bar.
|
||||
///
|
||||
/// Primary tabs are placed at the top of the content pane under a top app bar.
|
||||
/// They display the main content destinations.
|
||||
///
|
||||
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
|
||||
/// conjunction with a [TabBarView].
|
||||
@ -635,12 +639,23 @@ class _TabBarScrollController extends ScrollController {
|
||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample showcases nested Material 3 [TabBar]s. It consists of a primary
|
||||
/// [TabBar] with nested a secondary [TabBar]. The primary [TabBar] uses a
|
||||
/// [DefaultTabController] while the secondary [TabBar] uses a [TabController].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TabBar.secondary], for a secondary tab bar.
|
||||
/// * [TabBarView], which displays page views that correspond to each tab.
|
||||
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
|
||||
/// * https://m3.material.io/components/tab-bar/overview, the Material 3
|
||||
/// tab bar specification.
|
||||
class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Creates a Material Design tab bar.
|
||||
/// Creates a Material Design primary tab bar.
|
||||
///
|
||||
/// The [tabs] argument must not be null and its length must match the [controller]'s
|
||||
/// [TabController.length].
|
||||
@ -680,7 +695,57 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.physics,
|
||||
this.splashFactory,
|
||||
this.splashBorderRadius,
|
||||
}) : assert(indicator != null || (indicatorWeight > 0.0));
|
||||
}) : _isPrimary = true,
|
||||
assert(indicator != null || (indicatorWeight > 0.0));
|
||||
|
||||
/// Creates a Material Design secondary tab bar.
|
||||
///
|
||||
/// Secondary tabs are used within a content area to further separate related
|
||||
/// content and establish hierarchy.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This sample showcases nested Material 3 [TabBar]s. It consists of a primary
|
||||
/// [TabBar] with nested a secondary [TabBar]. The primary [TabBar] uses a
|
||||
/// [DefaultTabController] while the secondary [TabBar] uses a [TabController].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TabBar], for a primary tab bar.
|
||||
/// * [TabBarView], which displays page views that correspond to each tab.
|
||||
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
|
||||
/// * https://m3.material.io/components/tab-bar/overview, the Material 3
|
||||
/// tab bar specification.
|
||||
const TabBar.secondary({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
this.controller,
|
||||
this.isScrollable = false,
|
||||
this.padding,
|
||||
this.indicatorColor,
|
||||
this.automaticIndicatorColorAdjustment = true,
|
||||
this.indicatorWeight = 2.0,
|
||||
this.indicatorPadding = EdgeInsets.zero,
|
||||
this.indicator,
|
||||
this.indicatorSize,
|
||||
this.dividerColor,
|
||||
this.labelColor,
|
||||
this.labelStyle,
|
||||
this.labelPadding,
|
||||
this.unselectedLabelColor,
|
||||
this.unselectedLabelStyle,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.overlayColor,
|
||||
this.mouseCursor,
|
||||
this.enableFeedback,
|
||||
this.onTap,
|
||||
this.physics,
|
||||
this.splashFactory,
|
||||
this.splashBorderRadius,
|
||||
}) : _isPrimary = false,
|
||||
assert(indicator != null || (indicatorWeight > 0.0));
|
||||
|
||||
/// Typically a list of two or more [Tab] widgets.
|
||||
///
|
||||
@ -993,6 +1058,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Whether this tab bar is a primary tab bar.
|
||||
///
|
||||
/// Otherwise, it is a secondary tab bar.
|
||||
final bool _isPrimary;
|
||||
|
||||
@override
|
||||
State<TabBar> createState() => _TabBarState();
|
||||
}
|
||||
@ -1016,10 +1086,19 @@ class _TabBarState extends State<TabBar> {
|
||||
_labelPaddings = List<EdgeInsetsGeometry>.filled(widget.tabs.length, EdgeInsets.zero, growable: true);
|
||||
}
|
||||
|
||||
TabBarTheme get _defaults {
|
||||
if (Theme.of(context).useMaterial3) {
|
||||
return widget._isPrimary
|
||||
? _TabsPrimaryDefaultsM3(context)
|
||||
: _TabsSecondaryDefaultsM3(context);
|
||||
} else {
|
||||
return _TabsDefaultsM2(context);
|
||||
}
|
||||
}
|
||||
|
||||
Decoration _getIndicator() {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
||||
|
||||
if (widget.indicator != null) {
|
||||
return widget.indicator!;
|
||||
@ -1030,7 +1109,7 @@ class _TabBarState extends State<TabBar> {
|
||||
|
||||
Color color = widget.indicatorColor
|
||||
?? (theme.useMaterial3
|
||||
? tabBarTheme.indicatorColor ?? defaults.indicatorColor!
|
||||
? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!
|
||||
: Theme.of(context).indicatorColor);
|
||||
// ThemeData tries to avoid this by having indicatorColor avoid being the
|
||||
// primaryColor. However, it's possible that the tab bar is on a
|
||||
@ -1046,12 +1125,13 @@ class _TabBarState extends State<TabBar> {
|
||||
// TODO(xu-baolin): Remove automatic adjustment to white color indicator
|
||||
// with a better long-term solution.
|
||||
// https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917
|
||||
if (widget.automaticIndicatorColorAdjustment && color.value == Material.maybeOf(context)?.color?.value) {
|
||||
if (widget.automaticIndicatorColorAdjustment &&
|
||||
color.value == Material.maybeOf(context)?.color?.value) {
|
||||
color = Colors.white;
|
||||
}
|
||||
|
||||
return UnderlineTabIndicator(
|
||||
borderRadius: theme.useMaterial3
|
||||
borderRadius: theme.useMaterial3 && widget._isPrimary
|
||||
// TODO(tahatesser): Make sure this value matches Material 3 Tabs spec
|
||||
// when `preferredSize`and `indicatorWeight` are updated to support Material 3
|
||||
// https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30,
|
||||
@ -1107,16 +1187,15 @@ class _TabBarState extends State<TabBar> {
|
||||
void _initIndicatorPainter() {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
||||
|
||||
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
|
||||
controller: _controller!,
|
||||
indicator: _getIndicator(),
|
||||
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? defaults.indicatorSize!,
|
||||
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!,
|
||||
indicatorPadding: widget.indicatorPadding,
|
||||
tabKeys: _tabKeys,
|
||||
old: _indicatorPainter,
|
||||
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null,
|
||||
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
|
||||
labelPaddings: _labelPaddings,
|
||||
);
|
||||
}
|
||||
@ -1262,14 +1341,16 @@ class _TabBarState extends State<TabBar> {
|
||||
widget.onTap?.call(index);
|
||||
}
|
||||
|
||||
Widget _buildStyledTab(Widget child, bool isSelected, Animation<double> animation) {
|
||||
Widget _buildStyledTab(Widget child, bool isSelected, Animation<double> animation, TabBarTheme defaults) {
|
||||
return _TabStyle(
|
||||
animation: animation,
|
||||
isSelected: isSelected,
|
||||
isPrimary: widget._isPrimary,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
defaults: defaults,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@ -1309,9 +1390,7 @@ class _TabBarState extends State<TabBar> {
|
||||
);
|
||||
}
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
||||
|
||||
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
|
||||
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
|
||||
@ -1353,22 +1432,22 @@ class _TabBarState extends State<TabBar> {
|
||||
// The user tapped on a tab, the tab controller's animation is running.
|
||||
assert(_currentIndex != previousIndex);
|
||||
final Animation<double> animation = _ChangeAnimation(_controller!);
|
||||
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation);
|
||||
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
|
||||
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation, _defaults);
|
||||
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation, _defaults);
|
||||
} else {
|
||||
// The user is dragging the TabBarView's PageView left or right.
|
||||
final int tabIndex = _currentIndex!;
|
||||
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation, _defaults);
|
||||
if (_currentIndex! > 0) {
|
||||
final int tabIndex = _currentIndex! - 1;
|
||||
final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation, _defaults);
|
||||
}
|
||||
if (_currentIndex! < widget.tabs.length - 1) {
|
||||
final int tabIndex = _currentIndex! + 1;
|
||||
final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation);
|
||||
wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation, _defaults);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1389,7 +1468,7 @@ class _TabBarState extends State<TabBar> {
|
||||
final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>(
|
||||
(Set<MaterialState> states) {
|
||||
final Set<MaterialState> effectiveStates = selectedState..addAll(states);
|
||||
return defaults.overlayColor?.resolve(effectiveStates);
|
||||
return _defaults.overlayColor?.resolve(effectiveStates);
|
||||
},
|
||||
);
|
||||
wrappedTabs[index] = InkWell(
|
||||
@ -1397,7 +1476,7 @@ class _TabBarState extends State<TabBar> {
|
||||
onTap: () { _handleTap(index); },
|
||||
enableFeedback: widget.enableFeedback ?? true,
|
||||
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
|
||||
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? defaults.splashFactory,
|
||||
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory,
|
||||
borderRadius: widget.splashBorderRadius,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
|
||||
@ -1422,10 +1501,12 @@ class _TabBarState extends State<TabBar> {
|
||||
child: _TabStyle(
|
||||
animation: kAlwaysDismissedAnimation,
|
||||
isSelected: false,
|
||||
isPrimary: widget._isPrimary,
|
||||
labelColor: widget.labelColor,
|
||||
unselectedLabelColor: widget.unselectedLabelColor,
|
||||
labelStyle: widget.labelStyle,
|
||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||
defaults: _defaults,
|
||||
child: _TabLabelBar(
|
||||
onPerformLayout: _saveTabOffsets,
|
||||
children: wrappedTabs,
|
||||
@ -1979,8 +2060,8 @@ class _TabsDefaultsM2 extends TabBarTheme {
|
||||
|
||||
// Token database version: v0_162
|
||||
|
||||
class _TabsDefaultsM3 extends TabBarTheme {
|
||||
_TabsDefaultsM3(this.context)
|
||||
class _TabsPrimaryDefaultsM3 extends TabBarTheme {
|
||||
_TabsPrimaryDefaultsM3(this.context)
|
||||
: super(indicatorSize: TabBarIndicatorSize.label);
|
||||
|
||||
final BuildContext context;
|
||||
@ -2037,4 +2118,62 @@ class _TabsDefaultsM3 extends TabBarTheme {
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
|
||||
class _TabsSecondaryDefaultsM3 extends TabBarTheme {
|
||||
_TabsSecondaryDefaultsM3(this.context)
|
||||
: super(indicatorSize: TabBarIndicatorSize.tab);
|
||||
|
||||
final BuildContext context;
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
late final TextTheme _textTheme = Theme.of(context).textTheme;
|
||||
|
||||
@override
|
||||
Color? get dividerColor => _colors.surfaceVariant;
|
||||
|
||||
@override
|
||||
Color? get indicatorColor => _colors.primary;
|
||||
|
||||
@override
|
||||
Color? get labelColor => _colors.onSurface;
|
||||
|
||||
@override
|
||||
TextStyle? get labelStyle => _textTheme.titleSmall;
|
||||
|
||||
@override
|
||||
Color? get unselectedLabelColor => _colors.onSurfaceVariant;
|
||||
|
||||
@override
|
||||
TextStyle? get unselectedLabelStyle => _textTheme.titleSmall;
|
||||
|
||||
@override
|
||||
MaterialStateProperty<Color?> get overlayColor {
|
||||
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return _colors.onSurface.withOpacity(0.08);
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return _colors.onSurface.withOpacity(0.12);
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return _colors.onSurface.withOpacity(0.12);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return _colors.onSurface.withOpacity(0.08);
|
||||
}
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return _colors.onSurface.withOpacity(0.12);
|
||||
}
|
||||
if (states.contains(MaterialState.pressed)) {
|
||||
return _colors.onSurface.withOpacity(0.12);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Tabs
|
||||
|
@ -31,14 +31,30 @@ final List<SizedBox> _sizedTabs = <SizedBox>[
|
||||
SizedBox(key: UniqueKey(), width: 100.0, height: 50.0),
|
||||
];
|
||||
|
||||
Widget _withTheme(
|
||||
TabBarTheme? theme, {
|
||||
Widget buildTabBar({
|
||||
TabBarTheme? tabBarTheme,
|
||||
bool secondaryTabBar = false,
|
||||
List<Widget> tabs = _tabs,
|
||||
bool isScrollable = false,
|
||||
bool useMaterial3 = false,
|
||||
}) {
|
||||
if (secondaryTabBar) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: useMaterial3),
|
||||
home: Scaffold(
|
||||
body: RepaintBoundary(
|
||||
key: _painterKey,
|
||||
child: TabBar.secondary(
|
||||
tabs: tabs,
|
||||
isScrollable: isScrollable,
|
||||
controller: TabController(length: tabs.length, vsync: const TestVSync()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return MaterialApp(
|
||||
theme: ThemeData(tabBarTheme: theme, useMaterial3: useMaterial3),
|
||||
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: useMaterial3),
|
||||
home: Scaffold(
|
||||
body: RepaintBoundary(
|
||||
key: _painterKey,
|
||||
@ -52,12 +68,17 @@ Widget _withTheme(
|
||||
);
|
||||
}
|
||||
|
||||
RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) {
|
||||
|
||||
RenderParagraph _getIcon(WidgetTester tester, IconData icon) {
|
||||
return tester.renderObject<RenderParagraph>(
|
||||
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
|
||||
);
|
||||
}
|
||||
|
||||
RenderParagraph _getText(WidgetTester tester, String text) {
|
||||
return tester.renderObject<RenderParagraph>(find.text(text));
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('TabBarTheme copyWith, ==, hashCode, defaults', () {
|
||||
expect(const TabBarTheme(), const TabBarTheme().copyWith());
|
||||
@ -82,60 +103,113 @@ void main() {
|
||||
expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true);
|
||||
});
|
||||
|
||||
testWidgets('Tab bar defaults', (WidgetTester tester) async {
|
||||
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
|
||||
await tester.pumpWidget(_withTheme(null, useMaterial3: true));
|
||||
testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
|
||||
// Test default label color and label styles.
|
||||
await tester.pumpWidget(buildTabBar(useMaterial3: true));
|
||||
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(selectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedRenderObject.text.style!.color, equals(theme.colorScheme.primary));
|
||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedRenderObject.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedLabel.text.style!.color, equals(theme.colorScheme.primary));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
|
||||
|
||||
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
|
||||
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
|
||||
// Test default labelPadding.
|
||||
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
|
||||
|
||||
const double indicatorWeight = 2.0;
|
||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
|
||||
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
|
||||
|
||||
// verify coordinates of tabOne
|
||||
// Verify tabOne coordinates.
|
||||
expect(tabOneRect.left, equals(kTabLabelPadding.left));
|
||||
expect(tabOneRect.top, equals(kTabLabelPadding.top));
|
||||
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
||||
|
||||
// verify coordinates of tabTwo
|
||||
// Verify tabTwo coordinates.
|
||||
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
|
||||
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
|
||||
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
||||
|
||||
// verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo
|
||||
// Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo.
|
||||
expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
|
||||
|
||||
// Verify divider color and indicator color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(
|
||||
tabBarBox,
|
||||
paints
|
||||
..line(color: theme.colorScheme.surfaceVariant)
|
||||
// Indicator is a rrect in the primary tab bar.
|
||||
..rrect(color: theme.colorScheme.primary),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Tab bar defaults (secondary)', (WidgetTester tester) async {
|
||||
// Test default label color and label styles.
|
||||
await tester.pumpWidget(buildTabBar(secondaryTabBar: true, useMaterial3: true));
|
||||
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedLabel.text.style!.color, equals(theme.colorScheme.onSurface));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
|
||||
|
||||
// Test default labelPadding.
|
||||
await tester.pumpWidget(buildTabBar(
|
||||
secondaryTabBar: true,
|
||||
tabs: _sizedTabs,
|
||||
isScrollable: true,
|
||||
useMaterial3: true,
|
||||
));
|
||||
|
||||
const double indicatorWeight = 2.0;
|
||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
|
||||
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].key!));
|
||||
|
||||
// Verify tabOne coordinates.
|
||||
expect(tabOneRect.left, equals(kTabLabelPadding.left));
|
||||
expect(tabOneRect.top, equals(kTabLabelPadding.top));
|
||||
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
||||
|
||||
// Verify tabTwo coordinates.
|
||||
expect(tabTwoRect.right, equals(tabBar.width - kTabLabelPadding.right));
|
||||
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
|
||||
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
||||
|
||||
// Verify tabOne and tabTwo is separated by right padding of tabOne and left padding of tabTwo.
|
||||
expect(tabOneRect.right, equals(tabTwoRect.left - kTabLabelPadding.left - kTabLabelPadding.right));
|
||||
|
||||
// Verify divider color and indicator color.
|
||||
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||
expect(
|
||||
tabBarBox,
|
||||
paints
|
||||
..line(color: theme.colorScheme.surfaceVariant)
|
||||
// Indicator is a line in the secondary tab bar.
|
||||
..line(color: theme.colorScheme.primary),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
|
||||
const Color labelColor = Colors.black;
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(textRenderObject.text.style!.color, equals(labelColor));
|
||||
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_one);
|
||||
expect(iconRenderObject.text.style!.color, equals(labelColor));
|
||||
final RenderParagraph tabLabel = _getText(tester, _tab1Text);
|
||||
expect(tabLabel.text.style!.color, equals(labelColor));
|
||||
final RenderParagraph tabIcon = _getIcon(tester, Icons.looks_one);
|
||||
expect(tabIcon.text.style!.color, equals(labelColor));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async {
|
||||
@ -151,8 +225,8 @@ void main() {
|
||||
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding);
|
||||
|
||||
await tester.pumpWidget(_withTheme(
|
||||
tabBarTheme,
|
||||
await tester.pumpWidget(buildTabBar(
|
||||
tabBarTheme: tabBarTheme,
|
||||
tabs: _sizedTabs,
|
||||
isScrollable: true,
|
||||
));
|
||||
@ -183,12 +257,12 @@ void main() {
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async {
|
||||
@ -198,14 +272,14 @@ void main() {
|
||||
labelStyle: labelStyle,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async {
|
||||
@ -220,8 +294,9 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(tabBarTheme: tabBarTheme),
|
||||
home: Scaffold(body: TabBar(
|
||||
theme: ThemeData(tabBarTheme: tabBarTheme),
|
||||
home: Scaffold(
|
||||
body: TabBar(
|
||||
tabs: _tabs,
|
||||
controller: TabController(length: _tabs.length, vsync: const TestVSync()),
|
||||
labelStyle: labelStyle,
|
||||
@ -231,10 +306,10 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
|
||||
@ -295,16 +370,25 @@ void main() {
|
||||
const Color unselectedLabelColor = Colors.black;
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(textRenderObject.text.style!.color, equals(unselectedLabelColor));
|
||||
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_two);
|
||||
final RenderParagraph iconRenderObject = _getIcon(tester, Icons.looks_two);
|
||||
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
|
||||
});
|
||||
|
||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_withTheme(null, useMaterial3: true, isScrollable: true));
|
||||
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
matchesGoldenFile('tab_bar.default.tab_indicator_size.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(buildTabBar(useMaterial3: true, isScrollable: true));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
@ -315,7 +399,7 @@ void main() {
|
||||
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
@ -326,7 +410,7 @@ void main() {
|
||||
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
@ -337,7 +421,7 @@ void main() {
|
||||
testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async {
|
||||
const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
final Offset tabBar = tester.getCenter(
|
||||
find.ancestor(of: find.text('tab 1'),matching: find.byType(TabBar)),
|
||||
@ -356,7 +440,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
@ -372,7 +456,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
||||
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
@ -386,19 +470,19 @@ void main() {
|
||||
|
||||
testWidgets('Tab bar defaults', (WidgetTester tester) async {
|
||||
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
|
||||
await tester.pumpWidget(_withTheme(null));
|
||||
await tester.pumpWidget(buildTabBar());
|
||||
|
||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
||||
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedRenderObject.text.style!.color, equals(Colors.white));
|
||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(selectedLabel.text.style!.color, equals(Colors.white));
|
||||
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||
|
||||
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
|
||||
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
|
||||
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
|
||||
|
||||
const double indicatorWeight = 2.0;
|
||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||
@ -423,7 +507,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_withTheme(null));
|
||||
await tester.pumpWidget(buildTabBar());
|
||||
|
||||
await expectLater(
|
||||
find.byKey(_painterKey),
|
||||
|
@ -106,6 +106,7 @@ class _NestedTabBarContainer extends StatelessWidget {
|
||||
|
||||
Widget buildFrame({
|
||||
Key? tabBarKey,
|
||||
bool secondaryTabBar = false,
|
||||
required List<String> tabs,
|
||||
required String value,
|
||||
bool isScrollable = false,
|
||||
@ -114,6 +115,24 @@ Widget buildFrame({
|
||||
EdgeInsetsGeometry? padding,
|
||||
TextDirection textDirection = TextDirection.ltr,
|
||||
}) {
|
||||
if (secondaryTabBar) {
|
||||
return boilerplate(
|
||||
textDirection: textDirection,
|
||||
child: DefaultTabController(
|
||||
animationDuration: animationDuration,
|
||||
initialIndex: tabs.indexOf(value),
|
||||
length: tabs.length,
|
||||
child: TabBar.secondary(
|
||||
key: tabBarKey,
|
||||
tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(),
|
||||
isScrollable: isScrollable,
|
||||
indicatorColor: indicatorColor,
|
||||
padding: padding,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return boilerplate(
|
||||
textDirection: textDirection,
|
||||
child: DefaultTabController(
|
||||
@ -238,6 +257,10 @@ class TestScrollPhysics extends ScrollPhysics {
|
||||
SpringDescription get spring => _kDefaultSpring;
|
||||
}
|
||||
|
||||
RenderParagraph _getText(WidgetTester tester, String text) {
|
||||
return tester.renderObject<RenderParagraph>(find.text(text));
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
debugResetSemanticsIdCounter();
|
||||
@ -358,12 +381,12 @@ void main() {
|
||||
expect(find.byType(TabBar), paints..line(color: Colors.blue[500]));
|
||||
});
|
||||
|
||||
testWidgets('TabBar default selected/unselected text style', (WidgetTester tester) async {
|
||||
testWidgets('TabBar default selected/unselected label style (primary)', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
const String selectedValue = 'A';
|
||||
const String unSelectedValue = 'C';
|
||||
const String unselectedValue = 'C';
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
@ -375,20 +398,95 @@ void main() {
|
||||
expect(find.text('C'), findsOneWidget);
|
||||
|
||||
// Test selected label text style.
|
||||
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto');
|
||||
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0);
|
||||
expect(tester.renderObject<RenderParagraph>(
|
||||
find.text(selectedValue)).text.style!.color,
|
||||
theme.colorScheme.primary,
|
||||
);
|
||||
final RenderParagraph selectedLabel = _getText(tester, selectedValue);
|
||||
expect(selectedLabel.text.style!.fontFamily, 'Roboto');
|
||||
expect(selectedLabel.text.style!.fontSize, 14.0);
|
||||
expect(selectedLabel.text.style!.color, theme.colorScheme.primary);
|
||||
|
||||
// Test unselected label text style.
|
||||
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto');
|
||||
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0);
|
||||
expect(tester.renderObject<RenderParagraph>(
|
||||
find.text(unSelectedValue)).text.style!.color,
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
|
||||
expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
|
||||
expect(unselectedLabel.text.style!.fontSize, 14.0);
|
||||
expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
|
||||
});
|
||||
|
||||
testWidgets('TabBar default selected/unselected label style (secondary)', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||
|
||||
const String selectedValue = 'A';
|
||||
const String unselectedValue = 'C';
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true),
|
||||
),
|
||||
);
|
||||
expect(find.text('A'), findsOneWidget);
|
||||
expect(find.text('B'), findsOneWidget);
|
||||
expect(find.text('C'), findsOneWidget);
|
||||
|
||||
// Test selected label text style.
|
||||
final RenderParagraph selectedLabel = _getText(tester, selectedValue);
|
||||
expect(selectedLabel.text.style!.fontFamily, 'Roboto');
|
||||
expect(selectedLabel.text.style!.fontSize, 14.0);
|
||||
expect(selectedLabel.text.style!.color, theme.colorScheme.onSurface);
|
||||
|
||||
// Test unselected label text style.
|
||||
final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
|
||||
expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
|
||||
expect(unselectedLabel.text.style!.fontSize, 14.0);
|
||||
expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
|
||||
});
|
||||
|
||||
testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
const String selectedValue = 'A';
|
||||
const String unselectedValue = 'B';
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: buildFrame(tabs: tabs, value: selectedValue),
|
||||
),
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.text(selectedValue)));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||
expect(inkFeatures, paints..rect(color: theme.colorScheme.primary.withOpacity(0.08)));
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text(unselectedValue)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
|
||||
});
|
||||
|
||||
testWidgets('TabBar default overlay (secondary)', (WidgetTester tester) async {
|
||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||
final List<String> tabs = <String>['A', 'B'];
|
||||
|
||||
const String selectedValue = 'A';
|
||||
const String unselectedValue = 'B';
|
||||
await tester.pumpWidget(
|
||||
Theme(
|
||||
data: theme,
|
||||
child: buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true),
|
||||
),
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.moveTo(tester.getCenter(find.text(selectedValue)));
|
||||
await tester.pumpAndSettle();
|
||||
final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
|
||||
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
|
||||
|
||||
await gesture.moveTo(tester.getCenter(find.text(unselectedValue)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(inkFeatures, paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08)));
|
||||
});
|
||||
|
||||
testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
|
||||
|
Loading…
Reference in New Issue
Block a user