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
|
@override
|
||||||
String generate() => '''
|
String generate() => '''
|
||||||
class _${blockName}DefaultsM3 extends TabBarTheme {
|
class _${blockName}PrimaryDefaultsM3 extends TabBarTheme {
|
||||||
_${blockName}DefaultsM3(this.context)
|
_${blockName}PrimaryDefaultsM3(this.context)
|
||||||
: super(indicatorSize: TabBarIndicatorSize.label);
|
: super(indicatorSize: TabBarIndicatorSize.label);
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
@ -69,5 +69,64 @@ class _${blockName}DefaultsM3 extends TabBarTheme {
|
|||||||
@override
|
@override
|
||||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() => runApp(const MyApp());
|
void main() => runApp(const TabBarApp());
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class TabBarApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const TabBarApp({super.key});
|
||||||
|
|
||||||
static const String _title = 'Flutter Code Sample';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const MaterialApp(
|
return MaterialApp(
|
||||||
title: _title,
|
theme: ThemeData(useMaterial3: true),
|
||||||
home: MyStatelessWidget(),
|
home: const TabBarExample(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyStatelessWidget extends StatelessWidget {
|
class TabBarExample extends StatelessWidget {
|
||||||
const MyStatelessWidget({super.key});
|
const TabBarExample({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -32,7 +30,7 @@ class MyStatelessWidget extends StatelessWidget {
|
|||||||
length: 3,
|
length: 3,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('TabBar Widget'),
|
title: const Text('TabBar Sample'),
|
||||||
bottom: const TabBar(
|
bottom: const TabBar(
|
||||||
tabs: <Widget>[
|
tabs: <Widget>[
|
||||||
Tab(
|
Tab(
|
||||||
|
@ -6,34 +6,31 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() => runApp(const MyApp());
|
void main() => runApp(const TabBarApp());
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class TabBarApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const TabBarApp({super.key});
|
||||||
|
|
||||||
static const String _title = 'Flutter Code Sample';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const MaterialApp(
|
return MaterialApp(
|
||||||
title: _title,
|
theme: ThemeData(useMaterial3: true),
|
||||||
home: MyStatefulWidget(),
|
home: const TabBarExample(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyStatefulWidget extends StatefulWidget {
|
class TabBarExample extends StatefulWidget {
|
||||||
const MyStatefulWidget({super.key});
|
const TabBarExample({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
|
State<TabBarExample> createState() => _TabBarExampleState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [AnimationController]s can be created with `vsync: this` because of
|
/// [AnimationController]s can be created with `vsync: this` because of
|
||||||
/// [TickerProviderStateMixin].
|
/// [TickerProviderStateMixin].
|
||||||
class _MyStatefulWidgetState extends State<MyStatefulWidget>
|
class _TabBarExampleState extends State<TabBarExample> with TickerProviderStateMixin {
|
||||||
with TickerProviderStateMixin {
|
late final TabController _tabController;
|
||||||
late TabController _tabController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -41,11 +38,17 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget>
|
|||||||
_tabController = TabController(length: 3, vsync: this);
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('TabBar Widget'),
|
title: const Text('TabBar Sample'),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: const <Widget>[
|
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({
|
const _TabStyle({
|
||||||
required Animation<double> animation,
|
required Animation<double> animation,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
|
required this.isPrimary,
|
||||||
required this.labelColor,
|
required this.labelColor,
|
||||||
required this.unselectedLabelColor,
|
required this.unselectedLabelColor,
|
||||||
required this.labelStyle,
|
required this.labelStyle,
|
||||||
required this.unselectedLabelStyle,
|
required this.unselectedLabelStyle,
|
||||||
|
required this.defaults,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(listenable: animation);
|
}) : super(listenable: animation);
|
||||||
|
|
||||||
final TextStyle? labelStyle;
|
final TextStyle? labelStyle;
|
||||||
final TextStyle? unselectedLabelStyle;
|
final TextStyle? unselectedLabelStyle;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
final bool isPrimary;
|
||||||
final Color? labelColor;
|
final Color? labelColor;
|
||||||
final Color? unselectedLabelColor;
|
final Color? unselectedLabelColor;
|
||||||
|
final TabBarTheme defaults;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
MaterialStateColor _resolveWithLabelColor(BuildContext context) {
|
MaterialStateColor _resolveWithLabelColor(BuildContext context) {
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
final TabBarTheme tabBarTheme = TabBarTheme.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 Animation<double> animation = listenable as Animation<double>;
|
||||||
|
|
||||||
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
|
// labelStyle.color (and tabBarTheme.labelStyle.color) is not considered
|
||||||
@ -219,9 +222,7 @@ class _TabStyle extends AnimatedWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ThemeData themeData = Theme.of(context);
|
|
||||||
final TabBarTheme tabBarTheme = TabBarTheme.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 Animation<double> animation = listenable as Animation<double>;
|
||||||
|
|
||||||
final Set<MaterialState> states = isSelected
|
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
|
/// Typically created as the [AppBar.bottom] part of an [AppBar] and in
|
||||||
/// conjunction with a [TabBarView].
|
/// conjunction with a [TabBarView].
|
||||||
@ -635,12 +639,23 @@ class _TabBarScrollController extends ScrollController {
|
|||||||
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart **
|
/// ** See code in examples/api/lib/material/tabs/tab_bar.1.dart **
|
||||||
/// {@end-tool}
|
/// {@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:
|
/// See also:
|
||||||
///
|
///
|
||||||
|
/// * [TabBar.secondary], for a secondary tab bar.
|
||||||
/// * [TabBarView], which displays page views that correspond to each tab.
|
/// * [TabBarView], which displays page views that correspond to each tab.
|
||||||
/// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView].
|
/// * [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 {
|
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
|
/// The [tabs] argument must not be null and its length must match the [controller]'s
|
||||||
/// [TabController.length].
|
/// [TabController.length].
|
||||||
@ -680,7 +695,57 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
this.physics,
|
this.physics,
|
||||||
this.splashFactory,
|
this.splashFactory,
|
||||||
this.splashBorderRadius,
|
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.
|
/// Typically a list of two or more [Tab] widgets.
|
||||||
///
|
///
|
||||||
@ -993,6 +1058,11 @@ class TabBar extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this tab bar is a primary tab bar.
|
||||||
|
///
|
||||||
|
/// Otherwise, it is a secondary tab bar.
|
||||||
|
final bool _isPrimary;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TabBar> createState() => _TabBarState();
|
State<TabBar> createState() => _TabBarState();
|
||||||
}
|
}
|
||||||
@ -1016,10 +1086,19 @@ class _TabBarState extends State<TabBar> {
|
|||||||
_labelPaddings = List<EdgeInsetsGeometry>.filled(widget.tabs.length, EdgeInsets.zero, growable: true);
|
_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() {
|
Decoration _getIndicator() {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||||
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
|
||||||
|
|
||||||
if (widget.indicator != null) {
|
if (widget.indicator != null) {
|
||||||
return widget.indicator!;
|
return widget.indicator!;
|
||||||
@ -1030,7 +1109,7 @@ class _TabBarState extends State<TabBar> {
|
|||||||
|
|
||||||
Color color = widget.indicatorColor
|
Color color = widget.indicatorColor
|
||||||
?? (theme.useMaterial3
|
?? (theme.useMaterial3
|
||||||
? tabBarTheme.indicatorColor ?? defaults.indicatorColor!
|
? tabBarTheme.indicatorColor ?? _defaults.indicatorColor!
|
||||||
: Theme.of(context).indicatorColor);
|
: Theme.of(context).indicatorColor);
|
||||||
// ThemeData tries to avoid this by having indicatorColor avoid being the
|
// ThemeData tries to avoid this by having indicatorColor avoid being the
|
||||||
// primaryColor. However, it's possible that the tab bar is on a
|
// 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
|
// TODO(xu-baolin): Remove automatic adjustment to white color indicator
|
||||||
// with a better long-term solution.
|
// with a better long-term solution.
|
||||||
// https://github.com/flutter/flutter/pull/68171#pullrequestreview-517753917
|
// 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;
|
color = Colors.white;
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnderlineTabIndicator(
|
return UnderlineTabIndicator(
|
||||||
borderRadius: theme.useMaterial3
|
borderRadius: theme.useMaterial3 && widget._isPrimary
|
||||||
// TODO(tahatesser): Make sure this value matches Material 3 Tabs spec
|
// TODO(tahatesser): Make sure this value matches Material 3 Tabs spec
|
||||||
// when `preferredSize`and `indicatorWeight` are updated to support Material 3
|
// when `preferredSize`and `indicatorWeight` are updated to support Material 3
|
||||||
// https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30,
|
// https://m3.material.io/components/tabs/specs#149a189f-9039-4195-99da-15c205d20e30,
|
||||||
@ -1107,16 +1187,15 @@ class _TabBarState extends State<TabBar> {
|
|||||||
void _initIndicatorPainter() {
|
void _initIndicatorPainter() {
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
final TabBarTheme tabBarTheme = TabBarTheme.of(context);
|
||||||
final TabBarTheme defaults = theme.useMaterial3 ? _TabsDefaultsM3(context) : _TabsDefaultsM2(context);
|
|
||||||
|
|
||||||
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
|
_indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter(
|
||||||
controller: _controller!,
|
controller: _controller!,
|
||||||
indicator: _getIndicator(),
|
indicator: _getIndicator(),
|
||||||
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? defaults.indicatorSize!,
|
indicatorSize: widget.indicatorSize ?? tabBarTheme.indicatorSize ?? _defaults.indicatorSize!,
|
||||||
indicatorPadding: widget.indicatorPadding,
|
indicatorPadding: widget.indicatorPadding,
|
||||||
tabKeys: _tabKeys,
|
tabKeys: _tabKeys,
|
||||||
old: _indicatorPainter,
|
old: _indicatorPainter,
|
||||||
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? defaults.dividerColor : null,
|
dividerColor: theme.useMaterial3 ? widget.dividerColor ?? tabBarTheme.dividerColor ?? _defaults.dividerColor : null,
|
||||||
labelPaddings: _labelPaddings,
|
labelPaddings: _labelPaddings,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1262,14 +1341,16 @@ class _TabBarState extends State<TabBar> {
|
|||||||
widget.onTap?.call(index);
|
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(
|
return _TabStyle(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
isSelected: isSelected,
|
isSelected: isSelected,
|
||||||
|
isPrimary: widget._isPrimary,
|
||||||
labelColor: widget.labelColor,
|
labelColor: widget.labelColor,
|
||||||
unselectedLabelColor: widget.unselectedLabelColor,
|
unselectedLabelColor: widget.unselectedLabelColor,
|
||||||
labelStyle: widget.labelStyle,
|
labelStyle: widget.labelStyle,
|
||||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||||
|
defaults: defaults,
|
||||||
child: child,
|
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 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) {
|
final List<Widget> wrappedTabs = List<Widget>.generate(widget.tabs.length, (int index) {
|
||||||
const double verticalAdjustment = (_kTextAndIconTabHeight - _kTabHeight)/2.0;
|
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.
|
// The user tapped on a tab, the tab controller's animation is running.
|
||||||
assert(_currentIndex != previousIndex);
|
assert(_currentIndex != previousIndex);
|
||||||
final Animation<double> animation = _ChangeAnimation(_controller!);
|
final Animation<double> animation = _ChangeAnimation(_controller!);
|
||||||
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation);
|
wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation, _defaults);
|
||||||
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation);
|
wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation, _defaults);
|
||||||
} else {
|
} else {
|
||||||
// The user is dragging the TabBarView's PageView left or right.
|
// The user is dragging the TabBarView's PageView left or right.
|
||||||
final int tabIndex = _currentIndex!;
|
final int tabIndex = _currentIndex!;
|
||||||
final Animation<double> centerAnimation = _DragAnimation(_controller!, tabIndex);
|
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) {
|
if (_currentIndex! > 0) {
|
||||||
final int tabIndex = _currentIndex! - 1;
|
final int tabIndex = _currentIndex! - 1;
|
||||||
final Animation<double> previousAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
|
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) {
|
if (_currentIndex! < widget.tabs.length - 1) {
|
||||||
final int tabIndex = _currentIndex! + 1;
|
final int tabIndex = _currentIndex! + 1;
|
||||||
final Animation<double> nextAnimation = ReverseAnimation(_DragAnimation(_controller!, tabIndex));
|
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?>(
|
final MaterialStateProperty<Color?> defaultOverlay = MaterialStateProperty.resolveWith<Color?>(
|
||||||
(Set<MaterialState> states) {
|
(Set<MaterialState> states) {
|
||||||
final Set<MaterialState> effectiveStates = selectedState..addAll(states);
|
final Set<MaterialState> effectiveStates = selectedState..addAll(states);
|
||||||
return defaults.overlayColor?.resolve(effectiveStates);
|
return _defaults.overlayColor?.resolve(effectiveStates);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
wrappedTabs[index] = InkWell(
|
wrappedTabs[index] = InkWell(
|
||||||
@ -1397,7 +1476,7 @@ class _TabBarState extends State<TabBar> {
|
|||||||
onTap: () { _handleTap(index); },
|
onTap: () { _handleTap(index); },
|
||||||
enableFeedback: widget.enableFeedback ?? true,
|
enableFeedback: widget.enableFeedback ?? true,
|
||||||
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
|
overlayColor: widget.overlayColor ?? tabBarTheme.overlayColor ?? defaultOverlay,
|
||||||
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? defaults.splashFactory,
|
splashFactory: widget.splashFactory ?? tabBarTheme.splashFactory ?? _defaults.splashFactory,
|
||||||
borderRadius: widget.splashBorderRadius,
|
borderRadius: widget.splashBorderRadius,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
|
padding: EdgeInsets.only(bottom: widget.indicatorWeight),
|
||||||
@ -1422,10 +1501,12 @@ class _TabBarState extends State<TabBar> {
|
|||||||
child: _TabStyle(
|
child: _TabStyle(
|
||||||
animation: kAlwaysDismissedAnimation,
|
animation: kAlwaysDismissedAnimation,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
isPrimary: widget._isPrimary,
|
||||||
labelColor: widget.labelColor,
|
labelColor: widget.labelColor,
|
||||||
unselectedLabelColor: widget.unselectedLabelColor,
|
unselectedLabelColor: widget.unselectedLabelColor,
|
||||||
labelStyle: widget.labelStyle,
|
labelStyle: widget.labelStyle,
|
||||||
unselectedLabelStyle: widget.unselectedLabelStyle,
|
unselectedLabelStyle: widget.unselectedLabelStyle,
|
||||||
|
defaults: _defaults,
|
||||||
child: _TabLabelBar(
|
child: _TabLabelBar(
|
||||||
onPerformLayout: _saveTabOffsets,
|
onPerformLayout: _saveTabOffsets,
|
||||||
children: wrappedTabs,
|
children: wrappedTabs,
|
||||||
@ -1979,8 +2060,8 @@ class _TabsDefaultsM2 extends TabBarTheme {
|
|||||||
|
|
||||||
// Token database version: v0_162
|
// Token database version: v0_162
|
||||||
|
|
||||||
class _TabsDefaultsM3 extends TabBarTheme {
|
class _TabsPrimaryDefaultsM3 extends TabBarTheme {
|
||||||
_TabsDefaultsM3(this.context)
|
_TabsPrimaryDefaultsM3(this.context)
|
||||||
: super(indicatorSize: TabBarIndicatorSize.label);
|
: super(indicatorSize: TabBarIndicatorSize.label);
|
||||||
|
|
||||||
final BuildContext context;
|
final BuildContext context;
|
||||||
@ -2037,4 +2118,62 @@ class _TabsDefaultsM3 extends TabBarTheme {
|
|||||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
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
|
// END GENERATED TOKEN PROPERTIES - Tabs
|
||||||
|
@ -31,14 +31,30 @@ final List<SizedBox> _sizedTabs = <SizedBox>[
|
|||||||
SizedBox(key: UniqueKey(), width: 100.0, height: 50.0),
|
SizedBox(key: UniqueKey(), width: 100.0, height: 50.0),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget _withTheme(
|
Widget buildTabBar({
|
||||||
TabBarTheme? theme, {
|
TabBarTheme? tabBarTheme,
|
||||||
|
bool secondaryTabBar = false,
|
||||||
List<Widget> tabs = _tabs,
|
List<Widget> tabs = _tabs,
|
||||||
bool isScrollable = false,
|
bool isScrollable = false,
|
||||||
bool useMaterial3 = 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(
|
return MaterialApp(
|
||||||
theme: ThemeData(tabBarTheme: theme, useMaterial3: useMaterial3),
|
theme: ThemeData(tabBarTheme: tabBarTheme, useMaterial3: useMaterial3),
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
body: RepaintBoundary(
|
body: RepaintBoundary(
|
||||||
key: _painterKey,
|
key: _painterKey,
|
||||||
@ -52,12 +68,17 @@ Widget _withTheme(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderParagraph _iconRenderObject(WidgetTester tester, IconData icon) {
|
|
||||||
|
RenderParagraph _getIcon(WidgetTester tester, IconData icon) {
|
||||||
return tester.renderObject<RenderParagraph>(
|
return tester.renderObject<RenderParagraph>(
|
||||||
find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
|
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() {
|
void main() {
|
||||||
test('TabBarTheme copyWith, ==, hashCode, defaults', () {
|
test('TabBarTheme copyWith, ==, hashCode, defaults', () {
|
||||||
expect(const TabBarTheme(), const TabBarTheme().copyWith());
|
expect(const TabBarTheme(), const TabBarTheme().copyWith());
|
||||||
@ -82,60 +103,113 @@ void main() {
|
|||||||
expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true);
|
expect(identical(TabBarTheme.lerp(theme, theme, 0.5), theme), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar defaults', (WidgetTester tester) async {
|
testWidgets('Tab bar defaults (primary)', (WidgetTester tester) async {
|
||||||
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
|
// Test default label color and label styles.
|
||||||
await tester.pumpWidget(_withTheme(null, useMaterial3: true));
|
await tester.pumpWidget(buildTabBar(useMaterial3: true));
|
||||||
|
|
||||||
final ThemeData theme = ThemeData(useMaterial3: true);
|
final ThemeData theme = ThemeData(useMaterial3: true);
|
||||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||||
expect(selectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
expect(selectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||||
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
|
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||||
expect(selectedRenderObject.text.style!.color, equals(theme.colorScheme.primary));
|
expect(selectedLabel.text.style!.color, equals(theme.colorScheme.primary));
|
||||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
expect(unselectedLabel.text.style!.fontFamily, equals(theme.textTheme.titleSmall!.fontFamily));
|
||||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||||
expect(unselectedRenderObject.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
|
expect(unselectedLabel.text.style!.color, equals(theme.colorScheme.onSurfaceVariant));
|
||||||
|
|
||||||
// tests for the default value of labelPadding when tabBarTheme and tabBar do not provide one
|
// Test default labelPadding.
|
||||||
await tester.pumpWidget(_withTheme(null, tabs: _sizedTabs, isScrollable: true));
|
await tester.pumpWidget(buildTabBar(tabs: _sizedTabs, isScrollable: true));
|
||||||
|
|
||||||
const double indicatorWeight = 2.0;
|
const double indicatorWeight = 2.0;
|
||||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||||
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
|
final Rect tabOneRect = tester.getRect(find.byKey(_sizedTabs[0].key!));
|
||||||
final Rect tabTwoRect = tester.getRect(find.byKey(_sizedTabs[1].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.left, equals(kTabLabelPadding.left));
|
||||||
expect(tabOneRect.top, equals(kTabLabelPadding.top));
|
expect(tabOneRect.top, equals(kTabLabelPadding.top));
|
||||||
expect(tabOneRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
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.right, equals(tabBar.width - kTabLabelPadding.right));
|
||||||
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
|
expect(tabTwoRect.top, equals(kTabLabelPadding.top));
|
||||||
expect(tabTwoRect.bottom, equals(tabBar.bottom - kTabLabelPadding.bottom - indicatorWeight));
|
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));
|
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));
|
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
|
||||||
expect(
|
expect(
|
||||||
tabBarBox,
|
tabBarBox,
|
||||||
paints
|
paints
|
||||||
..line(color: theme.colorScheme.surfaceVariant)
|
..line(color: theme.colorScheme.surfaceVariant)
|
||||||
|
// Indicator is a rrect in the primary tab bar.
|
||||||
..rrect(color: theme.colorScheme.primary),
|
..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 {
|
testWidgets('Tab bar theme overrides label color (selected)', (WidgetTester tester) async {
|
||||||
const Color labelColor = Colors.black;
|
const Color labelColor = Colors.black;
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(labelColor: labelColor);
|
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));
|
final RenderParagraph tabLabel = _getText(tester, _tab1Text);
|
||||||
expect(textRenderObject.text.style!.color, equals(labelColor));
|
expect(tabLabel.text.style!.color, equals(labelColor));
|
||||||
final RenderParagraph iconRenderObject = _iconRenderObject(tester, Icons.looks_one);
|
final RenderParagraph tabIcon = _getIcon(tester, Icons.looks_one);
|
||||||
expect(iconRenderObject.text.style!.color, equals(labelColor));
|
expect(tabIcon.text.style!.color, equals(labelColor));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async {
|
testWidgets('Tab bar theme overrides label padding', (WidgetTester tester) async {
|
||||||
@ -151,8 +225,8 @@ void main() {
|
|||||||
|
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding);
|
const TabBarTheme tabBarTheme = TabBarTheme(labelPadding: labelPadding);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(
|
await tester.pumpWidget(buildTabBar(
|
||||||
tabBarTheme,
|
tabBarTheme: tabBarTheme,
|
||||||
tabs: _sizedTabs,
|
tabs: _sizedTabs,
|
||||||
isScrollable: true,
|
isScrollable: true,
|
||||||
));
|
));
|
||||||
@ -183,12 +257,12 @@ void main() {
|
|||||||
unselectedLabelStyle: unselectedLabelStyle,
|
unselectedLabelStyle: unselectedLabelStyle,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async {
|
testWidgets('Tab bar theme with just label style specified', (WidgetTester tester) async {
|
||||||
@ -198,14 +272,14 @@ void main() {
|
|||||||
labelStyle: labelStyle,
|
labelStyle: labelStyle,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||||
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||||
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
expect(unselectedLabel.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async {
|
testWidgets('Tab bar label styles override theme label styles', (WidgetTester tester) async {
|
||||||
@ -220,8 +294,9 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
theme: ThemeData(tabBarTheme: tabBarTheme),
|
theme: ThemeData(tabBarTheme: tabBarTheme),
|
||||||
home: Scaffold(body: TabBar(
|
home: Scaffold(
|
||||||
|
body: TabBar(
|
||||||
tabs: _tabs,
|
tabs: _tabs,
|
||||||
controller: TabController(length: _tabs.length, vsync: const TestVSync()),
|
controller: TabController(length: _tabs.length, vsync: const TestVSync()),
|
||||||
labelStyle: labelStyle,
|
labelStyle: labelStyle,
|
||||||
@ -231,10 +306,10 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final RenderParagraph selectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab1Text));
|
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||||
expect(selectedRenderObject.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
expect(selectedLabel.text.style!.fontFamily, equals(labelStyle.fontFamily));
|
||||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||||
expect(unselectedRenderObject.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
expect(unselectedLabel.text.style!.fontFamily, equals(unselectedLabelStyle.fontFamily));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
|
testWidgets('Tab bar label padding overrides theme label padding', (WidgetTester tester) async {
|
||||||
@ -295,16 +370,25 @@ void main() {
|
|||||||
const Color unselectedLabelColor = Colors.black;
|
const Color unselectedLabelColor = Colors.black;
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(unselectedLabelColor: unselectedLabelColor);
|
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));
|
final RenderParagraph textRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
||||||
expect(textRenderObject.text.style!.color, equals(unselectedLabelColor));
|
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));
|
expect(iconRenderObject.text.style!.color, equals(unselectedLabelColor));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
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(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
@ -315,7 +399,7 @@ void main() {
|
|||||||
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
|
testWidgets('Tab bar theme overrides tab indicator size (tab)', (WidgetTester tester) async {
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
|
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.tab);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
@ -326,7 +410,7 @@ void main() {
|
|||||||
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
|
testWidgets('Tab bar theme overrides tab indicator size (label)', (WidgetTester tester) async {
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
|
const TabBarTheme tabBarTheme = TabBarTheme(indicatorSize: TabBarIndicatorSize.label);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
@ -337,7 +421,7 @@ void main() {
|
|||||||
testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async {
|
testWidgets('Tab bar theme overrides tab mouse cursor', (WidgetTester tester) async {
|
||||||
const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable);
|
const TabBarTheme tabBarTheme = TabBarTheme(mouseCursor: MaterialStateMouseCursor.textable);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
final Offset tabBar = tester.getCenter(
|
final Offset tabBar = tester.getCenter(
|
||||||
find.ancestor(of: find.text('tab 1'),matching: find.byType(TabBar)),
|
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(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
@ -372,7 +456,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(_withTheme(tabBarTheme));
|
await tester.pumpWidget(buildTabBar(tabBarTheme: tabBarTheme));
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
@ -386,19 +470,19 @@ void main() {
|
|||||||
|
|
||||||
testWidgets('Tab bar defaults', (WidgetTester tester) async {
|
testWidgets('Tab bar defaults', (WidgetTester tester) async {
|
||||||
// tests for the default label color and label styles when tabBarTheme and tabBar do not provide any
|
// 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));
|
final RenderParagraph selectedLabel = _getText(tester, _tab1Text);
|
||||||
expect(selectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
expect(selectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||||
expect(selectedRenderObject.text.style!.fontSize, equals(14.0));
|
expect(selectedLabel.text.style!.fontSize, equals(14.0));
|
||||||
expect(selectedRenderObject.text.style!.color, equals(Colors.white));
|
expect(selectedLabel.text.style!.color, equals(Colors.white));
|
||||||
final RenderParagraph unselectedRenderObject = tester.renderObject<RenderParagraph>(find.text(_tab2Text));
|
final RenderParagraph unselectedLabel = _getText(tester, _tab2Text);
|
||||||
expect(unselectedRenderObject.text.style!.fontFamily, equals('Roboto'));
|
expect(unselectedLabel.text.style!.fontFamily, equals('Roboto'));
|
||||||
expect(unselectedRenderObject.text.style!.fontSize, equals(14.0));
|
expect(unselectedLabel.text.style!.fontSize, equals(14.0));
|
||||||
expect(unselectedRenderObject.text.style!.color, equals(Colors.white.withAlpha(0xB2)));
|
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
|
// 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;
|
const double indicatorWeight = 2.0;
|
||||||
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
final Rect tabBar = tester.getRect(find.byType(TabBar));
|
||||||
@ -423,7 +507,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
testWidgets('Tab bar default tab indicator size', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(_withTheme(null));
|
await tester.pumpWidget(buildTabBar());
|
||||||
|
|
||||||
await expectLater(
|
await expectLater(
|
||||||
find.byKey(_painterKey),
|
find.byKey(_painterKey),
|
||||||
|
@ -106,6 +106,7 @@ class _NestedTabBarContainer extends StatelessWidget {
|
|||||||
|
|
||||||
Widget buildFrame({
|
Widget buildFrame({
|
||||||
Key? tabBarKey,
|
Key? tabBarKey,
|
||||||
|
bool secondaryTabBar = false,
|
||||||
required List<String> tabs,
|
required List<String> tabs,
|
||||||
required String value,
|
required String value,
|
||||||
bool isScrollable = false,
|
bool isScrollable = false,
|
||||||
@ -114,6 +115,24 @@ Widget buildFrame({
|
|||||||
EdgeInsetsGeometry? padding,
|
EdgeInsetsGeometry? padding,
|
||||||
TextDirection textDirection = TextDirection.ltr,
|
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(
|
return boilerplate(
|
||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
@ -238,6 +257,10 @@ class TestScrollPhysics extends ScrollPhysics {
|
|||||||
SpringDescription get spring => _kDefaultSpring;
|
SpringDescription get spring => _kDefaultSpring;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderParagraph _getText(WidgetTester tester, String text) {
|
||||||
|
return tester.renderObject<RenderParagraph>(find.text(text));
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
debugResetSemanticsIdCounter();
|
debugResetSemanticsIdCounter();
|
||||||
@ -358,12 +381,12 @@ void main() {
|
|||||||
expect(find.byType(TabBar), paints..line(color: Colors.blue[500]));
|
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 ThemeData theme = ThemeData(useMaterial3: true);
|
||||||
final List<String> tabs = <String>['A', 'B', 'C'];
|
final List<String> tabs = <String>['A', 'B', 'C'];
|
||||||
|
|
||||||
const String selectedValue = 'A';
|
const String selectedValue = 'A';
|
||||||
const String unSelectedValue = 'C';
|
const String unselectedValue = 'C';
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Theme(
|
Theme(
|
||||||
data: theme,
|
data: theme,
|
||||||
@ -375,20 +398,95 @@ void main() {
|
|||||||
expect(find.text('C'), findsOneWidget);
|
expect(find.text('C'), findsOneWidget);
|
||||||
|
|
||||||
// Test selected label text style.
|
// Test selected label text style.
|
||||||
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontFamily, 'Roboto');
|
final RenderParagraph selectedLabel = _getText(tester, selectedValue);
|
||||||
expect(tester.renderObject<RenderParagraph>(find.text(selectedValue)).text.style!.fontSize, 14.0);
|
expect(selectedLabel.text.style!.fontFamily, 'Roboto');
|
||||||
expect(tester.renderObject<RenderParagraph>(
|
expect(selectedLabel.text.style!.fontSize, 14.0);
|
||||||
find.text(selectedValue)).text.style!.color,
|
expect(selectedLabel.text.style!.color, theme.colorScheme.primary);
|
||||||
theme.colorScheme.primary,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test unselected label text style.
|
// Test unselected label text style.
|
||||||
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontFamily, 'Roboto');
|
final RenderParagraph unselectedLabel = _getText(tester, unselectedValue);
|
||||||
expect(tester.renderObject<RenderParagraph>(find.text(unSelectedValue)).text.style!.fontSize, 14.0);
|
expect(unselectedLabel.text.style!.fontFamily, 'Roboto');
|
||||||
expect(tester.renderObject<RenderParagraph>(
|
expect(unselectedLabel.text.style!.fontSize, 14.0);
|
||||||
find.text(unSelectedValue)).text.style!.color,
|
expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant);
|
||||||
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 {
|
testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
|
||||||
|
Loading…
Reference in New Issue
Block a user