flutter_custom_roulette 0.0.4
flutter_custom_roulette: ^0.0.4 copied to clipboard
A highly customizable Flutter roulette wheel with weight-based probability, SVG support, and flexible theming.
flutter_custom_roulette #
A highly customizable Flutter roulette wheel with weight-based probability, SVG/Image/Icon support, and flexible theming.

Features #
- Equal sections with weighted probability — All sections are the same size, but winning chances are controlled by
weight - Weight-based section sizing — Optionally make section widths proportional to their weight with
weightedSections: true - Any widget as section content — Drop in SVG, PNG, Icon, or any Flutter widget
- Content rotation control — Choose whether icons/text rotate with the wheel or stay upright from the user's perspective
- Customizable pointer — Set position, size, color, or gradient. Place it inside or outside the wheel border
- Built-in GO button —
RouletteButtonwith label, color, font, size, and spin-lock support - Border theming — Color, width, and gradient on the wheel border
- Per-item text offset — Fine-tune label position for each section individually
Getting Started #
dependencies:
flutter_custom_roulette: ^0.0.1
To use SVG assets inside sections, add flutter_svg separately:
dependencies:
flutter_svg: ^2.0.0
Usage #
Basic example #
import 'package:flutter_custom_roulette/flutter_custom_roulette.dart';
final _controller = RouletteController();
final _items = const [
RouletteItem(label: 'Miss', weight: 26, backgroundColor: Colors.grey),
RouletteItem(label: '1st', weight: 5, backgroundColor: Colors.red),
RouletteItem(label: '2nd', weight: 15, backgroundColor: Colors.blue),
];
@override
void initState() {
super.initState();
_controller.addListener(() {
if (_controller.result != null) {
print('Result: ${_controller.result!.item.label}');
}
});
}
@override
Widget build(BuildContext context) {
return RouletteWheel(
items: _items,
controller: _controller,
pointer: const RoulettePointer(width: 30, height: 40, color: Colors.red),
pointerOffset: 20,
centerWidget: RouletteButton(
controller: _controller,
items: _items,
),
);
}
With SVG content #
import 'package:flutter_svg/flutter_svg.dart';
RouletteItem(
label: 'Brick x3',
weight: 15,
backgroundColor: Color(0xFFFFE0B2),
labelColor: Colors.orange,
rotateWithWheel: false,
content: SvgPicture.asset('assets/brick.svg'),
contentSize: 40,
)
Spin to a specific index #
// Always lands on index 2
_controller.spinToIndex(2, _items);
Gradient border + weighted sections #
RouletteWheel(
items: _items,
controller: _controller,
borderGradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
borderWidth: 6,
weightedSections: true,
)
Custom spin button #
RouletteButton(
controller: _controller,
items: _items,
label: 'SPIN',
size: 80,
gradient: LinearGradient(colors: [Colors.purple, Colors.indigo]),
labelStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
disableWhileSpinning: true,
)
API Reference #
Roulette Item #
| Property | Type | Default | Description |
|---|---|---|---|
label |
String |
required | Text displayed on the section |
weight |
int |
required | Probability weight. Does not need to sum to 100 |
backgroundColor |
Color |
required | Section fill color |
labelColor |
Color |
Colors.black |
Label text color |
labelStyle |
TextStyle? |
null |
Full text style. Overrides labelColor when set |
labelOffset |
Offset |
Offset.zero |
Nudge the label position |
rotateWithWheel |
bool |
true |
true: content rotates with the wheel / false: always upright |
content |
Widget? |
null |
Widget to show inside the section (SVG, Image, Icon, etc.) |
contentSize |
double |
36 |
Width and height of the content widget |
data |
dynamic |
null |
Attach any custom data to retrieve from RouletteResult |
Roulette Wheel #
| Property | Type | Default | Description |
|---|---|---|---|
items |
List<RouletteItem> |
required | Section list (minimum 2) |
controller |
RouletteController |
required | Controls spinning and exposes results |
size |
double |
300 |
Wheel diameter in logical pixels |
spinDuration |
Duration |
4000ms |
Duration of one spin |
spinCurve |
Curve |
Curves.easeOutCubic |
Animation curve |
minSpins |
int |
5 |
Minimum number of full rotations per spin |
maxSpins |
int |
8 |
Maximum number of full rotations per spin |
borderColor |
Color? |
null |
Wheel border color |
borderGradient |
Gradient? |
null |
Wheel border gradient. Overrides borderColor when set |
borderWidth |
double |
4 |
Wheel border stroke width |
alternatingColors |
List<Color>? |
null |
When set, sections alternate through these colors instead of using item.backgroundColor |
showLabels |
bool |
true |
Show or hide all section labels |
itemRadius |
double |
0.65 |
Placement radius ratio for items (0.0 = center, 1.0 = edge) |
centerWidget |
Widget? |
null |
Widget placed at the center of the wheel |
weightedSections |
bool |
false |
true: section width proportional to weight / false: equal width |
pointer |
Widget? |
null |
Pointer widget rendered above the wheel |
pointerOffset |
double |
20 |
How far the pointer tip overlaps into the wheel (0 = flush with border, positive = further in) |
Roulette Controller #
Extends ChangeNotifier. Use addListener to receive spin results.
| Member | Type | Description |
|---|---|---|
isSpinning |
bool |
Whether the wheel is currently spinning |
result |
RouletteResult? |
Last spin result. Reset to null when a new spin starts |
spin(items) |
Future<void> |
Pick a winner by weight and spin |
spinToIndex(index, items) |
Future<void> |
Force a specific index to win and spin |
_controller.addListener(() {
if (_controller.result != null) {
final result = _controller.result!;
print('Index: ${result.index}');
print('Label: ${result.item.label}');
print('Data: ${result.item.data}');
}
});
Roulette Result #
| Property | Type | Description |
|---|---|---|
index |
int |
Index of the winning section |
item |
RouletteItem |
The winning RouletteItem |
Roulette Button #
| Property | Type | Default | Description |
|---|---|---|---|
controller |
RouletteController |
required | Controller to trigger |
items |
List<RouletteItem> |
required | Items to spin |
label |
String |
'GO' |
Button label |
size |
double |
70 |
Button diameter |
backgroundColor |
Color |
Colors.deepPurple |
Background color |
labelColor |
Color |
Colors.white |
Label color |
labelStyle |
TextStyle? |
null |
Full text style. Overrides labelColor when set |
gradient |
Gradient? |
null |
Background gradient. Overrides backgroundColor when set |
disableWhileSpinning |
bool |
true |
Disable and dim the button while the wheel spins |
onPressed |
VoidCallback? |
null |
Custom tap handler. Replaces the default controller.spin() call |
Roulette Pointer #
| Property | Type | Default | Description |
|---|---|---|---|
width |
double |
30 |
Pointer width |
height |
double |
40 |
Pointer height |
color |
Color? |
Colors.red |
Pointer color |
gradient |
Gradient? |
null |
Pointer gradient. Overrides color when set |
Pass any widget to RouletteWheel.pointer — RoulettePointer is just the default implementation.
How It Works #
Spin flow #
Tap SPIN button
→ controller.spin(items)
→ ProbabilityPicker selects winning index before spinning
→ Calls _spinHandler registered by RouletteWheel
→ Calculates target angle so the winning section lands under the pointer
→ Runs AnimationController (default 4 seconds)
→ _currentAngle updates every frame via the spin curve
→ CustomPainter rotates the background
→ Item positions recalculated in real time
→ Animation completes
→ Sets result + calls notifyListeners()
→ Your addListener callback fires with the result
Probability #
The total weight is summed, and a random number is drawn. The winning index is determined before the wheel starts spinning — the animation is just visual confirmation.
weights: [26, 18, 15, 10, 5, 3, 2, 21] → total: 100
roll 0–25 → index 0 (26%)
roll 26–43 → index 1 (18%)
roll 44–58 → index 2 (15%)
...
Angle calculation #
Flutter's coordinate system places 0° at the 3 o'clock position, so a -pi/2 offset is applied throughout to treat 12 o'clock as the origin. The target angle is:
targetAngle = (numSpins × 360°) + (rotation needed to place winning section under pointer) + (random offset within section)
The random offset within the section ensures the wheel never stops at the exact center of a section, making the result feel natural.
rotateWithWheel #
The background (section colors) and the items (text/icons) are rendered separately:
Background CustomPainter → always rotates with the wheel angle
Item widgets → positioned by computing screen coordinates directly from the current angle
rotateWithWheel: true → item content also rotates by the current angle (stamped onto the wheel)
rotateWithWheel: false → item content has no rotation (always upright from user's view)