Files
smooth-app/packages/smooth_app/lib/widgets/v2/smooth_topbar2.dart
Edouard Marquez 72ec278689 feat: Redesign of the list of prices (#6716)
* Redesign of the list of prices

* Fix

* Revert the iOS config
2025-07-07 12:27:58 +02:00

322 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/helpers/num_utils.dart';
import 'package:smooth_app/pages/product/product_type_extensions.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';
import 'package:smooth_app/widgets/v2/smooth_leading_button.dart';
class SmoothTopBar2 extends StatefulWidget implements PreferredSizeWidget {
const SmoothTopBar2({
required this.title,
this.subTitle,
this.topWidget,
this.leadingAction,
this.forceMultiLines = false,
this.reducedHeightOnScroll = false,
this.elevation = 4.0,
this.elevationColor,
this.elevationOnScroll = true,
this.foregroundColor,
this.backgroundColor,
this.productType,
super.key,
}) : assert(title.length > 0),
assert(forceMultiLines == false || subTitle == null);
/// Height without the top view padding
static double kTopBar2Height = 100;
final String title;
final String? subTitle;
final double elevation;
final Color? elevationColor;
final bool elevationOnScroll;
final Color? foregroundColor;
final Color? backgroundColor;
final bool forceMultiLines;
final bool reducedHeightOnScroll;
final ProductType? productType;
final PreferredSizeWidget? topWidget;
final SmoothLeadingAction? leadingAction;
@override
State<SmoothTopBar2> createState() => _SmoothTopBar2State();
@override
Size get preferredSize => Size(
double.infinity,
kTopBar2Height + (topWidget?.preferredSize.height ?? 0.0),
);
}
class _SmoothTopBar2State extends State<SmoothTopBar2> {
late double _progress = 0.0;
late double _elevation = 0.0;
@override
void initState() {
super.initState();
if (widget.elevationOnScroll || widget.reducedHeightOnScroll) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => PrimaryScrollController.maybeOf(
context,
)?.addListener(() => _onScroll()),
);
}
if (!widget.elevationOnScroll) {
_elevation = widget.elevation;
}
}
void _onScroll() {
final double offset = PrimaryScrollController.of(context).offset;
final double newProgress = offset.progressAndClamp(
0.0,
HEADER_ROUNDED_RADIUS.x * 2.0,
1.0,
);
if (newProgress != _progress) {
setState(() {
if (widget.elevationOnScroll) {
_elevation = widget.elevation * newProgress;
}
if (widget.reducedHeightOnScroll) {
_progress = newProgress;
}
});
}
}
@override
Widget build(BuildContext context) {
final SmoothColorsThemeExtension colors = context
.extension<SmoothColorsThemeExtension>();
final TextDirection textDirection = Directionality.of(context);
final bool darkTheme = context.darkTheme();
final double imageWidth = MediaQuery.sizeOf(context).width * 0.22;
final double imageHeight = imageWidth * 114 / 92;
final BorderRadius borderRadius = BorderRadius.vertical(
bottom: Radius.circular(
HEADER_BORDER_RADIUS.topRight.x * (1 - _progress),
),
);
final Color backgroundColor =
widget.backgroundColor ??
(darkTheme ? colors.primaryDark : colors.primaryMedium);
return PhysicalModel(
color: Colors.transparent,
elevation: _elevation,
shadowColor:
widget.elevationColor ??
(darkTheme ? Colors.white10 : Colors.black12),
borderRadius: borderRadius,
child: ClipRRect(
borderRadius: borderRadius,
clipBehavior: Clip.antiAlias,
child: ColoredBox(
color: backgroundColor,
child: Padding(
padding: EdgeInsetsDirectional.only(
top: MediaQuery.viewPaddingOf(context).top,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.topWidget != null)
SizedBox.fromSize(
size: widget.topWidget!.preferredSize,
child: widget.topWidget,
),
SizedBox(
height: _computeHeight(),
child: Stack(
children: <Widget>[
_getImageAsset(
backgroundColor: backgroundColor,
textDirection: textDirection,
imageWidth: imageWidth,
imageHeight: imageHeight,
),
Positioned.directional(
textDirection: textDirection,
top: 0.0,
bottom: VERY_LARGE_SPACE * (1 - _progress),
start: widget.leadingAction != null
? BALANCED_SPACE
: VERY_LARGE_SPACE,
end:
(imageWidth * 0.75) *
(1 - _progress.progressAndClamp(0.5, 0.9, 1.0)),
child: Align(
alignment: AlignmentDirectional.topStart,
child: Row(
crossAxisAlignment: widget.subTitle != null
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: <Widget>[
if (widget.leadingAction != null) ...<Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(
top: 9.0,
),
child: SmoothLeadingButton(
action: widget.leadingAction!,
foregroundColor: widget.foregroundColor,
),
),
const SizedBox(width: BALANCED_SPACE),
],
Expanded(
child: Padding(
padding: EdgeInsetsDirectional.only(
bottom: 1.56 * (1 - _progress),
top: _computeTextTopPadding(),
),
child: _getText(darkTheme, colors),
),
),
],
),
),
),
],
),
),
],
),
),
),
),
);
}
double _computeHeight() =>
kToolbarHeight +
((SmoothTopBar2.kTopBar2Height - kToolbarHeight) * (1 - _progress));
Positioned _getImageAsset({
required Color backgroundColor,
required TextDirection textDirection,
required double imageWidth,
required double imageHeight,
}) {
final double progress = _progress.progressAndClamp(0.0, 0.7, 1.0);
if (widget.productType == null) {
return Positioned.directional(
textDirection: textDirection,
bottom: -(imageHeight / 2.1),
end: -imageWidth * 0.15,
child: Offstage(
offstage: progress == 1.0,
child: ExcludeSemantics(
child: SvgPicture.asset(
'assets/product/product_completed_graphic_light.svg',
width: MediaQuery.sizeOf(context).width * 0.22,
height: imageHeight,
),
),
),
);
}
final double height = switch (widget.productType!) {
ProductType.food => imageHeight / 2.1,
ProductType.beauty => imageHeight / 2.0,
ProductType.petFood => imageHeight / 2.7,
ProductType.product => imageHeight / 2.65,
};
return Positioned.directional(
textDirection: textDirection,
bottom: 0.0,
end: 0.0,
child: Offstage(
offstage: progress == 1.0,
child: ExcludeSemantics(
child: SvgPicture.asset(
widget.productType!.getIllustration(),
width: imageWidth,
height: height,
colorFilter: progress == 0.0
? null
: ColorFilter.mode(
backgroundColor.withValues(alpha: progress),
BlendMode.srcATop,
),
),
),
),
);
}
Widget _getText(bool darkTheme, SmoothColorsThemeExtension colors) {
final Widget text = Text(
widget.title,
maxLines: widget.subTitle != null ? 1 : 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
widget.foregroundColor ??
(darkTheme ? colors.primaryMedium : colors.primaryBlack),
fontSize: 20.0,
height: widget.reducedHeightOnScroll ? 1.3 : 1.5,
fontWeight: FontWeight.bold,
),
);
if (widget.forceMultiLines) {
return SizedBox(
height: (MediaQuery.textScalerOf(context).scale(20.0) * 2.0) * 1.5,
child: Align(alignment: AlignmentDirectional.centerStart, child: text),
);
} else if (widget.subTitle == null) {
return text;
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
text,
Text(
widget.subTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
widget.foregroundColor ??
(darkTheme ? colors.primaryMedium : colors.primaryBlack),
fontSize: 16.0,
height: 1.5,
fontWeight: FontWeight.w500,
),
),
],
);
}
double _computeTextTopPadding() {
double topPadding = widget.leadingAction != null && widget.subTitle != null
? (9.0 * (1 - _progress.progressAndClamp(0.2, 0.9, 0.50)))
: MEDIUM_SPACE;
if (widget.subTitle != null) {
topPadding += 4.5 * (1 - _progress);
}
return topPadding;
}
}