• Taha Tesser's avatar
    Fix Scrollable `TabBar` for Material 3 (#131409) · 2c71881f
    Taha Tesser authored
    fixes [Material 3 `TabBar` does not take full width when `isScrollable: true`](https://github.com/flutter/flutter/issues/117722)
    
    ### Description
    1. Fixed the divider doesn't stretch to take all the available width in the scrollable tab bar in M3
    2. Added `dividerHeight` property.
    
    ### Code sample
    
    <details> 
    <summary>expand to view the code sample</summary> 
    
    ```dart
    import 'package:flutter/material.dart';
    
    /// Flutter code sample for [TabBar].
    
    void main() => runApp(const TabBarApp());
    
    class TabBarApp extends StatelessWidget {
      const TabBarApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          debugShowCheckedModeBanner: false,
          home: TabBarExample(),
        );
      }
    }
    
    class TabBarExample extends StatefulWidget {
      const TabBarExample({super.key});
    
      @override
      State<TabBarExample> createState() => _TabBarExampleState();
    }
    
    class _TabBarExampleState extends State<TabBarExample> {
      bool rtl = false;
      bool customColors = false;
      bool removeDivider = false;
      Color dividerColor = Colors.amber;
      Color indicatorColor = Colors.red;
    
      @override
      Widget build(BuildContext context) {
        return DefaultTabController(
          initialIndex: 1,
          length: 3,
          child: Directionality(
            textDirection: rtl ? TextDirection.rtl : TextDirection.ltr,
            child: Scaffold(
              appBar: AppBar(
                title: const Text('TabBar Sample'),
                actions: <Widget>[
                  IconButton.filledTonal(
                    tooltip: 'Switch direction',
                    icon: const Icon(Icons.swap_horiz),
                    onPressed: () {
                      setState(() {
                        rtl = !rtl;
                      });
                    },
                  ),
                  IconButton.filledTonal(
                    tooltip: 'Use custom colors',
                    icon: const Icon(Icons.color_lens),
                    onPressed: () {
                      setState(() {
                        customColors = !customColors;
                      });
                    },
                  ),
                  IconButton.filledTonal(
                    tooltip: 'Show/hide divider',
                    icon: const Icon(Icons.remove_rounded),
                    onPressed: () {
                      setState(() {
                        removeDivider = !removeDivider;
                      });
                    },
                  ),
                ],
              ),
              body: Column(
                children: <Widget>[
                  const Spacer(),
                  const Text('Scrollable - TabAlignment.start'),
                  TabBar(
                    isScrollable: true,
                    tabAlignment: TabAlignment.start,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Text('Scrollable - TabAlignment.startOffset'),
                  TabBar(
                    isScrollable: true,
                    tabAlignment: TabAlignment.startOffset,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Text('Scrollable - TabAlignment.center'),
                  TabBar(
                    isScrollable: true,
                    tabAlignment: TabAlignment.center,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Spacer(),
                  const Text('Non-scrollable - TabAlignment.fill'),
                  TabBar(
                    tabAlignment: TabAlignment.fill,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Text('Non-scrollable - TabAlignment.center'),
                  TabBar(
                    tabAlignment: TabAlignment.center,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Spacer(),
                  const Text('Secondary - TabAlignment.fill'),
                  TabBar.secondary(
                    tabAlignment: TabAlignment.fill,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Text('Secondary - TabAlignment.center'),
                  TabBar.secondary(
                    tabAlignment: TabAlignment.center,
                    dividerColor: customColors ? dividerColor : null,
                    indicatorColor: customColors ? indicatorColor : null,
                    dividerHeight: removeDivider ? 0 : null,
                    tabs: const <Widget>[
                      Tab(
                        icon: Icon(Icons.cloud_outlined),
                      ),
                      Tab(
                        icon: Icon(Icons.beach_access_sharp),
                      ),
                      Tab(
                        icon: Icon(Icons.brightness_5_sharp),
                      ),
                    ],
                  ),
                  const Spacer(),
                ],
              ),
            ),
          ),
        );
      }
    }
    ``` 
    	
    </details>
    
    ### Before
    
    ![Screenshot 2023-07-27 at 14 12 36](https://github.com/flutter/flutter/assets/48603081/1c08a9d2-ac15-4d33-8fa1-c765b4b10f92)
    
    ### After 
    
    ![Screenshot 2023-07-27 at 14 13 12](https://github.com/flutter/flutter/assets/48603081/7e662dfe-9f32-46c9-a128-3024a4782882)
    
    This also contains regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151
    
    ```dart
      // This is a regression test for https://github.com/flutter/flutter/pull/125974#discussion_r1239089151.
      testWidgets('Divider can be constrained', (WidgetTester tester) async {
    ```
    
    ![Screenshot 2023-07-27 at 14 16 37](https://github.com/flutter/flutter/assets/48603081/ac2ef49b-2410-46d0-8ae2-d9b77236abba)
    2c71881f
Name
Last commit
Last update
..
bin Loading commit data...
gen_defaults Loading commit data...
gen_keycodes Loading commit data...
localization Loading commit data...
test Loading commit data...
vitool Loading commit data...
.gitignore Loading commit data...
dartdoc.dart Loading commit data...
dartdoc_checker.dart Loading commit data...
examples_smoke_test.dart Loading commit data...
find_engine_commit.dart Loading commit data...
java_and_objc_doc.dart Loading commit data...
mega_gallery.dart Loading commit data...
pubspec.yaml Loading commit data...
repackage_gradle_wrapper.sh Loading commit data...
update_icons.dart Loading commit data...