Ever wondered how those addictive Spin-and-Win games that keep users hooked are made? Imagine integrating such a dynamic and exciting feature into your app, giving your users the thrill of spinning and winning, all in a few taps! Well, guess what? With Flutter, creating a vibrant, fully functional spin wheel is easier than you think.
In today’s competitive app world, user engagement is everything. Adding interactive elements like a Spin-and-Win feature can not only elevate the user experience but also keep them coming back for more. Whether it’s for a rewards system or just a fun game mechanic, this tutorial will show you how to build it from scratch using Flutter!
In this step-by-step guide, we’ll walk you through everything you need to know to create a stunning and functional spin wheel. From designing the sectors to adding spinning logic, you’ll have a working wheel in no time!
“Ready to add some interactive magic to your Flutter app? Let’s dive in and spin the wheel!”
Step 1: Setting Up the Project
Before we dive into the code, ensure that you have set up your Flutter project and added the necessary dependencies. In this example, we are using google_fonts for styling text. You can add it to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
google_fonts: ^2.1.0
Step 2: First, you create a new project and open your main.dart file and paste this code and create a new dart file name SpinTesting with StatefulWidget.
main.dart
import 'package:flutter/material.dart';
import 'SpinWin/spin.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Spin & Win',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SpinTesting(),
);
}
}
Step 3: Animating the Wheel
We begin by creating a SpinTesting
widget. This widget uses AnimationController
and Tween
to rotate the wheel.
class SpinTesting extends StatefulWidget {
const SpinTesting({super.key});
@override
State<SpinTesting> createState() => _SpinTestingState();
}
class _SpinTestingState extends State<SpinTesting> with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> _animation;
@override
void initState() {
_initializeAnimation();
super.initState();
}
Stateful Widget: We need a StatefulWidget because the wheel will animate, and the state will change during rotation.
TickerProviderStateMixin: Required to provide vsync for smooth animations.
AnimationController: This controls the duration and speed of the wheel’s spin.
Tween Animation: Defines the range of rotation. The wheel rotates for 5 full rotations
in the initial setup.
The animation is initialized in the <strong><a href="https://api.flutter.dev/flutter/widgets/State/initState.html">initState</a></strong>
method. Here, the AnimationController
runs for 10 seconds, with the wheel spinning for 5 full rotations:
void _initializeAnimation() {
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 10), // Duration of the spin
);
_animation = Tween<double>(begin: 0.0, end: 2 * pi * 5) // 5 full rotations
.animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
controller.forward(); // Start the animation
}
Step 4: Re-spinning the Wheel
We want users to be able to “Play Again” by pressing a button. When the button is clicked, the animation resets, and the wheel spins for 8 full rotations.
ElevatedButton(
onPressed: () {
controller.reset(); // Reset the animation
controller.duration = Duration(seconds: 25); // New spin duration
_animation = Tween<double>(begin: 0.0, end: 2 * pi * 8) // 8 full rotations
.animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
controller.forward(); // Start the re-spin
},
child: Text('Play Now'),
)
This is where we set a new duration and adjust the number of spins.
Step 5: Custom Drawing the Wheel
We use <strong><a href="https://api.flutter.dev/flutter/rendering/CustomPainter-class.html">CustomPainter</a></strong>
to draw the wheel. This gives us control over how the segments of the wheel are rendered.
class SpinAndWinPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); // Center of the circle
final outerRadius = size.width / 2; // Outer radius of the wheel
final innerRadius = outerRadius - 130; // Inner circle radius
Canvas and Paint: These are Flutter’s lower-level drawing tools. We specify the center
and radius
of the wheel and divide it into segments.
Segment Drawing: We use the drawArc
method to draw each segment, alternating colors (red and black):
final segmentCount = 40; // Total segments
final sweepAngle = 2 * pi / segmentCount; // Angle for each segment
for (int i = 0; i < segmentCount; i++) {
final startAngle = i * sweepAngle;
final paint = Paint()..color = segmentColors[i % segmentColors.length];
canvas.drawArc(
Rect.fromCircle(center: center, radius: outerRadius),
startAngle,
sweepAngle,
true,
paint,
);
}
Here, drawArc
draws each segment with a sweep angle calculated by dividing the full circle by the number of segments.
Step 6: Adding Numbers to the Wheel
Each segment of the wheel is numbered. To position the numbers in the middle of each segment, we use trigonometry:
for (int i = 0; i < numbers.length; i++) {
final angle = (i + 0.5) * sweepAngle; // Position in the center of each segment
final x = center.dx + (innerRadius + 90) * cos(angle); // X position
final y = center.dy + (innerRadius + 90) * sin(angle); // Y position
The numbers are drawn using a TextPainter
, allowing us to align and paint the text on the canvas.
Step 7: Rendering the Wheel
We then use an AnimatedBuilder
to render the wheel on the screen, with its rotation handled by the _animation
object.
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final double angle = _animation.value; // Get current angle
return Transform.rotate(
angle: angle, // Rotate based on animation
child: CustomPaint(
size: Size(500, 500), // Size of the wheel
painter: SpinAndWinPainter(),
),
);
},
)
Full code
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../AppColors/project_color.dart';
class SpinTesting extends StatefulWidget {
const SpinTesting({super.key});
@override
State<SpinTesting> createState() => _SpinTestingState();
}
class _SpinTestingState extends State<SpinTesting> with TickerProviderStateMixin {
late AnimationController controller;
late Animation<double> _animation;
@override
void initState() {
_initializeAnimation(); // Initialize animation when the widget is created
super.initState();
}
void _initializeAnimation() {
controller = AnimationController(
vsync: this,
duration: Duration(seconds: 10), // Initial duration of 10 seconds
);
// Tween for rotation (spins the wheel multiple times)
_animation = Tween<double>(begin: 0.0, end: 2 * pi * 5) // 5 full rotations
.animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
controller.forward(); // Start the animation
}
@override
void dispose() {
controller.dispose(); // Dispose the controller when the widget is destroyed
super.dispose();
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
bottomSheet: ElevatedButton(
onPressed: () {
controller.reset(); // Reset the animation
controller.duration = Duration(seconds: 25); // Set new duration for the spin
_animation = Tween<double>(begin: 0.0, end: 2 * pi * 8) // 8 full rotations for re-spin
.animate(CurvedAnimation(parent: controller, curve: Curves.easeOut));
controller.forward(); // Start the re-spin animation
},
child: Text(
'Play Now',
style: GoogleFonts.lato(fontSize: 16, fontWeight: FontWeight.bold, color: ProjectColors.textColor),
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.all(10),
backgroundColor: Color(0xFFffe801),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
),
body: Container(
child: Stack(
children: <Widget>[
Positioned(
child: Center(
child: Align(
alignment: Alignment.topCenter,
child: SizedBox(
height: 570,
width: 1000,
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
children: [
Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final double angle = _animation.value; // Get the current angle from animation
return Transform.rotate(
angle: angle,
child: CustomPaint(
size: Size(500, 500), // Size of the wheel
painter: SpinAndWinPainter(),
),
);
},
),
),
CircleAvatar(
radius: 80,
backgroundImage: AssetImage('assets/images/SpinWin/11111.png'),
),
],
),
),
),
),
),
Positioned(
top: 30,
child: SizedBox(
height: 80,
width: MediaQuery.of(context).size.width,
child: ClipRect(
child: Align(
alignment: Alignment.topCenter,
heightFactor: 0.5, // Display half the height of the image
child: Container(
alignment: Alignment.center,
child: SizedBox(
width: 90, // Larger width for the image to be scaled bigger
height: 100, // Larger height for the image to be scaled bigger
child: Image.asset(
'assets/images/SpinWin/arrowtesting.png',
fit: BoxFit.contain,
),
),
),
),
),
),
),
],
),
),
),
);
}
}
class SpinAndWinPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2); // Center of the circle
final outerRadius = size.width / 2; // Outer radius of the wheel
final innerRadius = outerRadius - 130; // Inner circle radius
final borderRadius = innerRadius + 5;
// Colors for the segments (e.g., red, black)
List<Color> segmentColors = [
Colors.red, Colors.black,
];
// Draw a gold border around the outer circle
final goldBorderPaint = Paint()
..color = Colors.amber
..style = PaintingStyle.stroke
..strokeWidth = 3; // Adjust this for border thickness
// Draw the gold border
canvas.drawCircle(center, outerRadius, goldBorderPaint);
// Paint the outer circle divided into segments
final segmentCount = 40; // Number of segments
final sweepAngle = 2 * pi / segmentCount; // Angle for each segment
for (int i = 0; i < segmentCount; i++) {
final startAngle = i * sweepAngle;
final paint = Paint()..color = segmentColors[i % segmentColors.length];
// Draw each segment as an arc
canvas.drawArc(
Rect.fromCircle(center: center, radius: outerRadius),
startAngle,
sweepAngle,
true,
paint,
);
}
// Paint the inner circle at the center
final innerPaint = Paint()
..color = Colors.amber
..style = PaintingStyle.fill;
final innerPaint1 = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
canvas.drawCircle(center, borderRadius, innerPaint);
canvas.drawCircle(center, innerRadius, innerPaint1);
// Draw dividing lines
final linePaint = Paint()
..color = Colors.white
..strokeWidth = 1;
for (int i = 0; i < segmentCount; i++) {
final angle = i * sweepAngle;
final startX = center.dx + outerRadius * cos(angle);
final startY = center.dy + outerRadius * sin(angle);
final endX = center.dx + innerRadius * cos(angle);
final endY = center.dy + innerRadius * sin(angle);
canvas.drawLine(Offset(startX, startY), Offset(endX, endY), linePaint);
}
// Create a list of numbers for each segment
List<int> numbers = List.generate(segmentCount, (index) => index + 1);
final textPainter = TextPainter(
textAlign: TextAlign.center,
textDirection: ui.TextDirection.ltr,
);
// Draw the numbers inside the segments...
for (int i = 0; i < numbers.length; i++) {
final angle = (i + 0.5) * sweepAngle; // Position number in the middle of the segment
final x = center.dx + (innerRadius + 90) * cos(angle); // Adjust the position
final y = center.dy + (innerRadius + 90) * sin(angle);
textPainter.text = TextSpan(
text: '${numbers[i]}',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
);
textPainter.layout();
textPainter.paint(canvas, Offset(x - textPainter.width / 2, y - textPainter.height / 2));
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false; // No need to repaint for static content
}
}
Output
Final Words
In this tutorial, we’ve walked through how to create a spin-and-win game wheel using Flutter’s animation and custom painting capabilities. With this setup, you can build interactive games that allow users to engage with a fun spinning wheel!
❤️❤️ Thanks for reading this article ❤️❤️
If I got something wrong? Let me know in the comments. I would love to improve 🥰🥰🥰.
Clap 👏👏👏 If this article helps you,
if you like our work, please follow us on this Dosomthings
Our more attractive articles:
Refresh in Flutter: How to implement Pull to Refresh in Flutter?
Date & Time in Flutter: How to Format Date and Time in Flutter
Image save in Flutter: How to download and save image to file in Flutter