Introduction
The scratcher
package in Flutter allows you to create a scratch-off effect, similar to lottery tickets, where users can swipe to reveal hidden content. This guide will show you how to use this package to implement a Scratcher in your Flutter app.
Content
1.Add the scratcher
dependency:
Open your pubspec.yaml
file and add the scratcher
dependency.
dependencies:
scratcher: ^latest_version
Run flutter pub get
to install the package.
2.Import the package:
Import the scratcher
package in your Dart file.
import 'package:scratcher/scratcher.dart';
3.Use the Scratcher
widget:
Wrap the content you want to scratch off with the Scratcher
widget. For example, if you want to reveal an image, you can use an Image
widget as the child of the Scratcher
.
Scratcher(
brushSize: 30, // Size of the brush
threshold: 50, // Amount of scratching needed to reveal the content
color: Colors.grey, // Color of the scratcher surface
onChange: (value) {
// Callback when the user is scratching
},
onThreshold: () {
// Callback when enough scratching has been done to reveal the content
},
child: Image.asset('assets/image_to_reveal.png'),
)
Customize the brushSize
, threshold
, and color
properties according to your design and requirements. The onChange
callback is called while the user is scratching, and the onThreshold
callback is called when the user has scratched enough to reveal the content.
4.Enhance the user experience (optional):
You can enhance the user experience by adding animations, sounds, or feedback when the content is revealed. For example, you can show a congratulatory message or play a sound effect.
5.Test your implementation:
Run your app and test the Scratcher to ensure it behaves as expected. Try different brush sizes and scratching thresholds to find the best configuration for your app.
Sample Code
// ignore_for_file: prefer_const_literals_to_create_immutables, prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:scratcher/scratcher.dart';
// import 'advanced.dart';
// import 'basic.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DefaultTabController(
length: 2,
child: Scaffold(
bottomNavigationBar: SafeArea(
child: TabBar(
labelColor: Colors.blueAccent,
unselectedLabelColor: Colors.blueGrey,
indicatorColor: Colors.blueAccent,
indicatorSize: TabBarIndicatorSize.label,
tabs: [
Tab(icon: Icon(Icons.looks_one)),
Tab(icon: Icon(Icons.looks_two)),
],
),
),
body: TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [
AdvancedScreen(),
BasicScreen(),
],
),
),
),
);
}
}
class BasicScreen extends StatefulWidget {
@override
_BasicScreenState createState() => _BasicScreenState();
}
class _BasicScreenState extends State<BasicScreen> {
double brushSize = 30;
double progress = 0;
bool thresholdReached = false;
bool enabled = true;
double? size;
final key = GlobalKey<ScratcherState>();
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: const Text('Reset'),
onPressed: () {
key.currentState?.reset(
duration: const Duration(milliseconds: 2000),
);
setState(() => thresholdReached = false);
},
),
ElevatedButton(
child: const Text('Change size'),
onPressed: () {
setState(() {
if (size == null) {
size = 200;
} else if (size == 200) {
size = 0;
} else {
size = null;
}
});
},
),
ElevatedButton(
child: const Text('Reveal'),
onPressed: () {
key.currentState?.reveal(
duration: const Duration(milliseconds: 2000),
);
},
),
],
),
Column(
children: [
Text('Brush size (${brushSize.round()})'),
Slider(
value: brushSize,
onChanged: (v) => setState(() => brushSize = v),
min: 5,
max: 100,
),
],
),
CheckboxListTile(
value: enabled,
title: Text('Scratcher enabled'),
onChanged: (e) => setState(() {
enabled = e ?? false;
}),
),
Expanded(
child: Stack(
children: [
SizedBox(
height: size,
width: size,
child: Scratcher(
key: key,
enabled: enabled,
brushSize: brushSize,
threshold: 30,
image: Image.network(
'https://c8.alamy.com/comp/2D6N8NX/gift-card-voucher-certificate-or-coupon-vector-design-template-discount-banner-layout-for-seasonal-holidays-sale-abstract-3d-multicolor-plastic-ge-2D6N8NX.jpg'),
onThreshold: () => setState(() => thresholdReached = true),
onChange: (value) {
setState(() {
progress = value;
});
},
onScratchStart: () {
print("Scratching has started");
},
onScratchUpdate: () {
print("Scratching in progress");
},
onScratchEnd: () {
print("Scratching has finished");
},
child: Container(
color: Colors.black,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Scratch the screen!',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.amber,
),
),
SizedBox(height: 8),
const Text(
'add here your scratch coupon code or photo',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.amber,
),
)
],
),
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 12,
),
color: Colors.black,
child: Text(
'${progress.floor().toString()}% '
'(${thresholdReached ? 'done' : 'pending'})',
textAlign: TextAlign.right,
style: const TextStyle(
color: Colors.white,
),
),
),
),
],
),
),
],
),
);
}
}
const _googleIcon = 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Thank-you-transparent.svg/800px-Thank-you-transparent.svg.png';
const _dartIcon = 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Thank-you-transparent.svg/800px-Thank-you-transparent.svg.png';
const _flutterIcon = 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Thank-you-transparent.svg/800px-Thank-you-transparent.svg.png';
class AdvancedScreen extends StatefulWidget {
@override
_AdvancedScreenState createState() => _AdvancedScreenState();
}
class _AdvancedScreenState extends State<AdvancedScreen> with SingleTickerProviderStateMixin {
double validScratches = 0;
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..addStatusListener(
(listener) {
if (listener == AnimationStatus.completed) {
_animationController.reverse();
}
},
);
_animation = Tween(begin: 1.0, end: 1.25).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.elasticIn,
),
);
super.initState();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Scratcher',
style: TextStyle(
fontFamily: 'The unseen',
color: Colors.blueAccent,
fontSize: 50,
fontWeight: FontWeight.bold,
),
),
const Text(
'scratch to win!',
style: TextStyle(
fontFamily: 'The unseen',
color: Colors.black,
fontSize: 20,
),
),
Container(
margin: const EdgeInsets.only(top: 10),
height: 1,
width: 300,
color: Colors.black12,
)
],
),
buildRow(_googleIcon, _flutterIcon, _googleIcon),
buildRow(_dartIcon, _flutterIcon, _googleIcon),
buildRow(_dartIcon, _flutterIcon, _dartIcon),
],
),
),
);
}
Widget buildRow(String left, String center, String right) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ScratchBox(image: left),
ScratchBox(
image: center,
animation: _animation,
onScratch: () {
setState(() {
validScratches++;
if (validScratches == 3) {
_animationController.forward();
}
});
},
),
ScratchBox(image: right),
],
);
}
}
class ScratchBox extends StatefulWidget {
ScratchBox({
required this.image,
this.onScratch,
this.animation,
});
final String image;
final VoidCallback? onScratch;
final Animation<double>? animation;
@override
_ScratchBoxState createState() => _ScratchBoxState();
}
class _ScratchBoxState extends State<ScratchBox> {
bool isScratched = false;
double opacity = 0.6;
@override
Widget build(BuildContext context) {
var icon = AnimatedOpacity(
opacity: opacity,
duration: const Duration(milliseconds: 750),
child: Image.network(
widget.image,
width: 115,
height: 115,
fit: BoxFit.cover,
),
);
return Container(
width: 110,
height: 110,
margin: const EdgeInsets.symmetric(horizontal: 10),
child: Scratcher(
accuracy: ScratchAccuracy.high,
color: Colors.blueGrey,
image: Image.network('https://play-lh.googleusercontent.com/A1gsZWGEWlKhqKPbnJajuByaOlvpanDEqTBNG3gMveS65YmG5rICZN4poKBfeMtnRTI',
fit: BoxFit.contain),
brushSize: 15,
threshold: 60,
onThreshold: () {
setState(() {
opacity = 1;
isScratched = true;
});
widget.onScratch?.call();
},
child: Container(
child: widget.animation == null
? icon
: ScaleTransition(
scale: widget.animation!,
child: icon,
),
),
),
);
}
}
Output
Conclusion
Implementing a Scratcher in your Flutter app using the scratcher
package can add an interactive and engaging element for users. By following this guide, you can easily create a scratch-off effect that allows users to reveal hidden content with a swipe gesture.