// 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'; /// Flutter code sample for a [TabBar] that displays custom effects on top of /// the tab bar itself when there are more tabs in the scroll direction. void main() => runApp(const TabBarApp()); class TabBarApp extends StatelessWidget { const TabBarApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(home: TabBarExample()); } } class TabBarExample extends StatefulWidget { const TabBarExample({super.key}); @override State createState() => _TabBarExampleState(); } class _TabBarExampleState extends State { double scrollOffset = 0; double maxScrollExtent = 0; @override Widget build(BuildContext context) { return DefaultTabController( length: 20, child: Scaffold( appBar: AppBar( title: const Text('TabBar with scroll notifications'), bottom: PreferredSize( preferredSize: const Size.fromHeight(56.0), child: NotificationListener( onNotification: (Notification notification) { // ScrollMetricsNotification is for initial layout. // ScrollNotification is for real-time scroll updates. final ScrollMetrics? metrics = switch (notification) { ScrollMetricsNotification(:final metrics) => metrics, ScrollNotification(:final metrics) => metrics, _ => null, }; if (metrics != null) { setState(() { scrollOffset = metrics.pixels; maxScrollExtent = metrics.maxScrollExtent; }); } return false; }, child: Stack( children: [ TabBar( isScrollable: true, tabs: List.generate( 20, (int index) => Tab(text: 'Tab $index'), ), ), // When the selected tab is not at the beginning or end // (indicating TabBar is scrollable), add a gradient mask // to left or right. Positioned( top: 0, bottom: 0, left: 0, right: 0, child: GradientMasks( scrollOffset: scrollOffset, maxScrollExtent: maxScrollExtent, ), ), ], ), ), ), ), ), ); } } class GradientMasks extends StatelessWidget { final double scrollOffset; final double maxScrollExtent; const GradientMasks({ super.key, required this.scrollOffset, required this.maxScrollExtent, }); @override Widget build(BuildContext context) { return Row( children: [ if (scrollOffset > 0) const LeftMask(), const Spacer(), if (scrollOffset < maxScrollExtent) const RightMask(), ], ); } } /// This mask shows when the selected tab is not at the beginning. class LeftMask extends StatelessWidget { const LeftMask({super.key}); @override Widget build(BuildContext context) { return IgnorePointer( child: ClipRect( child: BackdropFilter( filter: ColorFilter.mode( Colors.black.withValues(alpha: 0.2), BlendMode.srcOver, ), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ Colors.white.withValues(alpha: 0.8), Colors.white.withValues(alpha: 0.2), ], ), ), child: Align( alignment: .centerLeft, child: Padding( padding: .only(left: 4), child: Icon( Icons.chevron_left, color: Colors.black.withValues(alpha: 0.4), ), ), ), ), ), ), ); } } /// This mask shows when the selected tab is not at the end. class RightMask extends StatelessWidget { const RightMask({super.key}); @override Widget build(BuildContext context) { return IgnorePointer( child: ClipRect( child: BackdropFilter( filter: ColorFilter.mode( Colors.black.withValues(alpha: 0.2), BlendMode.srcOver, ), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerRight, end: Alignment.centerLeft, colors: [ Colors.white.withValues(alpha: 0.8), Colors.white.withValues(alpha: 0.2), ], ), ), child: Align( alignment: .centerRight, child: Padding( padding: .only(right: 4), child: Icon( Icons.chevron_right, color: Colors.black.withValues(alpha: 0.4), ), ), ), ), ), ), ); } }