Created
September 21, 2024 09:59
-
-
Save SEGVeenstra/dda3861ae51964afea939475372cb605 to your computer and use it in GitHub Desktop.
SciFiContainer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'dart:math'; | |
| import 'package:collection/collection.dart'; | |
| void main() { | |
| runApp(const MyApp()); | |
| } | |
| class MyApp extends StatelessWidget { | |
| const MyApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| theme: ThemeData.dark(), | |
| debugShowCheckedModeBanner: false, | |
| home: Scaffold( | |
| body: Center( | |
| child: SciFiContainer( | |
| // This color will be used for the border | |
| foregroundColor: Colors.cyan, | |
| // This color will be used for the background | |
| backgroundColor: Colors.cyan.withOpacity(.2), | |
| // You can specify the 'size' of the corners. | |
| // The corners are cut in 45 degree angles | |
| corners: SciFiContainerCorners.only( | |
| bottomRight: 32.0, | |
| topLeft: 16.0, | |
| ), | |
| // You can add borders to your container to make it look more SciFi-like | |
| borders: SciFiContainerBorders.only( | |
| top: SciFiContainerBorderIndent( | |
| start: .2, // Where the indent should start 0.0 - 1.0 | |
| end: .7, // Where the indent should end 0.0 - 1.0 | |
| depth: 16, // DHow deep the indent goes inside the container | |
| ), | |
| bottom: SciFiContainerBorderIndent( | |
| start: .15, | |
| end: .75, | |
| depth: -16, // Negative numbers will make it an outdent | |
| ), | |
| ), | |
| // You can add an onTap function | |
| onTap: () {}, | |
| // This container will show the automatically calculated padding | |
| // SciFiContainer will add padding to make sure your child fits | |
| // inside the container. (not very optimised) | |
| // You can override this behavior by setting `SciFiContainer.padding` | |
| child: Container( | |
| width: 300, | |
| height: 200, | |
| color: Colors.green.withOpacity(.1), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class SciFiContainer extends StatelessWidget { | |
| const SciFiContainer({ | |
| super.key, | |
| required this.child, | |
| this.corners = const SciFiContainerCorners.zero(), | |
| this.borders = const SciFiContainerBorders.none(), | |
| required this.foregroundColor, | |
| required this.backgroundColor, | |
| this.padding, | |
| this.onTap, | |
| }); | |
| final Widget child; | |
| final Color foregroundColor; | |
| final Color backgroundColor; | |
| final SciFiContainerCorners corners; | |
| final SciFiContainerBorders borders; | |
| final EdgeInsets? padding; | |
| final VoidCallback? onTap; | |
| @override | |
| Widget build(BuildContext context) { | |
| final topPadding = [ | |
| corners.topLeft / 2 - borders.top.offset.clampNegative(), | |
| corners.topRight / 2 - borders.top.offset.clampNegative(), | |
| borders.top.offset, | |
| ].max; | |
| final rightPadding = <double>[ | |
| corners.topRight / 2 - borders.right.offset.clampNegative(), | |
| corners.bottomRight / 2 - borders.right.offset.clampNegative(), | |
| borders.right.offset, | |
| ].max; | |
| final bottomPadding = [ | |
| corners.bottomLeft / 2 - borders.bottom.offset.clampNegative(), | |
| corners.bottomRight / 2 - borders.bottom.offset.clampNegative(), | |
| borders.bottom.offset, | |
| ].max; | |
| final leftPadding = [ | |
| corners.topLeft / 2 - borders.left.offset.clampNegative(), | |
| corners.bottomLeft / 2 - borders.left.offset.clampNegative(), | |
| borders.left.offset, | |
| ].max; | |
| final padding = this.padding ?? | |
| EdgeInsets.only( | |
| left: leftPadding, | |
| top: topPadding, | |
| bottom: bottomPadding, | |
| right: rightPadding, | |
| ); | |
| return ClipPath( | |
| clipBehavior: Clip.hardEdge, | |
| clipper: _SciFiClipper( | |
| borders: borders, | |
| corners: corners, | |
| ), | |
| child: Material( | |
| color: backgroundColor, | |
| child: InkWell( | |
| onTap: onTap, | |
| child: CustomPaint( | |
| foregroundPainter: SciFiForegroundPainter( | |
| strokeColor: foregroundColor, | |
| corners: corners, | |
| borders: borders, | |
| ), | |
| child: Padding( | |
| padding: padding, | |
| child: DefaultTextStyle( | |
| style: TextStyle( | |
| color: foregroundColor, | |
| ), | |
| child: child, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class SciFiForegroundPainter extends CustomPainter { | |
| SciFiForegroundPainter({ | |
| required this.strokeColor, | |
| required this.corners, | |
| required this.borders, | |
| }); | |
| final Color strokeColor; | |
| final SciFiContainerCorners corners; | |
| final SciFiContainerBorders borders; | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| final strokePaint = Paint() | |
| ..color = strokeColor | |
| ..strokeWidth = 2 | |
| ..style = PaintingStyle.stroke; | |
| final path = _buildPath(size, corners, borders); | |
| canvas.drawPath(path, strokePaint); | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) { | |
| return true; | |
| } | |
| } | |
| class SciFiContainerCorners { | |
| const SciFiContainerCorners.only({ | |
| this.bottomLeft = 0, | |
| this.bottomRight = 0, | |
| this.topLeft = 0, | |
| this.topRight = 0, | |
| }); | |
| const SciFiContainerCorners.zero() | |
| : topLeft = 0, | |
| topRight = 0, | |
| bottomLeft = 0, | |
| bottomRight = 0; | |
| final double topLeft; | |
| final double topRight; | |
| final double bottomLeft; | |
| final double bottomRight; | |
| } | |
| class SciFiContainerBorders { | |
| const SciFiContainerBorders.none() | |
| : top = const SciFiContainerBorderNone(), | |
| right = const SciFiContainerBorderNone(), | |
| bottom = const SciFiContainerBorderNone(), | |
| left = const SciFiContainerBorderNone(); | |
| const SciFiContainerBorders.only({ | |
| this.top = const SciFiContainerBorderNone(), | |
| this.right = const SciFiContainerBorderNone(), | |
| this.bottom = const SciFiContainerBorderNone(), | |
| this.left = const SciFiContainerBorderNone(), | |
| }); | |
| final SciFiContainerBorderDecoration top; | |
| final SciFiContainerBorderDecoration bottom; | |
| final SciFiContainerBorderDecoration left; | |
| final SciFiContainerBorderDecoration right; | |
| } | |
| sealed class SciFiContainerBorderDecoration { | |
| const SciFiContainerBorderDecoration(); | |
| final double offset = 0; | |
| List<Point<double>> calculateTopBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) => | |
| []; | |
| List<Point<double>> calculateRightBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) => | |
| []; | |
| List<Point<double>> calculateBottomBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) => | |
| []; | |
| List<Point<double>> calculateLeftBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) => | |
| []; | |
| } | |
| class SciFiContainerBorderIndent extends SciFiContainerBorderDecoration { | |
| const SciFiContainerBorderIndent({ | |
| required this.start, | |
| required this.end, | |
| required this.depth, | |
| }); | |
| final double start; | |
| final double end; | |
| final double depth; | |
| @override | |
| double get offset => depth; | |
| @override | |
| List<Point<double>> calculateTopBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) { | |
| final borderWidth = end.x - start.x; | |
| final borderPoint1 = Point( | |
| start.x + (borderWidth * this.start), | |
| start.y, | |
| ); | |
| final borderPoint2 = Point( | |
| start.x + (borderWidth * this.start) + depth.abs(), | |
| start.y + depth, | |
| ); | |
| final borderPoint3 = Point( | |
| start.x + (borderWidth * this.end) - depth.abs(), | |
| start.y + depth, | |
| ); | |
| final borderPoint4 = Point( | |
| start.x + (borderWidth * this.end), | |
| start.y, | |
| ); | |
| return [ | |
| borderPoint1, | |
| borderPoint2, | |
| borderPoint3, | |
| borderPoint4, | |
| ]; | |
| } | |
| @override | |
| List<Point<double>> calculateRightBorder( | |
| Point<double> start, Point<double> end) { | |
| final borderLength = end.y - start.y; | |
| final borderPoint1 = Point( | |
| start.x, | |
| start.y + (borderLength * this.start), | |
| ); | |
| final borderPoint2 = Point( | |
| start.x - depth, | |
| start.y + (borderLength * this.start) + depth.abs(), | |
| ); | |
| final borderPoint3 = Point( | |
| start.x - depth, | |
| start.y + (borderLength * this.end) - depth.abs(), | |
| ); | |
| final borderPoint4 = Point( | |
| start.x, | |
| start.y + (borderLength * this.end), | |
| ); | |
| return [ | |
| borderPoint1, | |
| borderPoint2, | |
| borderPoint3, | |
| borderPoint4, | |
| ]; | |
| } | |
| @override | |
| List<Point<double>> calculateBottomBorder( | |
| Point<double> start, | |
| Point<double> end, | |
| ) { | |
| final borderLength = start.x - end.x; | |
| final borderPoint1 = Point( | |
| start.x - (borderLength * this.start), | |
| start.y, | |
| ); | |
| final borderPoint2 = Point( | |
| start.x - (borderLength * this.start) - depth.abs(), | |
| start.y - depth, | |
| ); | |
| final borderPoint3 = Point( | |
| start.x - (borderLength * this.end) + depth.abs(), | |
| start.y - depth, | |
| ); | |
| final borderPoint4 = Point( | |
| start.x - (borderLength * this.end), | |
| start.y, | |
| ); | |
| return [ | |
| borderPoint1, | |
| borderPoint2, | |
| borderPoint3, | |
| borderPoint4, | |
| ]; | |
| } | |
| @override | |
| List<Point<double>> calculateLeftBorder( | |
| Point<double> start, Point<double> end) { | |
| final borderLength = end.y - start.y; | |
| final borderPoint1 = Point( | |
| start.x, | |
| start.y + (borderLength * this.start), | |
| ); | |
| final borderPoint2 = Point( | |
| start.x + depth, | |
| start.y + (borderLength * this.start) - depth.abs(), | |
| ); | |
| final borderPoint3 = Point( | |
| start.x + depth, | |
| start.y + (borderLength * this.end) + depth.abs(), | |
| ); | |
| final borderPoint4 = Point( | |
| start.x, | |
| start.y + (borderLength * this.end), | |
| ); | |
| return [ | |
| borderPoint1, | |
| borderPoint2, | |
| borderPoint3, | |
| borderPoint4, | |
| ]; | |
| } | |
| } | |
| class SciFiContainerBorderNone extends SciFiContainerBorderDecoration { | |
| const SciFiContainerBorderNone(); | |
| } | |
| class _SciFiClipper extends CustomClipper<Path> { | |
| _SciFiClipper({ | |
| required this.corners, | |
| required this.borders, | |
| }); | |
| final SciFiContainerCorners corners; | |
| final SciFiContainerBorders borders; | |
| @override | |
| Path getClip(Size size) { | |
| return _buildPath(size, corners, borders); | |
| } | |
| @override | |
| bool shouldReclip(covariant CustomClipper<Path> oldClipper) { | |
| return true; | |
| } | |
| } | |
| Path _buildPath( | |
| Size size, | |
| SciFiContainerCorners corners, | |
| SciFiContainerBorders borders, | |
| ) { | |
| // Define the points for the base shape | |
| final p1 = | |
| Point<double>(0, corners.topLeft - borders.top.offset.clampNegative()); | |
| final p2 = | |
| Point<double>(corners.topLeft, 0 - borders.top.offset.clampNegative()); | |
| final p3 = Point<double>( | |
| size.width - corners.topRight + borders.right.offset.clampNegative(), | |
| 0 - borders.top.offset.clampNegative()); | |
| final p4 = Point<double>(size.width + borders.right.offset.clampNegative(), | |
| corners.topRight - borders.top.offset.clampNegative()); | |
| final p5 = Point<double>( | |
| size.width + borders.right.offset.clampNegative(), | |
| size.height - corners.bottomRight + borders.bottom.offset.clampNegative(), | |
| ); | |
| final p6 = Point<double>( | |
| size.width - corners.bottomRight + borders.right.offset.clampNegative(), | |
| size.height + borders.bottom.offset.clampNegative(), | |
| ); | |
| final p7 = Point<double>( | |
| corners.bottomLeft, | |
| size.height + borders.bottom.offset.clampNegative(), | |
| ); | |
| final p8 = Point<double>( | |
| 0, | |
| size.height - corners.bottomLeft + borders.bottom.offset.clampNegative(), | |
| ); | |
| // Draw the path based on the points | |
| final path = Path(); | |
| path.moveTo(p1.x, p1.y); | |
| path.lineTo(p2.x, p2.y); | |
| for (final position in borders.top.calculateTopBorder(p2, p3)) { | |
| path.lineTo(position.x, position.y); | |
| } | |
| path.lineTo(p3.x, p3.y); | |
| path.lineTo(p4.x, p4.y); | |
| for (final position in borders.right.calculateRightBorder(p4, p5)) { | |
| path.lineTo(position.x, position.y); | |
| } | |
| path.lineTo(p5.x, p5.y); | |
| path.lineTo(p6.x, p6.y); | |
| for (final position in borders.bottom.calculateBottomBorder(p6, p7)) { | |
| path.lineTo(position.x, position.y); | |
| } | |
| path.lineTo(p7.x, p7.y); | |
| path.lineTo(p8.x, p8.y); | |
| for (final position in borders.left.calculateLeftBorder(p8, p1)) { | |
| path.lineTo(position.x, position.y); | |
| } | |
| path.lineTo(p1.x, p1.y); | |
| return path; | |
| } | |
| extension DoubleExt on double { | |
| double clampNegative() { | |
| if (this > 0) { | |
| return 0; | |
| } else { | |
| return this; | |
| } | |
| } | |
| double clampPositive() { | |
| if (this < 0) { | |
| return 0; | |
| } else { | |
| return this; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment