flutter_custom_roulette

A highly customizable Flutter roulette wheel with weight-based probability, SVG/Image/Icon support, and flexible theming.

demo

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 buttonRouletteButton with 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.pointerRoulettePointer 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)