fortune_wheel 0.2.1
fortune_wheel: ^0.2.1 copied to clipboard
A highly customizable spinning fortune wheel widget for Flutter with advanced animations and backend integration support.
Fortune Wheel #
A highly customizable spinning fortune wheel widget for Flutter with advanced animations and backend integration support. Perfect for gamification, prize wheels, decision makers, and any application requiring an interactive spinning wheel.
Features #
- Backend Integration: Spin to results determined by your server
- Advanced Animations:
- Spin to specific index with multiple rotations
- Continuous rotation mode
- Smooth deceleration with customizable curves
- Maintain rotation direction for smoother animations
- Rich Customization:
- Multiple content types per slice (text, images, lines)
- Solid colors, gradients, or image backgrounds
- Configurable slice borders and colors
- Curved text support (text follows circular path)
- Straight and vertical text orientations
- Enhanced Visual Features (New in 0.2.0):
- Border dots: Decorative dots around wheel border
- Center indicator: Circular indicator in wheel center
- Custom arrow pins: ArrowPin and TriangleArrowPin widgets with customizable direction
- Interactive Features:
- Tap to select slices
- Pin/pointer indicator with multiple positions
- Collision detection with callbacks
- Custom arrow pins that remain fixed during spins
- Production Ready:
- No external dependencies (except curved_text)
- Optimized CustomPainter rendering
- Comprehensive example app with enhanced demos
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
fortune_wheel: ^0.2.0
Then run:
flutter pub get
Quick Start #
Basic Usage #
import 'package:fortune_wheel/fortune_wheel.dart';
FortuneWheel(
slices: [
Slice.text('Prize 1', backgroundColor: Colors.red),
Slice.text('Prize 2', backgroundColor: Colors.blue),
Slice.text('Prize 3', backgroundColor: Colors.green),
Slice.text('Prize 4', backgroundColor: Colors.orange),
],
pinConfiguration: PinConfiguration.icon(
icon: Icons.arrow_drop_down,
color: Colors.red,
),
)
Backend Integration (Sequential) #
Perfect for when you need to fetch the result first, then spin:
final GlobalKey<FortuneWheelState> wheelKey = GlobalKey();
// In your spin handler:
Future<void> handleSpin() async {
// Step 1: Call your backend API
final response = await http.post('https://your-api.com/spin');
final result = jsonDecode(response.body);
final winningIndex = result['winningIndex'];
// Step 2: Spin to the backend result
await wheelKey.currentState!.spinToBackendResult(
winningIndex,
fullRotations: 5,
duration: Duration(seconds: 4),
);
// Step 3: Show result
showWinDialog(winningIndex);
}
// Your widget:
FortuneWheel(
key: wheelKey,
slices: mySlices,
// ... other config
)
Backend Integration (Async) #
Start spinning immediately while fetching the result in background:
Future<void> handleAsyncSpin() async {
// Start spinning immediately
wheelKey.currentState!.startContinuousRotation(rotationsPerSecond: 2);
// Fetch result while spinning
final winningIndex = await fetchResultFromBackend();
// Stop at the result
await wheelKey.currentState!.stopContinuousRotation(
landOnIndex: winningIndex,
);
}
Configuration #
Wheel Configuration #
FortuneWheel(
slices: mySlices,
configuration: WheelConfiguration(
circlePreferences: CirclePreferences(
strokeWidth: 4,
strokeColor: Colors.black,
),
slicePreferences: SlicePreferences(
strokeWidth: 2,
strokeColor: Colors.white,
backgroundColors: SliceBackgroundColors.evenOdd(
evenColor: Colors.blue,
oddColor: Colors.red,
),
),
startPosition: WheelStartPosition.top,
layerInsets: EdgeInsets.all(15),
contentMargins: EdgeInsets.all(15),
),
)
Pin Configuration #
// Icon pin
PinConfiguration.icon(
icon: Icons.arrow_drop_down,
size: Size(50, 50),
color: Colors.red,
position: PinPosition.top,
)
// Image pin
PinConfiguration.image(
image: AssetImage('assets/pin.png'),
size: Size(40, 40),
position: PinPosition.top,
)
// Custom widget pin
PinConfiguration.custom(
widget: YourCustomWidget(),
size: Size(50, 50),
)
Slice Content #
// Simple text
Slice.text('Prize', backgroundColor: Colors.red)
// Text with image
Slice.textWithImage(
'Prize',
AssetImage('assets/icon.png'),
imageSize: Size(40, 40),
)
// Custom content
Slice(
contents: [
ImageContent(
image: AssetImage('assets/icon.png'),
preferredSize: Size(50, 50),
),
TextContent(
text: 'Grand Prize',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
isCurved: true, // Text follows circular arc
),
LineContent(
color: Colors.white,
width: 2,
),
],
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
),
)
Animation Methods #
Spin to Index #
// Spin with multiple rotations to specific slice
await wheelKey.currentState!.spinToIndex(
targetIndex,
fullRotations: 5,
duration: Duration(seconds: 4),
);
// Quick rotation without full spins
wheelKey.currentState!.rotateToIndex(
targetIndex,
duration: Duration(milliseconds: 500),
);
Continuous Rotation #
// Start continuous spinning
wheelKey.currentState!.startContinuousRotation(
rotationsPerSecond: 2.0,
);
// Stop continuous spinning
wheelKey.currentState!.stopContinuousRotation(
landOnIndex: 3, // Optional: land on specific slice
);
// Stop immediately
wheelKey.currentState!.stop();
Callbacks #
FortuneWheel(
slices: mySlices,
onSliceTap: (index) {
print('Tapped slice $index');
},
onEdgeCollision: (progress) {
// Called when slice edge passes pin
// progress: 0.0 to 1.0 during deceleration, null during continuous
HapticFeedback.lightImpact();
},
onCenterCollision: (progress) {
// Called when slice center passes pin
playSound();
},
edgeCollisionDetection: true,
centerCollisionDetection: true,
)
Advanced Features #
Enhanced Visual Features (New in 0.2.0) #
Border Dots
Add decorative dots around the wheel border:
FortuneWheel(
slices: mySlices,
configuration: WheelConfiguration(
circlePreferences: CirclePreferences(
strokeWidth: 8,
strokeColor: Colors.black,
borderDots: BorderDotsConfiguration(
dotSize: 8,
dotColor: Colors.orange,
dotBorderColor: Colors.black,
dotBorderWidth: 2,
dotsPerSlice: 1, // One dot per slice boundary
),
),
),
)
Center Indicator
Add a circular indicator in the wheel center:
FortuneWheel(
slices: mySlices,
configuration: WheelConfiguration(
circlePreferences: CirclePreferences(
centerIndicator: CenterIndicatorConfiguration(
radius: 35,
color: Colors.white,
borderColor: Colors.black,
borderWidth: 4,
),
),
),
)
Custom Arrow Pins
Use custom arrow widgets that stay fixed while the wheel spins:
// Classic arrow pin with customizable direction
PinConfiguration.custom(
widget: ArrowPin(
color: Colors.red,
size: Size(40, 50),
borderColor: Colors.black,
borderWidth: 2.0,
withShadow: true,
direction: math.pi / 2, // Points downward (0 = right, pi/2 = down, pi = left, 3*pi/2 = up)
),
size: Size(40, 50),
position: PinPosition.top,
verticalOffset: 10, // Distance from wheel edge
)
// Triangle arrow pin - simpler design
PinConfiguration.custom(
widget: TriangleArrowPin(
color: Colors.blue,
size: Size(30, 40),
direction: math.pi / 2,
),
size: Size(30, 40),
position: PinPosition.top,
)
Curved Text #
Text can follow the circular arc of the wheel:
TextContent(
text: 'Your Prize Name',
isCurved: true,
orientation: TextOrientation.horizontal,
style: TextStyle(fontSize: 18),
)
Gradients #
Slice(
contents: [TextContent(text: 'Prize')],
gradient: LinearGradient(
colors: [Colors.purple, Colors.blue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
)
Multiple Content Items #
Stack multiple content items in a slice:
Slice(
contents: [
ImageContent(
image: AssetImage('assets/icon.png'),
preferredSize: Size(40, 40),
),
TextContent(text: 'Prize Name'),
TextContent(
text: 'Value: \$100',
style: TextStyle(fontSize: 12),
),
],
)
Complete Enhanced Wheel Example #
Here's a complete example using all the new visual features:
import 'package:fortune_wheel/fortune_wheel.dart';
import 'dart:math' as math;
class EnhancedWheelExample extends StatelessWidget {
final GlobalKey<FortuneWheelState> wheelKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Enhanced Fortune Wheel')),
body: Center(
child: FortuneWheel(
key: wheelKey,
slices: [
Slice.text('Prize 1', backgroundColor: Color(0xFFE74C3C)),
Slice.text('Prize 2', backgroundColor: Color(0xFF3498DB)),
Slice.text('Prize 3', backgroundColor: Color(0xFF2ECC71)),
Slice.text('Prize 4', backgroundColor: Color(0xFFF39C12)),
Slice.text('Prize 5', backgroundColor: Color(0xFF9B59B6)),
Slice.text('Prize 6', backgroundColor: Color(0xFF1ABC9C)),
],
configuration: WheelConfiguration(
circlePreferences: CirclePreferences(
strokeWidth: 8,
strokeColor: Color(0xFF34495E),
// Decorative border dots
borderDots: BorderDotsConfiguration(
dotSize: 8,
dotColor: Color(0xFFFFA500),
dotBorderColor: Color(0xFF34495E),
dotBorderWidth: 2,
dotsPerSlice: 1,
),
// Center indicator circle
centerIndicator: CenterIndicatorConfiguration(
radius: 35,
color: Colors.white,
borderColor: Color(0xFF34495E),
borderWidth: 4,
),
),
slicePreferences: SlicePreferences(
strokeWidth: 2,
strokeColor: Color(0xFF2C3E50),
),
startPosition: WheelStartPosition.top,
layerInsets: EdgeInsets.all(20),
contentMargins: EdgeInsets.all(20),
),
// Custom arrow pin that stays fixed
pinConfiguration: PinConfiguration.custom(
widget: ArrowPin(
color: Color(0xFFE74C3C),
size: Size(40, 50),
borderColor: Color(0xFF34495E),
borderWidth: 2.5,
withShadow: true,
direction: math.pi / 2, // Points down
),
size: Size(40, 50),
position: PinPosition.top,
verticalOffset: 10,
),
edgeCollisionDetection: true,
onEdgeCollision: (progress) {
// Add haptic feedback or sound
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// Spin to random slice
final random = math.Random();
final targetIndex = random.nextInt(6);
await wheelKey.currentState!.spinToBackendResult(
targetIndex,
fullRotations: 5,
duration: Duration(seconds: 4),
);
},
child: Icon(Icons.play_arrow),
),
);
}
}
Real-World Backend Integration Example #
Here's a complete example with error handling:
Future<void> spinWithBackend() async {
try {
setState(() => isSpinning = true);
// Call your backend
final response = await http.post(
Uri.parse('https://api.example.com/spin'),
headers: {'Authorization': 'Bearer $token'},
body: jsonEncode({'userId': currentUserId}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final winningIndex = data['winningIndex'];
// Spin to result
await wheelKey.currentState!.spinToBackendResult(
winningIndex,
fullRotations: 5,
duration: Duration(seconds: 4),
);
// Update UI with result
setState(() {
currentPrize = prizes[winningIndex];
isSpinning = false;
});
showPrizeDialog();
} else {
throw Exception('Failed to fetch result');
}
} catch (e) {
setState(() => isSpinning = false);
showErrorDialog(e.toString());
}
}
Example App #
Run the example app to see all features in action:
cd example
flutter run
The example demonstrates:
- Enhanced Wheel Demo (New in 0.2.0):
- Border dots decoration
- Center indicator circle
- Custom arrow pin that stays fixed during spins
- Curved text mode for better readability
- Sequential backend integration
- Async backend integration
- Quick test buttons
- Collision detection
- Custom styling
The demo includes a selector to navigate between the Enhanced Wheel and the original Fortune Wheel examples.
Performance Tips #
- Reuse Slices: Create slices once in initState, don't recreate on every build
- Optimize Images: Use appropriately sized images for slice content
- Limit Collision Detection: Only enable when needed (adds overhead)
- Animation Duration: Longer animations (4-5s) feel more realistic
Platform Support #
- iOS
- Android
- Web
- macOS
- Linux
- Windows
Credits #
Inspired by SwiftFortuneWheel for iOS.
License #
MIT License - see LICENSE file for details
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.