| // 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<TabBarExample> createState() => _TabBarExampleState(); | |
| } | |
| class _TabBarExampleState extends State<TabBarExample> { | |
| 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<Notification>( | |
| 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<Widget>.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), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } |