Files
CityCards_Customer_Flutter/lib/my_pass/widgets/pass_widget.dart

286 lines
9.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:google_fonts/google_fonts.dart';
class PassTicketCard extends StatelessWidget {
final dynamic pass;
const PassTicketCard({super.key, required this.pass});
@override
Widget build(BuildContext context) {
// Dimensions tuned to your screenshot
final double cardWidth = MediaQuery.of(context).size.width - 32.w;
final double topSectionHeight = 105.h; // where dotted line sits
final double bottomSectionHeight = 50.h;
final double cardHeight = topSectionHeight + bottomSectionHeight;
return SizedBox(
width: cardWidth,
child: CustomPaint(
// paints white background, border, corner radius, side cuts, shadow, and divider dots
painter: _TicketBackgroundPainter(
cornerRadius: 16.r,
notchRadius: 9.r,
dividerY: topSectionHeight,
borderColor: Colors.white,
shadowColor: Colors.black.withOpacity(0.08),
),
child: ClipPath(
// actual clipping so child content never bleeds outside the shape
clipper: _TicketClipper(
cornerRadius: 16.r,
notchRadius: 9.r,
dividerY: topSectionHeight,
),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
child: Column(
children: [
// ---------- TOP SECTION ----------
SizedBox(
height: topSectionHeight - 12.h, // keep space for the dots line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(10.r),
child: Image.asset(
pass.imageUrl,
height: 80.h,
width: 80.w,
fit: BoxFit.cover,
),
),
SizedBox(width: 10.w),
// details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (pass.isActive)
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w, vertical: 3.h),
decoration: BoxDecoration(
color: const Color(0xff439F6E),
borderRadius: BorderRadius.circular(30.r),
),
child: Text(
"Active",
style: GoogleFonts.poppins(
color: Colors.white,
fontSize: 10.sp,
fontWeight: FontWeight.w400,
),
),
),
SizedBox(width: 8.w),
Text(
pass.duration, // "2 Days"
style: GoogleFonts.poppins(
color: Colors.black87,
fontSize: 12.sp,
),
),
],
),
SizedBox(height: 10.h),
Text(
pass.title,
style: GoogleFonts.poppins(
fontWeight: FontWeight.w600,
fontSize: 18.sp,
height: 1.1,
),
),
SizedBox(height: 4.h),
Text(
"Adults-${pass.adults} • Kids-${pass.kids}",
style: GoogleFonts.poppins(
color: Colors.black54,
fontSize: 11.sp,
),
),
],
),
),
// QR chip
CircleAvatar(
radius: 20.r,
backgroundColor: Color(0xffFEE7E7),
child: Image.asset(
"assets/images/qr_image.png",
scale: 6,
),
)
],
),
),
// space exactly where the dotted line is painted by the painter
SizedBox(height: 15.h),
// ---------- BOTTOM SECTION ----------
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Valid Till: ${pass.validity}",
style: GoogleFonts.poppins(
fontSize: 11.sp,
color: Colors.black,
fontWeight: FontWeight.w400
),
),
Text(
pass.city, // "Melbourne"
style: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
fontSize: 13.sp,
),
),
],
),
),
],
),
),
),
),
);
}
}
/// Clips the ticket with rounded corners and 2 side “cuts” centered at dividerY
class _TicketClipper extends CustomClipper<Path> {
final double cornerRadius;
final double notchRadius;
final double dividerY;
_TicketClipper({
required this.cornerRadius,
required this.notchRadius,
required this.dividerY,
});
@override
Path getClip(Size size) {
final rrectPath = Path()
..addRRect(RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
Radius.circular(cornerRadius),
));
final cuts = Path()
..addOval(Rect.fromCircle(center: Offset(0, dividerY), radius: notchRadius))
..addOval(Rect.fromCircle(center: Offset(size.width, dividerY), radius: notchRadius));
// Rounded-rect MINUS the two circles
return Path.combine(PathOperation.difference, rrectPath, cuts);
}
@override
bool shouldReclip(covariant _TicketClipper old) =>
cornerRadius != old.cornerRadius ||
notchRadius != old.notchRadius ||
dividerY != old.dividerY;
}
/// Paints fill, border, shadow and the dotted perforation line
class _TicketBackgroundPainter extends CustomPainter {
final double cornerRadius;
final double notchRadius;
final double dividerY;
final Color borderColor;
final Color shadowColor;
_TicketBackgroundPainter({
required this.cornerRadius,
required this.notchRadius,
required this.dividerY,
required this.borderColor,
required this.shadowColor,
});
Path _ticketPath(Size size) {
final clipper = _TicketClipper(
cornerRadius: cornerRadius,
notchRadius: notchRadius,
dividerY: dividerY,
);
return clipper.getClip(size);
}
@override
void paint(Canvas canvas, Size size) {
final path = _ticketPath(size);
// Realistic layered shadow
canvas.save();
canvas.translate(0, 2); // tiny downward offset for depth
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.10)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6);
canvas.drawPath(path, shadowPaint);
canvas.restore();
// Subtle ambient shadow (light spread around)
final ambientShadowPaint = Paint()
..color = Colors.black.withOpacity(0.04)
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
canvas.drawPath(path, ambientShadowPaint);
// Fill background
final fillPaint = Paint()
..style = PaintingStyle.fill
..color = const Color(0xffFFFBFB);
canvas.drawPath(path, fillPaint);
// Border stroke
final strokePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0.8
..color = const Color(0xffE5E5E5);
canvas.drawPath(path, strokePaint);
// 🔹 Dotted perforation line
final dashPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1
..color = const Color(0xff787878);
const double dashWidth = 4;
const double dashSpace = 4;
double startX = 12;
final double endX = size.width - 12;
while (startX < endX) {
final double currentEnd = (startX + dashWidth).clamp(0, endX);
canvas.drawLine(
Offset(startX, dividerY),
Offset(currentEnd, dividerY),
dashPaint,
);
startX += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(covariant _TicketBackgroundPainter oldDelegate) {
return cornerRadius != oldDelegate.cornerRadius ||
notchRadius != oldDelegate.notchRadius ||
dividerY != oldDelegate.dividerY ||
borderColor != oldDelegate.borderColor ||
shadowColor != oldDelegate.shadowColor;
}
}