mirror of
https://github.com/openfoodfacts/smooth-app.git
synced 2025-08-06 18:25:11 +08:00
243 lines
7.1 KiB
Dart
243 lines
7.1 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:smooth_app/generic_lib/design_constants.dart';
|
|
import 'package:smooth_app/helpers/num_utils.dart';
|
|
import 'package:smooth_app/helpers/ui_helpers.dart';
|
|
import 'package:smooth_app/themes/smooth_theme.dart';
|
|
import 'package:smooth_app/themes/smooth_theme_colors.dart';
|
|
import 'package:smooth_app/themes/theme_provider.dart';
|
|
|
|
class SmoothTabBar<T> extends StatefulWidget {
|
|
const SmoothTabBar({
|
|
required this.tabController,
|
|
required this.items,
|
|
required this.onTabChanged,
|
|
this.padding,
|
|
this.leadingItems,
|
|
this.trailingItems,
|
|
this.overflowMainColor,
|
|
super.key,
|
|
}) : assert(items.length > 0);
|
|
|
|
static const double TAB_BAR_HEIGHT = 46.0;
|
|
|
|
final TabController tabController;
|
|
final Iterable<SmoothTabBarItem<T>> items;
|
|
final Iterable<Widget?>? leadingItems;
|
|
final Iterable<Widget?>? trailingItems;
|
|
final Function(T) onTabChanged;
|
|
final EdgeInsetsGeometry? padding;
|
|
final Color? overflowMainColor;
|
|
|
|
@override
|
|
State<SmoothTabBar<T>> createState() => _SmoothTabBarState<T>();
|
|
}
|
|
|
|
class _SmoothTabBarState<T> extends State<SmoothTabBar<T>> {
|
|
double _horizontalProgress = 0.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final SmoothColorsThemeExtension theme = context
|
|
.extension<SmoothColorsThemeExtension>();
|
|
final bool lightTheme = context.lightTheme();
|
|
|
|
return CustomPaint(
|
|
foregroundPainter: _ProductHeaderTabBarPainter(
|
|
progress: _horizontalProgress,
|
|
primaryColor:
|
|
widget.overflowMainColor ??
|
|
(lightTheme ? theme.primaryLight : theme.primaryDark),
|
|
bottomSeparatorColor: lightTheme
|
|
? theme.primaryBlack
|
|
: theme.primaryNormal,
|
|
),
|
|
child: SizedBox(
|
|
height: SmoothTabBar.TAB_BAR_HEIGHT,
|
|
child: NotificationListener<ScrollNotification>(
|
|
onNotification: (ScrollNotification notif) {
|
|
onNextFrame(() {
|
|
setState(() {
|
|
_horizontalProgress =
|
|
notif.metrics.pixels / notif.metrics.maxScrollExtent;
|
|
});
|
|
});
|
|
|
|
return false;
|
|
},
|
|
child: TabBar(
|
|
controller: widget.tabController,
|
|
tabs: widget.items
|
|
.mapIndexed(
|
|
(int position, SmoothTabBarItem<T> item) => _SmoothTab<T>(
|
|
item: item,
|
|
leading: widget.leadingItems?.elementAtOrNull(position),
|
|
trailing: widget.trailingItems?.elementAtOrNull(position),
|
|
selected: widget.tabController.index == position,
|
|
),
|
|
)
|
|
.toList(growable: false),
|
|
isScrollable: true,
|
|
padding: widget.padding,
|
|
labelPadding: EdgeInsets.zero,
|
|
tabAlignment: TabAlignment.start,
|
|
overlayColor: WidgetStatePropertyAll<Color>(
|
|
lightTheme
|
|
? theme.primaryNormal.withValues(alpha: 0.2)
|
|
: theme.primaryLight.withValues(alpha: 0.2),
|
|
),
|
|
splashBorderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(5.0),
|
|
),
|
|
labelStyle: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15.0,
|
|
color: theme.primaryBlack,
|
|
),
|
|
unselectedLabelStyle: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 15.0,
|
|
color: lightTheme ? theme.primaryBlack : theme.primaryMedium,
|
|
),
|
|
dividerColor: theme.primaryDark,
|
|
indicator: BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(color: theme.primaryDark, width: 3.0),
|
|
),
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(5.0),
|
|
),
|
|
color: theme.primaryLight,
|
|
),
|
|
onTap: (int position) => widget.onTabChanged.call(
|
|
widget.items.elementAt(position).value,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SmoothTabBarItem<T> {
|
|
const SmoothTabBarItem({required this.label, required this.value})
|
|
: assert(label.length > 0);
|
|
|
|
final String label;
|
|
final T value;
|
|
}
|
|
|
|
class _SmoothTab<T> extends StatelessWidget {
|
|
const _SmoothTab({
|
|
required this.item,
|
|
required this.selected,
|
|
this.leading,
|
|
this.trailing,
|
|
});
|
|
|
|
final SmoothTabBarItem<T> item;
|
|
final Widget? leading;
|
|
final Widget? trailing;
|
|
final bool selected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Widget child;
|
|
if (leading == null && trailing == null) {
|
|
child = Text(item.label);
|
|
} else {
|
|
child = IconTheme(
|
|
data: IconThemeData(
|
|
color: DefaultTextStyle.of(context).style.color,
|
|
size: 15.0,
|
|
),
|
|
child: Row(
|
|
children: <Widget>[
|
|
if (leading != null) leading!,
|
|
Padding(
|
|
padding: EdgeInsetsDirectional.only(
|
|
start: leading != null ? SMALL_SPACE : 0.0,
|
|
end: trailing != null ? SMALL_SPACE : 0.0,
|
|
),
|
|
child: Text(item.label),
|
|
),
|
|
if (trailing != null) trailing!,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
|
child: Center(child: child),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ProductHeaderTabBarPainter extends CustomPainter {
|
|
_ProductHeaderTabBarPainter({
|
|
required this.progress,
|
|
required this.primaryColor,
|
|
required this.bottomSeparatorColor,
|
|
});
|
|
|
|
final double progress;
|
|
final Color primaryColor;
|
|
final Color bottomSeparatorColor;
|
|
final Paint _paint = Paint();
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final double gradientSize = size.width * 0.1;
|
|
final Color backgroundColor = primaryColor.withValues(alpha: 0.0);
|
|
|
|
if (progress > 0.0) {
|
|
_paint.shader =
|
|
ui.Gradient.linear(Offset.zero, Offset(gradientSize, 0.0), <Color>[
|
|
primaryColor.withValues(
|
|
alpha: progress.progressAndClamp(0.0, 0.3, 1.0),
|
|
),
|
|
backgroundColor,
|
|
]);
|
|
|
|
canvas.drawRect(Rect.fromLTWH(0, 0, gradientSize, size.height), _paint);
|
|
}
|
|
|
|
if (progress < 1.0) {
|
|
_paint.shader = ui.Gradient.linear(
|
|
Offset(size.width - gradientSize, 0.0),
|
|
Offset(size.width, 0.0),
|
|
<Color>[
|
|
backgroundColor,
|
|
primaryColor.withValues(
|
|
alpha: 1 - progress.progressAndClamp(0.7, 1.0, 1.0),
|
|
),
|
|
],
|
|
);
|
|
|
|
canvas.drawRect(
|
|
Rect.fromLTWH(size.width - gradientSize, 0, size.width, size.height),
|
|
_paint,
|
|
);
|
|
}
|
|
|
|
_paint
|
|
..shader = null
|
|
..color = bottomSeparatorColor;
|
|
canvas.drawLine(
|
|
Offset(0, size.height - 1.0),
|
|
Offset(size.width, size.height - 1.0),
|
|
_paint,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_ProductHeaderTabBarPainter oldDelegate) =>
|
|
oldDelegate.progress != progress;
|
|
|
|
@override
|
|
bool shouldRebuildSemantics(_ProductHeaderTabBarPainter oldDelegate) => true;
|
|
}
|