Now 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; |
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 :
|
|
|
|
1200 Views |