How to Implement Scratch Card Feature in Flutter Application?
Published January 25, 2022Now a days in every apps to attract the users to app engagement they will provide number of offers. To get that offers avail users need to scratch he give coupon cards/gift cards. In this flutter example we will create simple Scratch Card app with flutter using scratcher plugin
![]() |
So Let's implement Scratch Card feature in flutter
Step 1: Create Flutter Application
Step 2: Add required dependencies in puspec.yaml file
dependencies: flutter: sdk: flutter scratcher: |
Step 3: Import library in main dart file
import 'package:scratcher/scratcher.dart'; |
Step 4: Create Scratch Card
Create Scratch card by using Scratcher widget
Constructor of Scratcher widget
Scratcher({ Key? key, required this.child, this.enabled = true, this.threshold, this.brushSize = 25, this.accuracy = ScratchAccuracy.high, this.color = Colors.black, this.image, this.rebuildOnResize = true, this.onChange, this.onThreshold, this.onScratchStart, this.onScratchUpdate, this.onScratchEnd, }) : super(key: key); /// Widget rendered under the scratch area. final Widget child; /// Whether new scratches can be applied final bool enabled; /// Percentage level of scratch area which should be revealed to complete. final double? threshold; /// Size of the brush. The bigger it is the faster user can scratch the card. final double brushSize; /// Determines how accurate the progress should be reported. /// Lower accuracy means higher performance. final ScratchAccuracy accuracy; /// Color used to cover the child widget. final Color color; /// Image widget used to cover the child widget. final Image? image; /// Determines if the scratcher should rebuild itself when space constraints change (resize). final bool rebuildOnResize; /// Callback called when new part of area is revealed (min 0.1% difference, or progress == 100). final Function(double value)? onChange; /// Callback called when threshold is reached. final VoidCallback? onThreshold; /// Callback called when scratching starts final VoidCallback? onScratchStart; /// Callback called during scratching final VoidCallback? onScratchUpdate; /// Callback called when scratching ends final VoidCallback? onScratchEnd; |
- This widget contains property called child which we will place our hidden offer text/image.
- The image property will use to show on top on the offer, we can also set color instead of image
- Set the scratching brush size by using brushSize
- Set height and width for the Scratch card by height and width properties.
In this example we are showing multiple scratch cards
Let check the complete code for the main.dart file
import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:scratcher/utils.dart'; import 'painter.dart'; const _progressReportStep = 0.1; /// How accurate should the progress be tracked. enum ScratchAccuracy { /// Low accuracy, higher performance. low, /// Medium accuracy, medium performance medium, /// High accuracy, lower performance. high, } double _getAccuracyValue(ScratchAccuracy accuracy) { switch (accuracy) { case ScratchAccuracy.low: return 10.0; case ScratchAccuracy.medium: return 30.0; case ScratchAccuracy.high: return 100.0; } } /// Scratcher widget which covers given child with scratchable overlay. class Scratcher extends StatefulWidget { Scratcher({ Key? key, required this.child, this.enabled = true, this.threshold, this.brushSize = 25, this.accuracy = ScratchAccuracy.high, this.color = Colors.black, this.image, this.rebuildOnResize = true, this.onChange, this.onThreshold, this.onScratchStart, this.onScratchUpdate, this.onScratchEnd, }) : super(key: key); /// Widget rendered under the scratch area. final Widget child; /// Whether new scratches can be applied final bool enabled; /// Percentage level of scratch area which should be revealed to complete. final double? threshold; /// Size of the brush. The bigger it is the faster user can scratch the card. final double brushSize; /// Determines how accurate the progress should be reported. /// Lower accuracy means higher performance. final ScratchAccuracy accuracy; /// Color used to cover the child widget. final Color color; /// Image widget used to cover the child widget. final Image? image; /// Determines if the scratcher should rebuild itself when space constraints change (resize). final bool rebuildOnResize; /// Callback called when new part of area is revealed (min 0.1% difference, or progress == 100). final Function(double value)? onChange; /// Callback called when threshold is reached. final VoidCallback? onThreshold; /// Callback called when scratching starts final VoidCallback? onScratchStart; /// Callback called during scratching final VoidCallback? onScratchUpdate; /// Callback called when scratching ends final VoidCallback? onScratchEnd; @override ScratcherState createState() => ScratcherState(); } class ScratcherState extends State<Scratcher> { late Future<ui.Image?> _imageLoader; Offset? _lastPosition; List<ScratchPoint?> points = []; late Set<Offset> checkpoints; Set<Offset> checked = {}; int totalCheckpoints = 0; double progress = 0; double progressReported = 0; bool thresholdReported = false; bool isFinished = false; bool canScratch = true; Duration? transitionDuration; Size? _lastKnownSize; RenderBox? get _renderObject { return context.findRenderObject() as RenderBox?; } @override void initState() { if (widget.image == null) { final completer = Completer<ui.Image?>()..complete(); _imageLoader = completer.future; } else { _imageLoader = _loadImage(widget.image!); } super.initState(); } @override Widget build(BuildContext context) { return FutureBuilder<ui.Image?>( future: _imageLoader, builder: (BuildContext context, AsyncSnapshot<ui.Image?> snapshot) { if (snapshot.connectionState != ConnectionState.waiting) { return GestureDetector( behavior: HitTestBehavior.opaque, onPanStart: canScratch ? (details) { widget.onScratchStart?.call(); if (widget.enabled) { _addPoint(details.localPosition); } } : null, onPanUpdate: canScratch ? (details) { widget.onScratchUpdate?.call(); if (widget.enabled) { _addPoint(details.localPosition); } } : null, onPanEnd: canScratch ? (details) { widget.onScratchEnd?.call(); if (widget.enabled) { setState(() => points.add(null)); } } : null, child: AnimatedSwitcher( duration: transitionDuration ?? Duration.zero, child: isFinished ? widget.child : CustomPaint( foregroundPainter: ScratchPainter( image: snapshot.data, imageFit: widget.image == null ? null : widget.image!.fit ?? BoxFit.cover, points: points, color: widget.color, onDraw: (size) { if (_lastKnownSize == null) { _setCheckpoints(size); } else if (_lastKnownSize != size && widget.rebuildOnResize) { WidgetsBinding.instance?.addPostFrameCallback((_) { reset(); }); } _lastKnownSize = size; }, ), child: widget.child, ), ), ); } return Container(); }, ); } Future<ui.Image> _loadImage(Image image) async { final completer = Completer<ui.Image>(); final imageProvider = image.image as dynamic; final key = await imageProvider.obtainKey(const ImageConfiguration()); imageProvider.load(key, ( Uint8List bytes, { int? cacheWidth, int? cacheHeight, bool? allowUpscaling, }) async { return ui.instantiateImageCodec(bytes); }).addListener(ImageStreamListener((ImageInfo image, _) { completer.complete(image.image); })); return completer.future; } bool _inCircle(Offset center, Offset point, double radius) { final dX = center.dx - point.dx; final dY = center.dy - point.dy; final multi = dX * dX + dY * dY; final distance = sqrt(multi).roundToDouble(); return distance <= radius; } void _addPoint(Offset position) { // Ignore when same point is reported multiple times in a row if (_lastPosition == position) { return; } _lastPosition = position; ui.Offset? point = position; // Ignore when starting point of new line has been already scratched if (points.isNotEmpty && points.contains(point)) { if (points.last == null) { return; } else { point = null; } } setState(() { points.add(ScratchPoint(point, widget.brushSize)); }); if (point != null && !checked.contains(point)) { checked.add(point); final reached = <Offset>{}; for (final checkpoint in checkpoints) { final radius = widget.brushSize / 2; if (_inCircle(checkpoint, point, radius)) { reached.add(checkpoint); } } checkpoints = checkpoints.difference(reached); progress = ((totalCheckpoints - checkpoints.length) / totalCheckpoints) * 100; if (progress - progressReported >= _progressReportStep || progress == 100) { progressReported = progress; widget.onChange?.call(progress); } if (!thresholdReported && widget.threshold != null && progress >= widget.threshold!) { thresholdReported = true; widget.onThreshold?.call(); } if (progress == 100) { isFinished = true; } } } void _setCheckpoints(Size size) { final calculated = _calculateCheckpoints(size).toSet(); checkpoints = calculated; totalCheckpoints = calculated.length; } List<Offset> _calculateCheckpoints(Size size) { final accuracy = _getAccuracyValue(widget.accuracy); final xOffset = size.width / accuracy; final yOffset = size.height / accuracy; final points = <Offset>[]; for (var x = 0; x < accuracy; x++) { for (var y = 0; y < accuracy; y++) { final point = Offset( x * xOffset, y * yOffset, ); points.add(point); } } return points; } /// Resets the scratcher state to the initial values. void reset({Duration? duration}) { setState(() { transitionDuration = duration; isFinished = false; canScratch = duration == null; thresholdReported = false; _lastPosition = null; points = []; checked = {}; progress = 0; progressReported = 0; }); // Do not allow to scratch during transition if (duration != null) { Future.delayed(duration, () { setState(() { canScratch = true; }); }); } _setCheckpoints(_renderObject!.size); widget.onChange?.call(0); } /// Reveals the whole scratcher, so than only original child is displayed. void reveal({Duration? duration}) { setState(() { transitionDuration = duration; isFinished = true; canScratch = false; if (!thresholdReported && widget.threshold != null) { thresholdReported = true; widget.onThreshold?.call(); } }); widget.onChange?.call(100); } } |
Let's run the application you can see the Scratch cards on the UI.
![]() |
![]() |
Conclusion: In this Flutter example we covered how to create simple scratch card ui in flutter.
Article Contributed By :
|
|
|
|
1413 Views |