flutter_scratch_card 2.0.0
flutter_scratch_card: ^2.0.0 copied to clipboard
A production-ready, highly customizable Flutter scratch card SDK with ImageProvider overlays, themes, brushes, haptics, multi-area scratching, persistence, and accessibility.
import 'package:flutter/material.dart';
import 'package:flutter_scratch_card/flutter_scratch_card.dart';
void main() => runApp(const ScratchifyDemoApp());
class ScratchifyDemoApp extends StatelessWidget {
const ScratchifyDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'flutter_scratch_card',
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF0F766E),
),
home: const DemoHome(),
);
}
}
class DemoHome extends StatelessWidget {
const DemoHome({super.key});
@override
Widget build(BuildContext context) {
final demos = <_DemoItem>[
_DemoItem('Basic Scratch Card', (_) => const BasicDemo()),
_DemoItem('Network Overlay', (_) => const NetworkOverlayDemo()),
_DemoItem('Custom Brush', (_) => const CustomBrushDemo()),
_DemoItem('Custom Shape', (_) => const CustomShapeDemo()),
_DemoItem('Themes', (_) => const ThemesDemo()),
_DemoItem('Haptics', (_) => const HapticsDemo()),
_DemoItem('Sound (adapter)', (_) => const SoundDemo()),
_DemoItem('Completion Effects', (_) => const CompletionEffectsDemo()),
_DemoItem('Controller', (_) => const ControllerDemo()),
_DemoItem('Multiple Scratch Areas', (_) => const MultiAreaDemo()),
_DemoItem('Multiple Rewards', (_) => const MultiRewardDemo()),
_DemoItem('Async Reward', (_) => const AsyncRewardDemo()),
_DemoItem('Persistence', (_) => const PersistenceDemo()),
_DemoItem('Accessibility', (_) => const AccessibilityDemo()),
_DemoItem('Performance Demo', (_) => const PerformanceDemo()),
];
return Scaffold(
appBar: AppBar(title: const Text('Scratchify examples')),
body: ListView.separated(
itemCount: demos.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = demos[index];
return ListTile(
title: Text(item.title),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: item.builder,
settings: RouteSettings(name: item.title),
),
),
);
},
),
);
}
}
class _DemoItem {
const _DemoItem(this.title, this.builder);
final String title;
final WidgetBuilder builder;
}
Widget _reward(String text) {
return ColoredBox(
color: const Color(0xFFFFF7ED),
child: Center(
child: Text(
text,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.w800,
color: Color(0xFF9A3412),
),
),
),
);
}
class _CardFrame extends StatelessWidget {
const _CardFrame({required this.child, this.footer});
final Widget child;
final Widget? footer;
@override
Widget build(BuildContext context) {
final title = ModalRoute.of(context)?.settings.name ?? 'Demo';
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 280,
height: 280,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Color(0x33000000),
blurRadius: 18,
offset: Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: child,
),
),
),
if (footer != null) ...[const SizedBox(height: 24), footer!],
],
),
),
),
);
}
}
class BasicDemo extends StatelessWidget {
const BasicDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
overlayImage: const AssetImage('assets/scratch_texture.png'),
threshold: 0.55,
onComplete: () {},
child: _reward('₹100'),
),
);
}
}
class NetworkOverlayDemo extends StatelessWidget {
const NetworkOverlayDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
overlayImage: const NetworkImage(
'https://picsum.photos/seed/scratch/400/400',
),
fallbackOverlayImage: const AssetImage('assets/scratch_texture.png'),
scratchColor: Colors.blueGrey,
child: _reward('NET'),
),
);
}
}
class CustomBrushDemo extends StatelessWidget {
const CustomBrushDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
brush: ScratchBrush.soft(size: 42),
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('SOFT'),
),
);
}
}
class CustomShapeDemo extends StatelessWidget {
const CustomShapeDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
revealShape: ScratchRevealShape.circle(),
brush: ScratchBrush.circle(size: 36),
scratchColor: const Color(0xFF64748B),
child: _reward('◯'),
),
);
}
}
class ThemesDemo extends StatefulWidget {
const ThemesDemo({super.key});
@override
State<ThemesDemo> createState() => _ThemesDemoState();
}
class _ThemesDemoState extends State<ThemesDemo> {
var _index = 0;
final _themes = [
ScratchCardTheme.classic(),
ScratchCardTheme.metallic(),
ScratchCardTheme.golden(),
ScratchCardTheme.premium(),
ScratchCardTheme.neon(),
ScratchCardTheme.minimal(),
];
final _names = [
'classic',
'metallic',
'golden',
'premium',
'neon',
'minimal',
];
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: Wrap(
spacing: 8,
children: [
for (var i = 0; i < _names.length; i++)
ChoiceChip(
label: Text(_names[i]),
selected: _index == i,
onSelected: (_) => setState(() => _index = i),
),
],
),
child: ScratchCard(
key: ValueKey(_index),
theme: _themes[_index],
child: _reward(_names[_index].toUpperCase()),
),
);
}
}
class HapticsDemo extends StatelessWidget {
const HapticsDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
haptic: const ScratchHapticConfig(
enabled: true,
onScratch: HapticType.selection,
onMilestone: HapticType.medium,
onComplete: HapticType.heavy,
),
progressTriggers: const [0.3, 0.6],
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('VIB'),
),
);
}
}
class SoundDemo extends StatelessWidget {
const SoundDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
'Sound uses ScratchSoundPlayer — plug in audioplayers/just_audio in your app. This demo uses a no-op player.',
textAlign: TextAlign.center,
),
),
child: ScratchCard(
sound: const ScratchSoundConfig(
enabled: true,
player: NoOpScratchSoundPlayer(),
),
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('SND'),
),
);
}
}
class CompletionEffectsDemo extends StatefulWidget {
const CompletionEffectsDemo({super.key});
@override
State<CompletionEffectsDemo> createState() => _CompletionEffectsDemoState();
}
class _CompletionEffectsDemoState extends State<CompletionEffectsDemo> {
final _controller = ScratchController();
var _effect = ScratchCompletionEffect.confetti;
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: Column(
children: [
DropdownButton<ScratchCompletionEffect>(
value: _effect,
items: [
for (final e in ScratchCompletionEffect.values)
DropdownMenuItem(value: e, child: Text(e.name)),
],
onChanged: (v) {
if (v == null) return;
setState(() => _effect = v);
_controller.reset();
},
),
ElevatedButton(
onPressed: _controller.reveal,
child: const Text('Reveal'),
),
],
),
child: ScratchCard(
key: ValueKey(_effect),
controller: _controller,
completion: ScratchCompletionConfig(effect: _effect),
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('FX'),
),
);
}
}
class ControllerDemo extends StatefulWidget {
const ControllerDemo({super.key});
@override
State<ControllerDemo> createState() => _ControllerDemoState();
}
class _ControllerDemoState extends State<ControllerDemo> {
final _controller = ScratchController();
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: _controller.reveal,
child: const Text('Reveal'),
),
ElevatedButton(
onPressed: () => _controller.reset(animated: true),
child: const Text('Reset'),
),
ElevatedButton(
onPressed: _controller.pause,
child: const Text('Pause'),
),
ElevatedButton(
onPressed: _controller.resume,
child: const Text('Resume'),
),
],
),
child: ScratchCard(
controller: _controller,
animationType: ScratchAnimationType.lottie,
animationAsset: 'assets/Confetti.json',
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: Image.asset('assets/reveal.jpg', fit: BoxFit.cover),
),
);
}
}
class MultiAreaDemo extends StatelessWidget {
const MultiAreaDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
areas: [
ScratchArea(
id: 'a',
flex: 1,
child: _reward('A'),
brush: ScratchBrush.hard(size: 28),
),
ScratchArea(
id: 'b',
flex: 1,
child: _reward('B'),
brush: ScratchBrush.soft(size: 34),
),
],
onAreaComplete: (id) => debugPrint('Area complete: $id'),
scratchColor: const Color(0xFF94A3B8),
child: const SizedBox.shrink(),
),
);
}
}
class MultiRewardDemo extends StatelessWidget {
const MultiRewardDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
rewardSelection: ScratchRewardSelection.random,
rewards: [
ScratchReward(id: '50', child: _reward('₹50'), weight: 3),
ScratchReward(id: '100', child: _reward('₹100'), weight: 2),
ScratchReward(id: '500', child: _reward('₹500'), weight: 1),
],
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: const SizedBox.shrink(),
),
);
}
}
class AsyncRewardDemo extends StatelessWidget {
const AsyncRewardDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
rewardSelection: ScratchRewardSelection.async,
rewardBuilder: (context) async {
await Future<void>.delayed(const Duration(milliseconds: 800));
return _reward('ASYNC');
},
rewardLoadingBuilder: (_) =>
const Center(child: CircularProgressIndicator()),
rewardErrorBuilder: (context, error, retry) => Center(
child: TextButton(onPressed: retry, child: const Text('Retry')),
),
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: const SizedBox.shrink(),
),
);
}
}
class PersistenceDemo extends StatefulWidget {
const PersistenceDemo({super.key});
@override
State<PersistenceDemo> createState() => _PersistenceDemoState();
}
class _PersistenceDemoState extends State<PersistenceDemo> {
final _storage = MemoryScratchStateStorage();
final _controller = ScratchController();
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: ElevatedButton(
onPressed: () => _controller.reset(),
child: const Text('Reset + clear storage'),
),
child: ScratchCard(
controller: _controller,
stateKey: 'demo_persist',
stateStorage: _storage,
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('SAVE'),
),
);
}
}
class AccessibilityDemo extends StatelessWidget {
const AccessibilityDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
footer: const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
'TalkBack / VoiceOver can activate the reveal semantic action.',
textAlign: TextAlign.center,
),
),
child: ScratchCard(
semanticLabel: 'Scratch card containing a reward',
semanticHint: 'Scratch to reveal your reward, or double-tap to reveal',
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('A11Y'),
),
);
}
}
class PerformanceDemo extends StatelessWidget {
const PerformanceDemo({super.key});
@override
Widget build(BuildContext context) {
return _CardFrame(
child: ScratchCard(
performance: ScratchPerformanceConfig.lowEnd(),
brush: const ScratchBrush(size: 48, spacing: 0.35),
overlayImage: const AssetImage('assets/scratch_texture.png'),
child: _reward('PERF'),
),
);
}
}