Skip to content

Instantly share code, notes, and snippets.

@valterh4ck3r
Created July 28, 2025 22:55
Show Gist options
  • Save valterh4ck3r/933084477a1de58402ea4d34518d6daf to your computer and use it in GitHub Desktop.
Save valterh4ck3r/933084477a1de58402ea4d34518d6daf to your computer and use it in GitHub Desktop.
Rematch Flutter App
import 'package:flutter/material.dart';
class CollapsibleActions extends StatefulWidget {
const CollapsibleActions({super.key , required this.refreshAction , required this.eraseAction});
final Function refreshAction;
final Function eraseAction;
@override
State<CollapsibleActions> createState() => _CollapsibleActionsState();
}
class _CollapsibleActionsState extends State<CollapsibleActions> {
bool isExpanded = false;
@override
Widget build(BuildContext context) {
return Positioned(
right: 24,
bottom: 24,
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: isExpanded ? 160 : 68,
height: isExpanded ? 68 : 68,
decoration: BoxDecoration(
color: isExpanded ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(28),
),
child: Row(
children: [
isExpanded ? buildExpanded() : buildCollapsed(),
],
),
),
);
}
Widget buildExpanded() {
return Expanded(
child: ListView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(horizontal: 8),
children: [
IconButton(
icon: Icon(Icons.close, color: Colors.black),
onPressed: () => setState(() { isExpanded = !isExpanded; }),
),
IconButton(
icon: Icon(Icons.refresh, color: Colors.black),
onPressed: () => widget.refreshAction.call(),
),
IconButton(
icon: Icon(Icons.delete_forever, color: Colors.black),
onPressed: () => widget.eraseAction.call(),
),
],
),
);
}
Widget buildCollapsed() {
return IconButton(
icon: Icon(Icons.settings, color: Colors.white, size: 48,),
onPressed: () => setState(() { isExpanded = !isExpanded; }),
);
}
}
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../widgets/collapsible_actions.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
String backgroundPath = "assets/images/field-rematch.png";
String allyMarkerPath = "assets/images/ally-marker.png";
String enemyMarkerPath = "assets/images/enemy-marker.png";
String ballMarkerPath = "assets/images/ball-marker.png";
late Offset positionAlly1;
late Offset positionAlly2;
late Offset positionAlly3;
late Offset positionAlly4;
late Offset positionAlly5;
late Offset positionEnemy1;
late Offset positionEnemy2;
late Offset positionEnemy3;
late Offset positionEnemy4;
late Offset positionEnemy5;
late Offset positionBall;
Offset dragStartOffset = Offset.zero;
late Widget allyMarkerWidget;
late Widget enemyMarkerWidget;
late Widget ballMarkerWidget;
late List<Offset> points;
bool firstBuild = true;
@override
void initState() {
allyMarkerWidget = SizedBox(
width: 40,
height: 40,
child: Image.asset(
allyMarkerPath,
fit: BoxFit.cover,
),
);
enemyMarkerWidget = SizedBox(
width: 40,
height: 40,
child: Image.asset(
enemyMarkerPath,
fit: BoxFit.cover,
),
);
ballMarkerWidget = SizedBox(
width: 40,
height: 40,
child: Image.asset(
ballMarkerPath,
fit: BoxFit.cover,
),
);
super.initState();
}
@override
Widget build(BuildContext context) {
if(firstBuild){
startMarkers(context);
startPoints(context);
firstBuild = false;
}
return Scaffold(
backgroundColor: Colors.black,
body: buildContent(),
);
}
Widget buildContent() {
return SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
// Background
Positioned.fill(
child: Center(
child: Image.asset(
backgroundPath,
fit: BoxFit.fitHeight,
width: 600,
height: 900,
),
),
),
// Custom Paint
Positioned.fill(
child: GestureDetector(
onPanUpdate: (details) {
RenderBox box = context.findRenderObject() as RenderBox;
Offset localPosition =
box.globalToLocal(details.globalPosition);
setState(() {
points.add(localPosition);
});
},
onPanEnd: (details) {
points.add(Offset.zero); // Para separar os traços
},
child: CustomPaint(
painter: ScratchPainter(points),
),
),
),
// Enemy Markers
...buildEnemyMarkers(),
// Ally Markers
...buildAllyMarkers(),
// Ball Marker
Positioned(
left: positionBall.dx,
top: positionBall.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionBall += details.delta;
print(positionBall);
});
},
child: ballMarkerWidget,
),
),
// Actions
CollapsibleActions(
refreshAction: () {
setState(() {
startMarkers(context);
});
},
eraseAction: () {
setState(() {
startPoints(context);
});
},
)
],
),
);
}
List<Widget> buildEnemyMarkers() {
return [
// Enemy Marker 1
Positioned(
left: positionEnemy1.dx,
top: positionEnemy1.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionEnemy1 += details.delta;
});
},
child: enemyMarkerWidget,
),
),
// Enemy Marker 2
Positioned(
left: positionEnemy2.dx,
top: positionEnemy2.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionEnemy2 += details.delta;
});
},
child: enemyMarkerWidget,
),
),
// Enemy Marker 3
Positioned(
left: positionEnemy3.dx,
top: positionEnemy3.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionEnemy3 += details.delta;
});
},
child: enemyMarkerWidget,
),
),
// Enemy Marker 4
Positioned(
left: positionEnemy4.dx,
top: positionEnemy4.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionEnemy4 += details.delta;
});
},
child: enemyMarkerWidget,
),
),
// Enemy Marker 5
Positioned(
left: positionEnemy5.dx,
top: positionEnemy5.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionEnemy5 += details.delta;
});
},
child: enemyMarkerWidget,
),
),
];
}
List<Widget> buildAllyMarkers() {
return [
Positioned(
left: positionAlly1.dx,
top: positionAlly1.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionAlly1 += details.delta;
});
},
child: allyMarkerWidget,
),
),
// Ally Enemy 2
Positioned(
left: positionAlly2.dx,
top: positionAlly2.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionAlly2 += details.delta;
});
},
child: allyMarkerWidget,
),
),
// Ally Enemy 3
Positioned(
left: positionAlly3.dx,
top: positionAlly3.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionAlly3 += details.delta;
});
},
child: allyMarkerWidget,
),
),
// Ally Enemy 4
Positioned(
left: positionAlly4.dx,
top: positionAlly4.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionAlly4 += details.delta;
});
},
child: allyMarkerWidget,
),
),
// Ally Enemy 5
Positioned(
left: positionAlly5.dx,
top: positionAlly5.dy,
child: GestureDetector(
onPanStart: (details) {
dragStartOffset = details.localPosition;
},
onPanUpdate: (details) {
setState(() {
positionAlly5 += details.delta;
});
},
child: allyMarkerWidget,
),
),
];
}
startMarkers(BuildContext context) {
positionAlly1 = Offset(MediaQuery.of(context).size.width * 0.495, MediaQuery.of(context).size.height * 0.92);
positionAlly2 = Offset(MediaQuery.of(context).size.width * 0.495, MediaQuery.of(context).size.height * 0.51);
positionAlly3 = Offset(MediaQuery.of(context).size.width * 0.40, MediaQuery.of(context).size.height * 0.60);
positionAlly4 = Offset(MediaQuery.of(context).size.width * 0.495, MediaQuery.of(context).size.height * 0.65);
positionAlly5 = Offset(MediaQuery.of(context).size.width * 0.60, MediaQuery.of(context).size.height * 0.60);
positionEnemy1 = Offset(MediaQuery.of(context).size.width * 0.495, MediaQuery.of(context).size.width * 0.01);
positionEnemy2 = Offset(MediaQuery.of(context).size.width * 0.40, MediaQuery.of(context).size.height * 0.25);
positionEnemy3 = Offset(MediaQuery.of(context).size.width * 0.40, MediaQuery.of(context).size.height * 0.45);
positionEnemy4 = Offset(MediaQuery.of(context).size.width * 0.60, MediaQuery.of(context).size.height * 0.25);
positionEnemy5 = Offset(MediaQuery.of(context).size.width * 0.60, MediaQuery.of(context).size.height * 0.45);
positionBall = Offset(MediaQuery.of(context).size.width * 0.49, MediaQuery.of(context).size.height * 0.47);
}
startPoints(BuildContext context) {
points = [];
}
}
class ScratchPainter extends CustomPainter {
final List<Offset> points;
ScratchPainter(this.points);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white
..strokeWidth = 5.0
..strokeCap = StrokeCap.round;
for (int i = 0; i < points.length - 1; i++) {
if (points[i] != Offset.zero && points[i + 1] != Offset.zero) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}
@override
bool shouldRepaint(ScratchPainter oldDelegate) => true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment