Files
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

477 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:smooth_app/generic_lib/design_constants.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';
/// Renders a Material card with elevation, shadow, Border radius etc...
/// Note: If the caller updates BoxDecoration of the [header] or [child] widget,
/// the caller must also set the borderRadius to [ROUNDED_RADIUS] in
/// BoxDecoration.
/// Note: [padding] applies to both header and body, if you want to have a
/// padding only for body and not for header (or vice versa) set it to zero here
/// and set the padding explicitly in the desired element.
class SmoothCard extends StatelessWidget {
const SmoothCard({
required this.child,
this.color,
this.margin = const EdgeInsetsDirectional.symmetric(
horizontal: SMALL_SPACE,
vertical: VERY_SMALL_SPACE,
),
this.padding = const EdgeInsetsDirectional.all(5.0),
this.elevation = 8.0,
this.elevationColor,
this.borderRadius,
this.ignoreDefaultSemantics = false,
this.clipBehavior,
});
const SmoothCard.angular({
required this.child,
this.color,
this.margin = const EdgeInsetsDirectional.symmetric(
horizontal: SMALL_SPACE,
vertical: VERY_SMALL_SPACE,
),
this.padding = const EdgeInsetsDirectional.all(5.0),
this.elevation = 8.0,
this.elevationColor,
this.ignoreDefaultSemantics = false,
this.clipBehavior,
}) : borderRadius = ANGULAR_BORDER_RADIUS;
const SmoothCard.flat({
required this.child,
this.color,
this.margin = const EdgeInsetsDirectional.only(
start: SMALL_SPACE,
end: SMALL_SPACE,
top: VERY_SMALL_SPACE,
bottom: VERY_SMALL_SPACE,
),
this.padding = const EdgeInsetsDirectional.all(5.0),
this.elevation = 0.0,
this.borderRadius,
this.ignoreDefaultSemantics = false,
this.clipBehavior,
}) : elevationColor = null;
final Widget child;
final Color? color;
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? padding;
final BorderRadiusGeometry? borderRadius;
final double elevation;
final Color? elevationColor;
final bool ignoreDefaultSemantics;
final Clip? clipBehavior;
@override
Widget build(BuildContext context) {
Widget result = child;
if (padding != null) {
result = Padding(padding: padding!, child: result);
}
if (ignoreDefaultSemantics) {
result = Semantics(
container: false,
explicitChildNodes: true,
child: child,
);
}
result = Material(
elevation: elevation,
shadowColor: elevationColor ?? const Color.fromARGB(25, 0, 0, 0),
borderRadius: borderRadius ?? ROUNDED_BORDER_RADIUS,
color:
color ??
(Theme.of(context).brightness == Brightness.light
? Colors.white
: Colors.black),
clipBehavior: clipBehavior ?? Clip.none,
child: result,
);
return margin == null ? result : Padding(padding: margin!, child: result);
}
}
class SmoothCardWithRoundedHeader extends StatelessWidget {
const SmoothCardWithRoundedHeader({
required this.title,
required this.child,
this.leading,
this.leadingIconSize,
this.leadingPadding,
this.trailing,
this.titleTextStyle,
this.titlePadding,
this.contentPadding,
this.titleBackgroundColor,
this.contentBackgroundColor,
this.borderRadius,
this.includeShadow = true,
super.key,
});
final String title;
final Widget? leading;
final double? leadingIconSize;
final EdgeInsetsGeometry? leadingPadding;
final Widget? trailing;
final Widget child;
final TextStyle? titleTextStyle;
final EdgeInsetsGeometry? titlePadding;
final EdgeInsetsGeometry? contentPadding;
final Color? titleBackgroundColor;
final Color? contentBackgroundColor;
final BorderRadius? borderRadius;
final bool includeShadow;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius ?? ROUNDED_BORDER_RADIUS,
boxShadow: includeShadow
? const <BoxShadow>[
BoxShadow(
color: Color(0x10000000),
blurRadius: 2.0,
spreadRadius: 0.0,
offset: Offset(0.0, 2.0),
),
]
: null,
),
child: Column(
children: <Widget>[
SmoothCardWithRoundedHeaderTop(
title: title,
titleBackgroundColor: titleBackgroundColor,
leading: leading,
leadingIconSize: leadingIconSize,
leadingPadding: leadingPadding,
trailing: trailing,
titleTextStyle: titleTextStyle,
titlePadding: titlePadding,
borderRadius: borderRadius,
),
SmoothCardWithRoundedHeaderBody(
contentBackgroundColor: contentBackgroundColor,
contentPadding: contentPadding,
borderRadius: borderRadius,
child: child,
),
],
),
);
}
}
class SmoothCardWithRoundedHeaderBanner extends StatelessWidget {
const SmoothCardWithRoundedHeaderBanner({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
final SmoothColorsThemeExtension extension = context
.extension<SmoothColorsThemeExtension>();
return CustomPaint(
painter: _SmoothCardWithRoundedHeaderBackgroundPainter(
color: context.lightTheme() ? extension.greyLight : extension.greyDark,
radius: ROUNDED_RADIUS,
shadowElevation:
SmoothCardWithRoundedHeaderTopShadowProvider.of(context)?.shadow ??
0.0,
),
child: child,
);
}
}
class SmoothCardWithRoundedHeaderTop extends StatelessWidget {
const SmoothCardWithRoundedHeaderTop({
required this.title,
this.titleBackgroundColor,
this.leading,
this.leadingIconSize,
this.leadingPadding,
this.trailing,
this.titleTextStyle,
this.titlePadding,
this.borderRadius,
this.banner,
});
final String title;
final Color? titleBackgroundColor;
final Widget? leading;
final double? leadingIconSize;
final EdgeInsetsGeometry? leadingPadding;
final Widget? trailing;
final TextStyle? titleTextStyle;
final EdgeInsetsGeometry? titlePadding;
final BorderRadius? borderRadius;
final Widget? banner;
static const double _DEFAULT_LEADING_ICON_SIZE = 17.0;
@override
Widget build(BuildContext context) {
final Color color = titleBackgroundColor ?? getHeaderColor(context);
return Semantics(
label: title,
excludeSemantics: true,
child: Column(
children: <Widget>[
if (banner != null) SmoothCardWithRoundedHeaderBanner(child: banner!),
CustomPaint(
painter: _SmoothCardWithRoundedHeaderBackgroundPainter(
color: color,
radius: borderRadius?.topRight ?? ROUNDED_RADIUS,
shadowElevation:
SmoothCardWithRoundedHeaderTopShadowProvider.of(
context,
)?.shadow ??
0.0,
),
child: Padding(
padding:
titlePadding ??
(trailing != null
? const EdgeInsetsDirectional.only(
top: 2.0,
start: LARGE_SPACE,
end: SMALL_SPACE,
bottom: 2.0,
)
: const EdgeInsetsDirectional.symmetric(
vertical: BALANCED_SPACE,
horizontal: LARGE_SPACE,
)),
child: Row(
children: <Widget>[
if (leading != null)
IconTheme(
data: IconThemeData(
color: titleBackgroundColor,
size: leadingIconSize ?? _DEFAULT_LEADING_ICON_SIZE,
),
child: DecoratedBox(
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Padding(
padding:
leadingPadding ??
const EdgeInsetsDirectional.all(6.0),
child: leading,
),
),
),
const SizedBox(width: MEDIUM_SPACE),
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style:
(titleTextStyle ??
Theme.of(context).textTheme.displaySmall)
?.copyWith(color: Colors.white),
),
),
if (trailing != null) ...<Widget>[
const SizedBox(width: MEDIUM_SPACE),
IconTheme(
data: const IconThemeData(
color: Colors.white,
size: 20.0,
),
child: trailing!,
),
],
],
),
),
),
],
),
);
}
static Color getHeaderColor(BuildContext context) {
final SmoothColorsThemeExtension extension = context
.extension<SmoothColorsThemeExtension>();
return context.lightTheme(listen: false)
? extension.primaryBlack
: Colors.black;
}
}
class SmoothCardWithRoundedHeaderTopShadowProvider extends InheritedWidget {
const SmoothCardWithRoundedHeaderTopShadowProvider({
required this.shadow,
required super.child,
super.key,
});
final double shadow;
static SmoothCardWithRoundedHeaderTopShadowProvider? of(
BuildContext context,
) {
final SmoothCardWithRoundedHeaderTopShadowProvider? result = context
.dependOnInheritedWidgetOfExactType<
SmoothCardWithRoundedHeaderTopShadowProvider
>();
return result;
}
@override
bool updateShouldNotify(
SmoothCardWithRoundedHeaderTopShadowProvider oldWidget,
) {
return oldWidget.shadow != shadow;
}
}
class SmoothCardWithRoundedHeaderBody extends StatelessWidget {
const SmoothCardWithRoundedHeaderBody({
required this.child,
this.contentPadding,
this.contentBackgroundColor,
this.borderRadius,
});
final Widget child;
final EdgeInsetsGeometry? contentPadding;
final Color? contentBackgroundColor;
final BorderRadius? borderRadius;
@override
Widget build(BuildContext context) {
return SmoothCard(
margin: EdgeInsetsDirectional.zero,
padding:
contentPadding ?? const EdgeInsetsDirectional.only(top: MEDIUM_SPACE),
borderRadius: borderRadius ?? ROUNDED_BORDER_RADIUS,
color:
contentBackgroundColor ??
(context.darkTheme()
? context
.extension<SmoothColorsThemeExtension>()
.primaryUltraBlack
: null),
child: child,
);
}
}
/// We need this [CustomPainter] to draw the background below the other card
class _SmoothCardWithRoundedHeaderBackgroundPainter extends CustomPainter {
_SmoothCardWithRoundedHeaderBackgroundPainter({
required Color color,
required this.radius,
required this.shadowElevation,
}) : _paint = Paint()..color = color;
final Radius radius;
final Paint _paint;
final double shadowElevation;
@override
void paint(Canvas canvas, Size size) {
final Path path = Path()
..moveTo(0, radius.y)
..lineTo(0, size.height + radius.y)
..arcToPoint(
Offset(radius.x, size.height),
radius: radius,
clockwise: true,
)
..lineTo(size.width - radius.x, size.height)
..arcToPoint(
Offset(size.width, size.height + radius.y),
radius: radius,
clockwise: true,
)
..lineTo(size.width, radius.y)
..arcToPoint(
Offset(size.width - radius.x, 0),
radius: radius,
clockwise: false,
)
..lineTo(radius.x, 0)
..arcToPoint(Offset(0, radius.y), radius: radius, clockwise: false)
..close();
if (shadowElevation > 0.0) {
canvas.drawShadow(
path,
Colors.black.withValues(alpha: shadowElevation),
2.0,
false,
);
}
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(
_SmoothCardWithRoundedHeaderBackgroundPainter oldDelegate,
) => shadowElevation != oldDelegate.shadowElevation;
@override
bool shouldRebuildSemantics(
_SmoothCardWithRoundedHeaderBackgroundPainter oldDelegate,
) => false;
}
class SmoothCardHeaderButton extends StatelessWidget {
const SmoothCardHeaderButton({
required this.tooltip,
required this.child,
required this.onTap,
this.padding,
super.key,
});
final String tooltip;
final Widget child;
final VoidCallback onTap;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Semantics(
label: tooltip,
button: true,
excludeSemantics: true,
child: Tooltip(
message: tooltip,
child: InkWell(
customBorder: const CircleBorder(),
onTap: onTap,
child: Padding(
padding: padding ?? const EdgeInsetsDirectional.all(MEDIUM_SPACE),
child: child,
),
),
),
),
);
}
}