blob: bb54762d2c75d21917734bd8f795c0f4abad52d6 [file] [log] [blame]
// 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: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.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: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.only(right: 4),
child: Icon(
Icons.chevron_right,
color: Colors.black.withValues(alpha: 0.4),
),
),
),
),
),
),
);
}
}