Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active July 13, 2024 07:21
Show Gist options
  • Save PlugFox/9ab286842c7fab41dd8181a9f3a8461b to your computer and use it in GitHub Desktop.
Save PlugFox/9ab286842c7fab41dd8181a9f3a8461b to your computer and use it in GitHub Desktop.
Custom Clipper Ticket with Circular Cutouts (Serrator)
/*
* Custom Clipper Ticket with Circular Cutouts (Serrator)
* https://gist.github.com/PlugFox/9ab286842c7fab41dd8181a9f3a8461b
* https://dartpad.dev?id=9ab286842c7fab41dd8181a9f3a8461b
* Mike Matiunin <plugfox@gmail.com>, 13 July 2024
*/
import 'package:flutter/material.dart';
void main() => runApp(
MaterialApp(
home: Scaffold(
body: const SafeArea(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Ticket(),
),
),
),
),
),
);
class Ticket extends StatelessWidget {
const Ticket({
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return FittedBox(
fit: BoxFit.scaleDown,
child: SizedBox(
width: 420,
height: 240,
child: ClipPath(
clipper: TicketClipper(
radius: 8,
padding: 4,
margin: const EdgeInsets.fromLTRB(
8 + 16, // To cover left card margin and radius
8, // To cover top card margin
8 + 16, // To cover right card margin and radius
8,
),
),
child: ShaderMask(
shaderCallback: (Rect bounds) => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.transparent,
Colors.black87,
],
stops: <double>[.0, .5, 1.0],
).createShader(bounds),
blendMode: BlendMode.srcATop,
child: Card(
margin: const EdgeInsets.all(8),
color: theme.primaryColor,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 32, 16, 8),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
// ...
Text(
('Ticket with Circular Cutouts\n' * 6)
.trim()
.toUpperCase(),
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w900,
fontStyle: FontStyle.italic,
),
),
// ...
],
),
),
),
),
),
),
);
}
}
class TicketClipper extends CustomClipper<Path> {
TicketClipper({
this.radius = 12,
this.padding = 6,
this.margin = EdgeInsets.zero,
});
/// Radius of the circular cutouts
final double radius;
/// Padding is the space between the circles
final double padding;
/// Margin is the space between the clipper and the card edges
final EdgeInsets margin;
@override
Path getClip(Size size) {
final diameter = radius * 2;
final path = Path()..lineTo(margin.left, 0);
final serratorWidth = size.width - margin.horizontal;
final circles = serratorWidth ~/ (diameter + this.padding);
final padding = (serratorWidth - circles * diameter) / (circles + 1);
path.lineTo(margin.left + padding, margin.top);
for (var i = 0, x = margin.left + padding;
i < circles;
i++, x += diameter + padding) {
path
..arcToPoint(
Offset(x + diameter, margin.top),
radius: Radius.circular(radius),
clockwise: false,
)
..lineTo(x + diameter + padding, margin.top);
}
return path
..lineTo(size.width - margin.right, 0)
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height)
..lineTo(0, 0)
..close();
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) =>
oldClipper is! TicketClipper ||
oldClipper.radius != radius ||
oldClipper.padding != padding ||
oldClipper.margin != margin;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment