import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class PuzzlePiece extends StatefulWidget { final CachedNetworkImage image; final Size imageSize; final int row; final int col; final int maxRow; final int maxCol; final Function bringToTop; final Function sendToBack; final double initialTop; final double initialLeft; PuzzlePiece({ Key? key, required this.image, required this.imageSize, required this.row, required this.col, required this.maxRow, required this.maxCol, required this.bringToTop, required this.sendToBack, required this.initialTop, required this.initialLeft, }) : super(key: key); @override _PuzzlePieceState createState() => _PuzzlePieceState(); } class _PuzzlePieceState extends State { // the piece initial top offset double? top; // the piece initial left offset double? left; // can we move the piece ? bool isMovable = true; GlobalKey _widgetPieceKey = GlobalKey(); @override void initState() { super.initState(); top = widget.initialTop; left = widget.initialLeft; if (widget.row == 0 && widget.col == 0) { isMovable = false; top = 0; left = 0; } } @override Widget build(BuildContext context) { return AnimatedPositioned( duration: isMovable ? Duration.zero : const Duration(milliseconds: 300), curve: Curves.easeOutBack, top: top, left: left, width: widget.imageSize.width, child: AnimatedScale( duration: const Duration(milliseconds: 300), scale: isMovable ? 1.05 : 1.0, child: Container( key: _widgetPieceKey, decoration: widget.col == 0 && widget.row == 0 ? BoxDecoration( border: Border.all( color: Colors.black.withOpacity(0.3), width: 0.5, ), ) : null, child: GestureDetector( onTap: () { if (isMovable) { widget.bringToTop(widget); } }, onPanStart: (_) { if (isMovable) { widget.bringToTop(widget); } }, onPanUpdate: (dragUpdateDetails) { if (isMovable) { setState(() { top = top! + dragUpdateDetails.delta.dy; left = left! + dragUpdateDetails.delta.dx; // Target position is (0,0) relative to its original offset in the image if (top!.abs() < 15 && left!.abs() < 15) { setState(() { top = 0; left = 0; isMovable = false; }); widget.sendToBack(widget); } }); } }, child: PhysicalShape( clipper: PuzzlePieceClipper( widget.row, widget.col, widget.maxRow, widget.maxCol), elevation: isMovable ? 4.0 : 0.0, color: Colors.transparent, shadowColor: Colors.black.withOpacity(0.5), child: ClipPath( child: CustomPaint( foregroundPainter: PuzzlePiecePainter( widget.row, widget.col, widget.maxRow, widget.maxCol), child: widget.image), clipper: PuzzlePieceClipper( widget.row, widget.col, widget.maxRow, widget.maxCol), ), ), ), ), ), ); } } // this class is used to clip the image to the puzzle piece path class PuzzlePieceClipper extends CustomClipper { final int row; final int col; final int maxRow; final int maxCol; PuzzlePieceClipper(this.row, this.col, this.maxRow, this.maxCol); @override Path getClip(Size size) { return getPiecePath(size, row, col, maxRow, maxCol); } @override bool shouldReclip(CustomClipper oldClipper) => false; } // this class is used to draw a border around the clipped image class PuzzlePiecePainter extends CustomPainter { final int row; final int col; final int maxRow; final int maxCol; PuzzlePiecePainter(this.row, this.col, this.maxRow, this.maxCol); @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = Colors.black.withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 1.0; canvas.drawPath(getPiecePath(size, row, col, maxRow, maxCol), paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } } // this is the path used to clip the image and, then, to draw a border around it; here we actually draw the puzzle piece Path getPiecePath(Size size, int row, int col, int maxRow, int maxCol) { final width = size.width / maxCol; final height = size.height / maxRow; final offsetX = col * width; final offsetY = row * height; final bumpSize = height / 4; var path = Path(); path.moveTo(offsetX, offsetY); if (row == 0) { // top side piece path.lineTo(offsetX + width, offsetY); } else { // top bump path.lineTo(offsetX + width / 3, offsetY); path.cubicTo( offsetX + width / 6, offsetY - bumpSize, offsetX + width / 6 * 5, offsetY - bumpSize, offsetX + width / 3 * 2, offsetY); path.lineTo(offsetX + width, offsetY); } if (col == maxCol - 1) { // right side piece path.lineTo(offsetX + width, offsetY + height); } else { // right bump path.lineTo(offsetX + width, offsetY + height / 3); path.cubicTo( offsetX + width - bumpSize, offsetY + height / 6, offsetX + width - bumpSize, offsetY + height / 6 * 5, offsetX + width, offsetY + height / 3 * 2); path.lineTo(offsetX + width, offsetY + height); } if (row == maxRow - 1) { // bottom side piece path.lineTo(offsetX, offsetY + height); } else { // bottom bump path.lineTo(offsetX + width / 3 * 2, offsetY + height); path.cubicTo( offsetX + width / 6 * 5, offsetY + height - bumpSize, offsetX + width / 6, offsetY + height - bumpSize, offsetX + width / 3, offsetY + height); path.lineTo(offsetX, offsetY + height); } if (col == 0) { // left side piece path.close(); } else { // left bump path.lineTo(offsetX, offsetY + height / 3 * 2); path.cubicTo( offsetX - bumpSize, offsetY + height / 6 * 5, offsetX - bumpSize, offsetY + height / 6, offsetX, offsetY + height / 3); path.close(); } return path; }