I'm developing a mobile app using the Flutter framework.
I need to read QR Codes, and I have successfully implemented the Barcode Scan library, based on ZXing to decode one through the camera.
Now I would also like to add the chance to pick an image containing a QR code from the gallery and decoding, without having to go through the camera.
I checked the library I'm using and also this one without finding any reference to such functionality: qrcode_reader, qr.
but in vain.
A solution that would imply serializing and decoding byte by byte an image using pure Dart would be acceptable as well.
As I suggested you in my comment you could try using firebase_ml_visionpackage.
Always remember:
You must also configure Firebase for each platform project: Android
and iOS (see the example folder or
https://codelabs.developers.google.com/codelabs/flutter-firebase/#4
for step by step details).
In this example (taken from the official one, but with a specific plugin version - not the latest one) we use image_picker plugin to get the image from device and then we decode the QRCode.
pubspec.yaml
firebase_ml_vision: 0.2.1
image_picker: 0.4.12+1
detector_painters.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
enum Detector { barcode, face, label, cloudLabel, text }
class BarcodeDetectorPainter extends CustomPainter {
BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations);
final Size absoluteImageSize;
final List<Barcode> barcodeLocations;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(Barcode barcode) {
return Rect.fromLTRB(
barcode.boundingBox.left * scaleX,
barcode.boundingBox.top * scaleY,
barcode.boundingBox.right * scaleX,
barcode.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (Barcode barcode in barcodeLocations) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(barcode), paint);
}
}
#override
bool shouldRepaint(BarcodeDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.barcodeLocations != barcodeLocations;
}
}
class FaceDetectorPainter extends CustomPainter {
FaceDetectorPainter(this.absoluteImageSize, this.faces);
final Size absoluteImageSize;
final List<Face> faces;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.red;
for (Face face in faces) {
canvas.drawRect(
Rect.fromLTRB(
face.boundingBox.left * scaleX,
face.boundingBox.top * scaleY,
face.boundingBox.right * scaleX,
face.boundingBox.bottom * scaleY,
),
paint,
);
}
}
#override
bool shouldRepaint(FaceDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.faces != faces;
}
}
class LabelDetectorPainter extends CustomPainter {
LabelDetectorPainter(this.absoluteImageSize, this.labels);
final Size absoluteImageSize;
final List<Label> labels;
#override
void paint(Canvas canvas, Size size) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: TextAlign.left,
fontSize: 23.0,
textDirection: TextDirection.ltr),
);
builder.pushStyle(ui.TextStyle(color: Colors.green));
for (Label label in labels) {
builder.addText('Label: ${label.label}, '
'Confidence: ${label.confidence.toStringAsFixed(2)}\n');
}
builder.pop();
canvas.drawParagraph(
builder.build()
..layout(ui.ParagraphConstraints(
width: size.width,
)),
const Offset(0.0, 0.0),
);
}
#override
bool shouldRepaint(LabelDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.labels != labels;
}
}
// Paints rectangles around all the text in the image.
class TextDetectorPainter extends CustomPainter {
TextDetectorPainter(this.absoluteImageSize, this.visionText);
final Size absoluteImageSize;
final VisionText visionText;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(TextContainer container) {
return Rect.fromLTRB(
container.boundingBox.left * scaleX,
container.boundingBox.top * scaleY,
container.boundingBox.right * scaleX,
container.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (TextBlock block in visionText.blocks) {
for (TextLine line in block.lines) {
for (TextElement element in line.elements) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(element), paint);
}
paint.color = Colors.yellow;
canvas.drawRect(scaleRect(line), paint);
}
paint.color = Colors.red;
canvas.drawRect(scaleRect(block), paint);
}
}
#override
bool shouldRepaint(TextDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.visionText != visionText;
}
}
main.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'detector_painters.dart';
void main() => runApp(MaterialApp(home: _MyHomePage()));
class _MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<_MyHomePage> {
File _imageFile;
Size _imageSize;
dynamic _scanResults;
String _title = 'ML Vision Example';
Detector _currentDetector = Detector.text;
Future<void> _getAndScanImage() async {
setState(() {
_imageFile = null;
_imageSize = null;
});
final File imageFile =
await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
_getImageSize(imageFile);
_scanImage(imageFile);
}
setState(() {
_imageFile = imageFile;
});
}
Future<void> _getImageSize(File imageFile) async {
final Completer<Size> completer = Completer<Size>();
final Image image = Image.file(imageFile);
image.image.resolve(const ImageConfiguration()).addListener(
(ImageInfo info, bool _) {
completer.complete(Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
));
},
);
final Size imageSize = await completer.future;
setState(() {
_imageSize = imageSize;
});
}
Future<void> _scanImage(File imageFile) async {
setState(() {
_scanResults = null;
});
final FirebaseVisionImage visionImage =
FirebaseVisionImage.fromFile(imageFile);
FirebaseVisionDetector detector;
switch (_currentDetector) {
case Detector.barcode:
detector = FirebaseVision.instance.barcodeDetector();
break;
case Detector.face:
detector = FirebaseVision.instance.faceDetector();
break;
case Detector.label:
detector = FirebaseVision.instance.labelDetector();
break;
case Detector.cloudLabel:
detector = FirebaseVision.instance.cloudLabelDetector();
break;
case Detector.text:
detector = FirebaseVision.instance.textRecognizer();
break;
default:
return;
}
final dynamic results =
await detector.detectInImage(visionImage) ?? <dynamic>[];
setState(() {
_scanResults = results;
if (results is List<Barcode>
&& results[0] is Barcode) {
Barcode res = results[0];
_title = res.displayValue;
}
});
}
CustomPaint _buildResults(Size imageSize, dynamic results) {
CustomPainter painter;
switch (_currentDetector) {
case Detector.barcode:
painter = BarcodeDetectorPainter(_imageSize, results);
break;
case Detector.face:
painter = FaceDetectorPainter(_imageSize, results);
break;
case Detector.label:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.cloudLabel:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.text:
painter = TextDetectorPainter(_imageSize, results);
break;
default:
break;
}
return CustomPaint(
painter: painter,
);
}
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
decoration: BoxDecoration(
image: DecorationImage(
image: Image.file(_imageFile).image,
fit: BoxFit.fill,
),
),
child: _imageSize == null || _scanResults == null
? const Center(
child: Text(
'Scanning...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: _buildResults(_imageSize, _scanResults),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
actions: <Widget>[
PopupMenuButton<Detector>(
onSelected: (Detector result) {
_currentDetector = result;
if (_imageFile != null) _scanImage(_imageFile);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<Detector>>[
const PopupMenuItem<Detector>(
child: Text('Detect Barcode'),
value: Detector.barcode,
),
const PopupMenuItem<Detector>(
child: Text('Detect Face'),
value: Detector.face,
),
const PopupMenuItem<Detector>(
child: Text('Detect Label'),
value: Detector.label,
),
const PopupMenuItem<Detector>(
child: Text('Detect Cloud Label'),
value: Detector.cloudLabel,
),
const PopupMenuItem<Detector>(
child: Text('Detect Text'),
value: Detector.text,
),
],
),
],
),
body: _imageFile == null
? const Center(child: Text('No image selected.'))
: _buildImage(),
floatingActionButton: FloatingActionButton(
onPressed: _getAndScanImage,
tooltip: 'Pick Image',
child: const Icon(Icons.add_a_photo),
),
);
}
}
UPDATE for iOS and Android
to address a successful build on iOS I've got to use an even lower version of firebase_ml_vision plugin otherwise you have this error.
pubspec.yaml
# https://github.com/firebase/firebase-ios-sdk/issues/2151
firebase_ml_vision: 0.1.2
image_picker: 0.4.12+1
And I get the error you have so I've got to modify also my classes.
main.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'detector_painters.dart';
void main() => runApp(MaterialApp(home: _MyHomePage()));
class _MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<_MyHomePage> {
File _imageFile;
Size _imageSize;
dynamic _scanResults;
String _title = 'ML Vision Example';
Detector _currentDetector = Detector.barcode;
Future<void> _getAndScanImage() async {
setState(() {
_imageFile = null;
_imageSize = null;
});
final File imageFile =
await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
_getImageSize(imageFile);
_scanImage(imageFile);
}
setState(() {
_imageFile = imageFile;
});
}
Future<void> _getImageSize(File imageFile) async {
final Completer<Size> completer = Completer<Size>();
final Image image = Image.file(imageFile);
image.image.resolve(const ImageConfiguration()).addListener(
(ImageInfo info, bool _) {
completer.complete(Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
));
},
);
final Size imageSize = await completer.future;
setState(() {
_imageSize = imageSize;
});
}
Future<void> _scanImage(File imageFile) async {
setState(() {
_scanResults = null;
});
final FirebaseVisionImage visionImage =
FirebaseVisionImage.fromFile(imageFile);
FirebaseVisionDetector detector;
switch (_currentDetector) {
case Detector.barcode:
detector = FirebaseVision.instance.barcodeDetector();
break;
case Detector.face:
detector = FirebaseVision.instance.faceDetector();
break;
case Detector.label:
detector = FirebaseVision.instance.labelDetector();
break;
default:
return;
}
final dynamic results =
await detector.detectInImage(visionImage) ?? <dynamic>[];
setState(() {
_scanResults = results;
if (results is List<Barcode>
&& results[0] is Barcode) {
Barcode res = results[0];
_title = res.displayValue;
}
});
}
CustomPaint _buildResults(Size imageSize, dynamic results) {
CustomPainter painter;
switch (_currentDetector) {
case Detector.barcode:
painter = BarcodeDetectorPainter(_imageSize, results);
break;
case Detector.face:
painter = FaceDetectorPainter(_imageSize, results);
break;
case Detector.label:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.cloudLabel:
painter = LabelDetectorPainter(_imageSize, results);
break;
default:
break;
}
return CustomPaint(
painter: painter,
);
}
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
decoration: BoxDecoration(
image: DecorationImage(
image: Image.file(_imageFile).image,
fit: BoxFit.fill,
),
),
child: _imageSize == null || _scanResults == null
? const Center(
child: Text(
'Scanning...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: _buildResults(_imageSize, _scanResults),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
actions: <Widget>[
PopupMenuButton<Detector>(
onSelected: (Detector result) {
_currentDetector = result;
if (_imageFile != null) _scanImage(_imageFile);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<Detector>>[
const PopupMenuItem<Detector>(
child: Text('Detect Barcode'),
value: Detector.barcode,
),
const PopupMenuItem<Detector>(
child: Text('Detect Face'),
value: Detector.face,
),
const PopupMenuItem<Detector>(
child: Text('Detect Label'),
value: Detector.label,
),
const PopupMenuItem<Detector>(
child: Text('Detect Cloud Label'),
value: Detector.cloudLabel,
),
const PopupMenuItem<Detector>(
child: Text('Detect Text'),
value: Detector.text,
),
],
),
],
),
body: _imageFile == null
? const Center(child: Text('No image selected.'))
: _buildImage(),
floatingActionButton: FloatingActionButton(
onPressed: _getAndScanImage,
tooltip: 'Pick Image',
child: const Icon(Icons.add_a_photo),
),
);
}
}
detector_painters.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
enum Detector { barcode, face, label, cloudLabel, text }
class BarcodeDetectorPainter extends CustomPainter {
BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations);
final Size absoluteImageSize;
final List<Barcode> barcodeLocations;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(Barcode barcode) {
return Rect.fromLTRB(
barcode.boundingBox.left * scaleX,
barcode.boundingBox.top * scaleY,
barcode.boundingBox.right * scaleX,
barcode.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (Barcode barcode in barcodeLocations) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(barcode), paint);
}
}
#override
bool shouldRepaint(BarcodeDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.barcodeLocations != barcodeLocations;
}
}
class FaceDetectorPainter extends CustomPainter {
FaceDetectorPainter(this.absoluteImageSize, this.faces);
final Size absoluteImageSize;
final List<Face> faces;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.red;
for (Face face in faces) {
canvas.drawRect(
Rect.fromLTRB(
face.boundingBox.left * scaleX,
face.boundingBox.top * scaleY,
face.boundingBox.right * scaleX,
face.boundingBox.bottom * scaleY,
),
paint,
);
}
}
#override
bool shouldRepaint(FaceDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.faces != faces;
}
}
class LabelDetectorPainter extends CustomPainter {
LabelDetectorPainter(this.absoluteImageSize, this.labels);
final Size absoluteImageSize;
final List<Label> labels;
#override
void paint(Canvas canvas, Size size) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: TextAlign.left,
fontSize: 23.0,
textDirection: TextDirection.ltr),
);
builder.pushStyle(ui.TextStyle(color: Colors.green));
for (Label label in labels) {
builder.addText('Label: ${label.label}, '
'Confidence: ${label.confidence.toStringAsFixed(2)}\n');
}
builder.pop();
canvas.drawParagraph(
builder.build()
..layout(ui.ParagraphConstraints(
width: size.width,
)),
const Offset(0.0, 0.0),
);
}
#override
bool shouldRepaint(LabelDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.labels != labels;
}
}
You can try this:
flutter plugin: qr_code_tools
Pub Link - https://pub.dev/packages/qr_code_tools
Home Page - https://github.com/AifeiI/qr_code_tools
You can try this:
flutter plugin: scan
Pub Link - https://pub.dev/packages/scan
Home Page - https://github.com/flutter-package/flutter_scan
Follow the below steps to build a simple QR scanner and generator app in Flutter:
Step 1: First add the following dependency in your pubspec.yaml file
dependencies:
path_provider: ^1.6.24
qr_flutter: ^3.2.0
barcode_scan_fix: ^1.0.2
Main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
//Given Title
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
//Given Theme Color
theme: ThemeData(
primarySwatch: Colors.indigo,
),
//Declared first page of our app
home: HomePage(),
);
}
}
HomePage.dart
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: 500,
height: 500,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//Display Image
Image(image: NetworkImage("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQQyYwscUPOH_qPPe8Hp0HAbFNMx-TxRFubpg&usqp=CAU")),
//First Button
FlatButton(
padding: EdgeInsets.all(15),
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context)=> ScanQR()));
},
child: Text("Scan QR Code",style: TextStyle(color: Colors.indigo[900]),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
SizedBox(height: 10),
//Second Button
FlatButton(
padding: EdgeInsets.all(15),
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>
GenerateQR()));
},
child: Text("Generate QR Code", style: TextStyle(color: Colors.indigo[900]),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
],
),
)
);
}
}
ScanQR.dart
import 'package:barcode_scan_fix/barcode_scan.dart';
import 'package:flutter/material.dart';
class ScanQR extends StatefulWidget {
#override
_ScanQRState createState() => _ScanQRState();
}
class _ScanQRState extends State<ScanQR> {
String qrCodeResult = "Not Yet Scanned";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scan QR Code"),
),
body: Container(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//Message displayed over here
Text(
"Result",
style: TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
Text(
qrCodeResult,
style: TextStyle(
fontSize: 20.0,
),
textAlign: TextAlign.center,
),
SizedBox(
height: 20.0,
),
//Button to scan QR code
FlatButton(
padding: EdgeInsets.all(15),
onPressed: () async {
String codeSanner = await BarcodeScanner.scan(); //barcode scanner
setState(() {
qrCodeResult = codeSanner;
});
},
child: Text("Open Scanner",style: TextStyle(color: Colors.indigo[900]),),
//Button having rounded rectangle border
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.indigo[900]),
borderRadius: BorderRadius.circular(20.0),
),
),
],
),
),
);
}
}
GenerateQR.dart
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
class GenerateQR extends StatefulWidget {
#override
_GenerateQRState createState() => _GenerateQRState();
}
class _GenerateQRState extends State<GenerateQR> {
String qrData="https://github.com/ChinmayMunje";
final qrdataFeed = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
//Appbar having title
appBar: AppBar(
title: Center(child: Text("Generate QR Code")),
),
body: Container(
padding: EdgeInsets.all(20),
child: SingleChildScrollView(
//Scroll view given to Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
QrImage(data: qrData),
SizedBox(height: 20),
Text("Generate QR Code",style: TextStyle(fontSize: 20),),
//TextField for input link
TextField(
decoration: InputDecoration(
hintText: "Enter your link here..."
),
),
Padding(
padding: const EdgeInsets.all(8.0),
//Button for generating QR code
child: FlatButton(
onPressed: () async {
//a little validation for the textfield
if (qrdataFeed.text.isEmpty) {
setState(() {
qrData = "";
});
} else {
setState(() {
qrData = qrdataFeed.text;
});
}
},
//Title given on Button
child: Text("Generate QR Code",style: TextStyle(color: Colors.indigo[900],),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
),
],
),
),
),
);
}
}
Related
I have a main widget called DashboardWidget. Inside it, I have a Scaffold with BottomNavigationBar and a FloatingActionButton:
Now, I want to make a widget that would be dragged from the bottom by:
Swiping up with the finger.
Pressing on FloatingActionButton.
In other words, I want to expand the BottomNavigationBar.
Here's a design concept in case I was unclear.
The problem is, I'm not sure where to start to implement that. I've thought about removing the BottomNavigationBar and create a custom widget that can be expanded, but I'm not sure if it's possible either.
Output:
I used a different approach and did it without AnimationController, GlobalKey etc, the logic code is very short (_handleClick).
I only used 4 variables, simple and short!
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static double _minHeight = 80, _maxHeight = 600;
Offset _offset = Offset(0, _minHeight);
bool _isOpen = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF6F6F6),
appBar: AppBar(backgroundColor: Color(0xFFF6F6F6), elevation: 0),
body: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Align(
alignment: Alignment.topLeft,
child: FlatButton(
onPressed: _handleClick,
splashColor: Colors.transparent,
textColor: Colors.grey,
child: Text(_isOpen ? "Back" : ""),
),
),
Align(child: FlutterLogo(size: 300)),
GestureDetector(
onPanUpdate: (details) {
_offset = Offset(0, _offset.dy - details.delta.dy);
if (_offset.dy < _HomePageState._minHeight) {
_offset = Offset(0, _HomePageState._minHeight);
_isOpen = false;
} else if (_offset.dy > _HomePageState._maxHeight) {
_offset = Offset(0, _HomePageState._maxHeight);
_isOpen = true;
}
setState(() {});
},
child: AnimatedContainer(
duration: Duration.zero,
curve: Curves.easeOut,
height: _offset.dy,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 10)]),
child: Text("This is my Bottom sheet"),
),
),
Positioned(
bottom: 2 * _HomePageState._minHeight - _offset.dy - 28, // 56 is the height of FAB so we use here half of it.
child: FloatingActionButton(
child: Icon(_isOpen ? Icons.keyboard_arrow_down : Icons.add),
onPressed: _handleClick,
),
),
],
),
);
}
// first it opens the sheet and when called again it closes.
void _handleClick() {
_isOpen = !_isOpen;
Timer.periodic(Duration(milliseconds: 5), (timer) {
if (_isOpen) {
double value = _offset.dy + 10; // we increment the height of the Container by 10 every 5ms
_offset = Offset(0, value);
if (_offset.dy > _maxHeight) {
_offset = Offset(0, _maxHeight); // makes sure it does't go above maxHeight
timer.cancel();
}
} else {
double value = _offset.dy - 10; // we decrement the height by 10 here
_offset = Offset(0, value);
if (_offset.dy < _minHeight) {
_offset = Offset(0, _minHeight); // makes sure it doesn't go beyond minHeight
timer.cancel();
}
}
setState(() {});
});
}
}
You can use the BottomSheet class.
Here is a Medium-tutorial for using that, here is a youtube-tutorial using it and here is the documentation for the class.
The only difference from the tutorials is that you have to add an extra call method for showBottomSheet from your FloatingActionButton when it is touched.
Bonus: here is the Material Design page on how to use it.
You can check this code, it is a complete example of how to start implementing this kind of UI, take it with a grain of salt.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Orination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
bool _isOpen;
double _dragStart;
double _hieght;
double _maxHight;
double _currentPosition;
GlobalKey _cardKey;
AnimationController _controller;
Animation<double> _cardAnimation;
#override
void initState() {
_isOpen = false;
_hieght = 50.0;
_cardKey = GlobalKey();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 700));
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.addListener(() {
setState(() {
_hieght = _cardAnimation.value;
});
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
titleSpacing: 0.0,
title: _isOpen
? MaterialButton(
child: Text(
"Back",
style: TextStyle(color: Colors.red),
),
onPressed: () {
_isOpen = false;
_cardAnimation = Tween(begin: _hieght, end: 50.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
},
)
: Text(""),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.keyboard_arrow_up),
onPressed: () {
final RenderBox renderBoxCard = _cardKey.currentContext
.findRenderObject();
_maxHight = renderBoxCard.size.height;
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
_isOpen = true;
}),
body: Stack(
key: _cardKey,
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black12,
),
GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child:Material(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
),
elevation: 60.0,
color: Colors.white,
// shadowColor: Colors.,
child: Container(
height: _hieght,
child: Center(
child: Text("Hello, You can drag up"),
),
),
),
),
],
),
);
}
void _onPanStart(DragStartDetails details) {
_dragStart = details.globalPosition.dy;
_currentPosition = _hieght;
}
void _onPanUpdate(DragUpdateDetails details) {
final RenderBox renderBoxCard = _cardKey.currentContext.findRenderObject();
_maxHight = renderBoxCard.size.height;
final hieght = _currentPosition - details.globalPosition.dy + _dragStart;
print(
"_currentPosition = $_currentPosition _hieght = $_hieght hieght = $hieght");
if (hieght <= _maxHight && hieght >= 50.0) {
setState(() {
_hieght = _currentPosition - details.globalPosition.dy + _dragStart;
});
}
}
void _onPanEnd(DragEndDetails details) {
_currentPosition = _hieght;
if (_hieght <= 60.0) {
setState(() {
_isOpen = false;
});
} else {
setState(() {
_isOpen = true;
});
}
}
}
Edit: I modified the code by using Material Widget instead of A container with shadow for better performance,If you have any issue, please let me know .
I have an image in which certain items are there which have starting offsets and their height and width, corresponding to each item in images I have different text, I have to drag the text and drop it to the correct position on the image,I am getting different offset while tap to particular location and while drag also I am getting different offsets. How can I get the same offsets?
Here is my code and image I am using. these are the details of Tree
x=673
y=635
h=214
w=149
with respect to image.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
ImageInfo _imageInfo;
AssetImage assestImage;
double dx;
double dy;
Offset dragOffset;
#override
void initState() {
super.initState();
assestImage = AssetImage('assets/hospital.jpg');
WidgetsBinding.instance.addPostFrameCallback((a) => _getImageInfo());
}
void _getImageInfo() async {
Image image = new Image.asset('assets/hospital.jpg');
image.image
.resolve(new ImageConfiguration())
.addListener((ImageInfo info, bool _) {
_imageInfo = info;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
DragTarget(
onAccept: (Offset dragOffset) {},
builder: (
BuildContext context,
List<dynamic> accepted,
List<dynamic> rejected,
) {
return TapImage(
onTap: (Offset offset) {
print('Offset on Tapping the image is $offset');
dx = offset.dx * _imageInfo.image.width;
dy = offset.dy * _imageInfo.image.height;
if (_imageInfo != null) {
print('Image clicked: ${dx.toInt()} x ${dy.toInt()}');
if ((673 <= dx && dx <= 822) &&
(635 <= dy && dy <= 849)) {
print('you drop in tree');
} else {}
}
},
image: assestImage,
);
}),
Draggable(
dragAnchor: DragAnchor.pointer,
onDragEnd: (details) {
setState(() {
dragOffset = details.offset;
});
print('dragoffset in onDrag Method is $dragOffset');
},
data: dragOffset,
child: Container(
color: Colors.green,
child: Text(
'Tree',
style: TextStyle(fontSize: 30.0),
)),
feedback: Container(
height: 10.0,
child: Text(
'Tree',
style: TextStyle(fontSize: 15.0),
),
)),
],
),
),
);
}
}
typedef void OnTapLocation(Offset offset);
class TapImage extends StatelessWidget {
TapImage({Key key, this.onTap, this.image}) : super(key: key);
final OnTapLocation onTap;
final ImageProvider image;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details) => _onTap(details, context),
onLongPress: () {},
child: Image(image: AssetImage('assets/hospital.jpg')),
);
}
void _onTap(TapDownDetails details, BuildContext context) {
RenderBox getBox = context.findRenderObject();
Offset local = getBox.globalToLocal(details.globalPosition);
print('locla ois $local');
onTap(Offset(local.dx / getBox.size.width, local.dy / getBox.size.height));
}
}
its working for me
class _HomeState extends State<Home> {
ImageInfo _imageInfo;
AssetImage assestImage;
double getheight;
double getywidth;
Offset dragOffset;
#override
void initState() {
super.initState();
assestImage = AssetImage('assets/hospital.jpg');
WidgetsBinding.instance.addPostFrameCallback((a) => _getImageInfo());
}
void _getImageInfo() async {
Image image = new Image.asset('assets/hospital.jpg');
image.image
.resolve(new ImageConfiguration())
.addListener((ImageInfo info, bool _) {
_imageInfo = info;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
children: <Widget>[
TapImage(
onTap: (Offset offset, RenderBox getBox, TapDownDetails details) {
double dx;
double dy;
dx = offset.dx * _imageInfo.image.width;
dy = offset.dy * _imageInfo.image.height;
setState(() {
dragEnd(dx, dy);
});
},
image: assestImage,
),
Draggable(
dragAnchor: DragAnchor.pointer,
onDragStarted: () {
WidgetsBinding.instance
.addPostFrameCallback((_) => setState(() {
RenderBox getBox = context.findRenderObject();
getheight = getBox.size.height;
getywidth = getBox.size.width;
}));
},
onDragEnd: (details) {
double dx;
double dy;
dx = (details.offset.dx / getywidth) * _imageInfo.image.width;
dy =
((details.offset.dy) / getywidth) * _imageInfo.image.height;
setState(() {
dragEnd(dx, dy);
});
},
child: Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Container(
color: Colors.green,
child: Text(
'tree',
style: TextStyle(fontSize: 30.0),
)),
),
feedback: Container(
height: 10.0,
child: Text(
'tree',
style: TextStyle(fontSize: 15.0),
),
)),
],
)),
);
}
void dragEnd(double dx, double dy) {
if (_imageInfo != null) {
if ((673 <= dx && dx <= 822) && (635 <= dy && dy <= 849)) {
showDialog(
context: context,
builder: (context) {
return _textDescriptionDialog(
context,
'Drag on tree',
);
},
);
} else {
showDialog(
context: context,
builder: (context) {
return _textDescriptionDialog(
context,
'Drag outside',
);
},
);
}
}
}
Widget _textDescriptionDialog(BuildContext context, String text) {
return new FractionallySizedBox(
heightFactor: MediaQuery.of(context).orientation == Orientation.portrait
? 0.5
: 0.8,
widthFactor: MediaQuery.of(context).orientation == Orientation.portrait
? 0.8
: 0.4,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
),
child: Container(child: Center(child: Text(text))),
));
}
}
typedef void OnTapLocation(
Offset offset, RenderBox getBox, TapDownDetails details);
class TapImage extends StatelessWidget {
TapImage({Key key, this.onTap, this.image}) : super(key: key);
final OnTapLocation onTap;
final ImageProvider image;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details) => _onTap(details, context),
child: Image(image: AssetImage('assets/hospital.jpg')),
);
}
void _onTap(TapDownDetails details, BuildContext context) {
RenderBox getBox = context.findRenderObject();
print('size is ${getBox.size}');
Offset local = getBox.globalToLocal(details.globalPosition);
print('local is $local');
onTap(Offset(local.dx / getBox.size.width, local.dy / getBox.size.height),
getBox, details);
}
}
You could make new Widget then get local render box size. Something like this:
class _MyHomePageState extends State<MyHomePage> {
NetworkImage _networkImage;
ImageInfo _imageInfo;
#override
void initState() {
super.initState();
_networkImage = NetworkImage('https://i.stack.imgur.com/2PnTa.jpg');
_getImageInfo();
}
void _getImageInfo() async {
NetworkImage _key = await _networkImage.obtainKey(ImageConfiguration());
_networkImage.load(_key).addListener((ImageInfo i, bool b){
print('Image size: ${i.image.width} - ${i.image.height}');
_imageInfo = i;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ImageDetector(
onTap: (Offset offset){
if(_imageInfo != null){
print('Image clicked: ${offset.dx * _imageInfo.image.width} x ${offset.dy * _imageInfo.image.height}');
}
},
image: _networkImage,
),
),
);
}
}
typedef void OnTapLocation(Offset offset);
class ImageDetector extends StatelessWidget {
ImageDetector({Key key, this.onTap, this.image}) : super(key: key);
final OnTapLocation onTap;
final ImageProvider image;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (TapDownDetails details) => _onTap(details, context),
child: Image(image: image),
);
}
void _onTap(TapDownDetails details, BuildContext context) {
RenderBox getBox = context.findRenderObject();
Offset local = getBox.globalToLocal(details.globalPosition);
print('Clicked on: ${local.dx / getBox.size.width} - ${local.dy / getBox.size.height}');
onTap(Offset(local.dx / getBox.size.width, local.dy / getBox.size.height));
}
}
This will return click position between 0.0, 0.0 and 1.0, 1.0, you can get size of the image and get exact location from those.
Edit: updated the code
I'm developing a mobile app using the Flutter framework.
I need to read QR Codes, and I have successfully implemented the Barcode Scan library, based on ZXing to decode one through the camera.
Now I would also like to add the chance to pick an image containing a QR code from the gallery and decoding, without having to go through the camera.
I checked the library I'm using and also this one without finding any reference to such functionality: qrcode_reader, qr.
but in vain.
A solution that would imply serializing and decoding byte by byte an image using pure Dart would be acceptable as well.
As I suggested you in my comment you could try using firebase_ml_visionpackage.
Always remember:
You must also configure Firebase for each platform project: Android
and iOS (see the example folder or
https://codelabs.developers.google.com/codelabs/flutter-firebase/#4
for step by step details).
In this example (taken from the official one, but with a specific plugin version - not the latest one) we use image_picker plugin to get the image from device and then we decode the QRCode.
pubspec.yaml
firebase_ml_vision: 0.2.1
image_picker: 0.4.12+1
detector_painters.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
enum Detector { barcode, face, label, cloudLabel, text }
class BarcodeDetectorPainter extends CustomPainter {
BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations);
final Size absoluteImageSize;
final List<Barcode> barcodeLocations;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(Barcode barcode) {
return Rect.fromLTRB(
barcode.boundingBox.left * scaleX,
barcode.boundingBox.top * scaleY,
barcode.boundingBox.right * scaleX,
barcode.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (Barcode barcode in barcodeLocations) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(barcode), paint);
}
}
#override
bool shouldRepaint(BarcodeDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.barcodeLocations != barcodeLocations;
}
}
class FaceDetectorPainter extends CustomPainter {
FaceDetectorPainter(this.absoluteImageSize, this.faces);
final Size absoluteImageSize;
final List<Face> faces;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.red;
for (Face face in faces) {
canvas.drawRect(
Rect.fromLTRB(
face.boundingBox.left * scaleX,
face.boundingBox.top * scaleY,
face.boundingBox.right * scaleX,
face.boundingBox.bottom * scaleY,
),
paint,
);
}
}
#override
bool shouldRepaint(FaceDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.faces != faces;
}
}
class LabelDetectorPainter extends CustomPainter {
LabelDetectorPainter(this.absoluteImageSize, this.labels);
final Size absoluteImageSize;
final List<Label> labels;
#override
void paint(Canvas canvas, Size size) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: TextAlign.left,
fontSize: 23.0,
textDirection: TextDirection.ltr),
);
builder.pushStyle(ui.TextStyle(color: Colors.green));
for (Label label in labels) {
builder.addText('Label: ${label.label}, '
'Confidence: ${label.confidence.toStringAsFixed(2)}\n');
}
builder.pop();
canvas.drawParagraph(
builder.build()
..layout(ui.ParagraphConstraints(
width: size.width,
)),
const Offset(0.0, 0.0),
);
}
#override
bool shouldRepaint(LabelDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.labels != labels;
}
}
// Paints rectangles around all the text in the image.
class TextDetectorPainter extends CustomPainter {
TextDetectorPainter(this.absoluteImageSize, this.visionText);
final Size absoluteImageSize;
final VisionText visionText;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(TextContainer container) {
return Rect.fromLTRB(
container.boundingBox.left * scaleX,
container.boundingBox.top * scaleY,
container.boundingBox.right * scaleX,
container.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (TextBlock block in visionText.blocks) {
for (TextLine line in block.lines) {
for (TextElement element in line.elements) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(element), paint);
}
paint.color = Colors.yellow;
canvas.drawRect(scaleRect(line), paint);
}
paint.color = Colors.red;
canvas.drawRect(scaleRect(block), paint);
}
}
#override
bool shouldRepaint(TextDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.visionText != visionText;
}
}
main.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'detector_painters.dart';
void main() => runApp(MaterialApp(home: _MyHomePage()));
class _MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<_MyHomePage> {
File _imageFile;
Size _imageSize;
dynamic _scanResults;
String _title = 'ML Vision Example';
Detector _currentDetector = Detector.text;
Future<void> _getAndScanImage() async {
setState(() {
_imageFile = null;
_imageSize = null;
});
final File imageFile =
await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
_getImageSize(imageFile);
_scanImage(imageFile);
}
setState(() {
_imageFile = imageFile;
});
}
Future<void> _getImageSize(File imageFile) async {
final Completer<Size> completer = Completer<Size>();
final Image image = Image.file(imageFile);
image.image.resolve(const ImageConfiguration()).addListener(
(ImageInfo info, bool _) {
completer.complete(Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
));
},
);
final Size imageSize = await completer.future;
setState(() {
_imageSize = imageSize;
});
}
Future<void> _scanImage(File imageFile) async {
setState(() {
_scanResults = null;
});
final FirebaseVisionImage visionImage =
FirebaseVisionImage.fromFile(imageFile);
FirebaseVisionDetector detector;
switch (_currentDetector) {
case Detector.barcode:
detector = FirebaseVision.instance.barcodeDetector();
break;
case Detector.face:
detector = FirebaseVision.instance.faceDetector();
break;
case Detector.label:
detector = FirebaseVision.instance.labelDetector();
break;
case Detector.cloudLabel:
detector = FirebaseVision.instance.cloudLabelDetector();
break;
case Detector.text:
detector = FirebaseVision.instance.textRecognizer();
break;
default:
return;
}
final dynamic results =
await detector.detectInImage(visionImage) ?? <dynamic>[];
setState(() {
_scanResults = results;
if (results is List<Barcode>
&& results[0] is Barcode) {
Barcode res = results[0];
_title = res.displayValue;
}
});
}
CustomPaint _buildResults(Size imageSize, dynamic results) {
CustomPainter painter;
switch (_currentDetector) {
case Detector.barcode:
painter = BarcodeDetectorPainter(_imageSize, results);
break;
case Detector.face:
painter = FaceDetectorPainter(_imageSize, results);
break;
case Detector.label:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.cloudLabel:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.text:
painter = TextDetectorPainter(_imageSize, results);
break;
default:
break;
}
return CustomPaint(
painter: painter,
);
}
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
decoration: BoxDecoration(
image: DecorationImage(
image: Image.file(_imageFile).image,
fit: BoxFit.fill,
),
),
child: _imageSize == null || _scanResults == null
? const Center(
child: Text(
'Scanning...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: _buildResults(_imageSize, _scanResults),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
actions: <Widget>[
PopupMenuButton<Detector>(
onSelected: (Detector result) {
_currentDetector = result;
if (_imageFile != null) _scanImage(_imageFile);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<Detector>>[
const PopupMenuItem<Detector>(
child: Text('Detect Barcode'),
value: Detector.barcode,
),
const PopupMenuItem<Detector>(
child: Text('Detect Face'),
value: Detector.face,
),
const PopupMenuItem<Detector>(
child: Text('Detect Label'),
value: Detector.label,
),
const PopupMenuItem<Detector>(
child: Text('Detect Cloud Label'),
value: Detector.cloudLabel,
),
const PopupMenuItem<Detector>(
child: Text('Detect Text'),
value: Detector.text,
),
],
),
],
),
body: _imageFile == null
? const Center(child: Text('No image selected.'))
: _buildImage(),
floatingActionButton: FloatingActionButton(
onPressed: _getAndScanImage,
tooltip: 'Pick Image',
child: const Icon(Icons.add_a_photo),
),
);
}
}
UPDATE for iOS and Android
to address a successful build on iOS I've got to use an even lower version of firebase_ml_vision plugin otherwise you have this error.
pubspec.yaml
# https://github.com/firebase/firebase-ios-sdk/issues/2151
firebase_ml_vision: 0.1.2
image_picker: 0.4.12+1
And I get the error you have so I've got to modify also my classes.
main.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'detector_painters.dart';
void main() => runApp(MaterialApp(home: _MyHomePage()));
class _MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<_MyHomePage> {
File _imageFile;
Size _imageSize;
dynamic _scanResults;
String _title = 'ML Vision Example';
Detector _currentDetector = Detector.barcode;
Future<void> _getAndScanImage() async {
setState(() {
_imageFile = null;
_imageSize = null;
});
final File imageFile =
await ImagePicker.pickImage(source: ImageSource.gallery);
if (imageFile != null) {
_getImageSize(imageFile);
_scanImage(imageFile);
}
setState(() {
_imageFile = imageFile;
});
}
Future<void> _getImageSize(File imageFile) async {
final Completer<Size> completer = Completer<Size>();
final Image image = Image.file(imageFile);
image.image.resolve(const ImageConfiguration()).addListener(
(ImageInfo info, bool _) {
completer.complete(Size(
info.image.width.toDouble(),
info.image.height.toDouble(),
));
},
);
final Size imageSize = await completer.future;
setState(() {
_imageSize = imageSize;
});
}
Future<void> _scanImage(File imageFile) async {
setState(() {
_scanResults = null;
});
final FirebaseVisionImage visionImage =
FirebaseVisionImage.fromFile(imageFile);
FirebaseVisionDetector detector;
switch (_currentDetector) {
case Detector.barcode:
detector = FirebaseVision.instance.barcodeDetector();
break;
case Detector.face:
detector = FirebaseVision.instance.faceDetector();
break;
case Detector.label:
detector = FirebaseVision.instance.labelDetector();
break;
default:
return;
}
final dynamic results =
await detector.detectInImage(visionImage) ?? <dynamic>[];
setState(() {
_scanResults = results;
if (results is List<Barcode>
&& results[0] is Barcode) {
Barcode res = results[0];
_title = res.displayValue;
}
});
}
CustomPaint _buildResults(Size imageSize, dynamic results) {
CustomPainter painter;
switch (_currentDetector) {
case Detector.barcode:
painter = BarcodeDetectorPainter(_imageSize, results);
break;
case Detector.face:
painter = FaceDetectorPainter(_imageSize, results);
break;
case Detector.label:
painter = LabelDetectorPainter(_imageSize, results);
break;
case Detector.cloudLabel:
painter = LabelDetectorPainter(_imageSize, results);
break;
default:
break;
}
return CustomPaint(
painter: painter,
);
}
Widget _buildImage() {
return Container(
constraints: const BoxConstraints.expand(),
decoration: BoxDecoration(
image: DecorationImage(
image: Image.file(_imageFile).image,
fit: BoxFit.fill,
),
),
child: _imageSize == null || _scanResults == null
? const Center(
child: Text(
'Scanning...',
style: TextStyle(
color: Colors.green,
fontSize: 30.0,
),
),
)
: _buildResults(_imageSize, _scanResults),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_title),
actions: <Widget>[
PopupMenuButton<Detector>(
onSelected: (Detector result) {
_currentDetector = result;
if (_imageFile != null) _scanImage(_imageFile);
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<Detector>>[
const PopupMenuItem<Detector>(
child: Text('Detect Barcode'),
value: Detector.barcode,
),
const PopupMenuItem<Detector>(
child: Text('Detect Face'),
value: Detector.face,
),
const PopupMenuItem<Detector>(
child: Text('Detect Label'),
value: Detector.label,
),
const PopupMenuItem<Detector>(
child: Text('Detect Cloud Label'),
value: Detector.cloudLabel,
),
const PopupMenuItem<Detector>(
child: Text('Detect Text'),
value: Detector.text,
),
],
),
],
),
body: _imageFile == null
? const Center(child: Text('No image selected.'))
: _buildImage(),
floatingActionButton: FloatingActionButton(
onPressed: _getAndScanImage,
tooltip: 'Pick Image',
child: const Icon(Icons.add_a_photo),
),
);
}
}
detector_painters.dart
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:firebase_ml_vision/firebase_ml_vision.dart';
import 'package:flutter/material.dart';
enum Detector { barcode, face, label, cloudLabel, text }
class BarcodeDetectorPainter extends CustomPainter {
BarcodeDetectorPainter(this.absoluteImageSize, this.barcodeLocations);
final Size absoluteImageSize;
final List<Barcode> barcodeLocations;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
Rect scaleRect(Barcode barcode) {
return Rect.fromLTRB(
barcode.boundingBox.left * scaleX,
barcode.boundingBox.top * scaleY,
barcode.boundingBox.right * scaleX,
barcode.boundingBox.bottom * scaleY,
);
}
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
for (Barcode barcode in barcodeLocations) {
paint.color = Colors.green;
canvas.drawRect(scaleRect(barcode), paint);
}
}
#override
bool shouldRepaint(BarcodeDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.barcodeLocations != barcodeLocations;
}
}
class FaceDetectorPainter extends CustomPainter {
FaceDetectorPainter(this.absoluteImageSize, this.faces);
final Size absoluteImageSize;
final List<Face> faces;
#override
void paint(Canvas canvas, Size size) {
final double scaleX = size.width / absoluteImageSize.width;
final double scaleY = size.height / absoluteImageSize.height;
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = Colors.red;
for (Face face in faces) {
canvas.drawRect(
Rect.fromLTRB(
face.boundingBox.left * scaleX,
face.boundingBox.top * scaleY,
face.boundingBox.right * scaleX,
face.boundingBox.bottom * scaleY,
),
paint,
);
}
}
#override
bool shouldRepaint(FaceDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.faces != faces;
}
}
class LabelDetectorPainter extends CustomPainter {
LabelDetectorPainter(this.absoluteImageSize, this.labels);
final Size absoluteImageSize;
final List<Label> labels;
#override
void paint(Canvas canvas, Size size) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
ui.ParagraphStyle(
textAlign: TextAlign.left,
fontSize: 23.0,
textDirection: TextDirection.ltr),
);
builder.pushStyle(ui.TextStyle(color: Colors.green));
for (Label label in labels) {
builder.addText('Label: ${label.label}, '
'Confidence: ${label.confidence.toStringAsFixed(2)}\n');
}
builder.pop();
canvas.drawParagraph(
builder.build()
..layout(ui.ParagraphConstraints(
width: size.width,
)),
const Offset(0.0, 0.0),
);
}
#override
bool shouldRepaint(LabelDetectorPainter oldDelegate) {
return oldDelegate.absoluteImageSize != absoluteImageSize ||
oldDelegate.labels != labels;
}
}
You can try this:
flutter plugin: qr_code_tools
Pub Link - https://pub.dev/packages/qr_code_tools
Home Page - https://github.com/AifeiI/qr_code_tools
You can try this:
flutter plugin: scan
Pub Link - https://pub.dev/packages/scan
Home Page - https://github.com/flutter-package/flutter_scan
Follow the below steps to build a simple QR scanner and generator app in Flutter:
Step 1: First add the following dependency in your pubspec.yaml file
dependencies:
path_provider: ^1.6.24
qr_flutter: ^3.2.0
barcode_scan_fix: ^1.0.2
Main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
//Given Title
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
//Given Theme Color
theme: ThemeData(
primarySwatch: Colors.indigo,
),
//Declared first page of our app
home: HomePage(),
);
}
}
HomePage.dart
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: 500,
height: 500,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//Display Image
Image(image: NetworkImage("https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQQyYwscUPOH_qPPe8Hp0HAbFNMx-TxRFubpg&usqp=CAU")),
//First Button
FlatButton(
padding: EdgeInsets.all(15),
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context)=> ScanQR()));
},
child: Text("Scan QR Code",style: TextStyle(color: Colors.indigo[900]),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
SizedBox(height: 10),
//Second Button
FlatButton(
padding: EdgeInsets.all(15),
onPressed: (){
Navigator.of(context).push(MaterialPageRoute(builder: (context)=>
GenerateQR()));
},
child: Text("Generate QR Code", style: TextStyle(color: Colors.indigo[900]),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
],
),
)
);
}
}
ScanQR.dart
import 'package:barcode_scan_fix/barcode_scan.dart';
import 'package:flutter/material.dart';
class ScanQR extends StatefulWidget {
#override
_ScanQRState createState() => _ScanQRState();
}
class _ScanQRState extends State<ScanQR> {
String qrCodeResult = "Not Yet Scanned";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scan QR Code"),
),
body: Container(
padding: EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//Message displayed over here
Text(
"Result",
style: TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
Text(
qrCodeResult,
style: TextStyle(
fontSize: 20.0,
),
textAlign: TextAlign.center,
),
SizedBox(
height: 20.0,
),
//Button to scan QR code
FlatButton(
padding: EdgeInsets.all(15),
onPressed: () async {
String codeSanner = await BarcodeScanner.scan(); //barcode scanner
setState(() {
qrCodeResult = codeSanner;
});
},
child: Text("Open Scanner",style: TextStyle(color: Colors.indigo[900]),),
//Button having rounded rectangle border
shape: RoundedRectangleBorder(
side: BorderSide(color: Colors.indigo[900]),
borderRadius: BorderRadius.circular(20.0),
),
),
],
),
),
);
}
}
GenerateQR.dart
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
class GenerateQR extends StatefulWidget {
#override
_GenerateQRState createState() => _GenerateQRState();
}
class _GenerateQRState extends State<GenerateQR> {
String qrData="https://github.com/ChinmayMunje";
final qrdataFeed = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(
//Appbar having title
appBar: AppBar(
title: Center(child: Text("Generate QR Code")),
),
body: Container(
padding: EdgeInsets.all(20),
child: SingleChildScrollView(
//Scroll view given to Column
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
QrImage(data: qrData),
SizedBox(height: 20),
Text("Generate QR Code",style: TextStyle(fontSize: 20),),
//TextField for input link
TextField(
decoration: InputDecoration(
hintText: "Enter your link here..."
),
),
Padding(
padding: const EdgeInsets.all(8.0),
//Button for generating QR code
child: FlatButton(
onPressed: () async {
//a little validation for the textfield
if (qrdataFeed.text.isEmpty) {
setState(() {
qrData = "";
});
} else {
setState(() {
qrData = qrdataFeed.text;
});
}
},
//Title given on Button
child: Text("Generate QR Code",style: TextStyle(color: Colors.indigo[900],),),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: BorderSide(color: Colors.indigo[900]),
),
),
),
],
),
),
),
);
}
}
Here I want to make a masking effect, like Wizard School app.
Here I am using RenderProxyBox but I can do only one mask at one time, I want to give multi-time effect. Using blendMode.clear here I am removing the cover image, and revealing reveals the image. So, is there any other way to implement multi masking effect as given in Expected section.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart: math' as math;
class DemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scratch Card',
home: Scaffold(
appBar: AppBar(
title: Text('Scratch Card'),
),
body: Material(
child: Center(
child: SizedBox(
width: 500.0,
height: 500.0,
child: Stack(
children: <Widget>[
ScratchCard(
cover: Stack(
fit: StackFit.expand,
children: <Widget>[
FittedBox(
child: Image.asset(
'assets/bird.jpg',
repeat: ImageRepeat.repeat,
),
),
],
),
reveal: DecoratedBox(
decoration: const BoxDecoration(color: Colors.black),
child: Center(
child:
FittedBox(child: Image.asset('assets/flower.jpg')),
),
),
strokeWidth: 15.0,
finishPercent: 50,
onComplete: () => print('The card is now clear!'),
),
],
),
),
),
),
),
);
}
}
class ScratchCard extends StatefulWidget {
const ScratchCard({
Key key,
this.cover,
this.reveal,
this.strokeWidth = 25.0,
this.finishPercent,
this.onComplete,
}) : super(key: key);
final Widget cover;
final Widget reveal;
final double strokeWidth;
final int finishPercent;
final VoidCallback onComplete;
#override
_ScratchCardState createState() => _ScratchCardState();
}
class _ScratchCardState extends State<ScratchCard> {
_ScratchData _data = _ScratchData();
Offset _lastPoint = null;
Offset _globalToLocal(Offset global) {
return (context.findRenderObject() as RenderBox).globalToLocal(global);
}
double _distanceBetween(Offset point1, Offset point2) {
return math.sqrt(math.pow(point2.dx - point1.dx, 2) +
math.pow(point2.dy - point1.dy, 2));
}
double _angleBetween(Offset point1, Offset point2) {
return math.atan2(point2.dx - point1.dx, point2.dy - point1.dy);
}
void _onPanDown(DragDownDetails details) {
_lastPoint = _globalToLocal(details.globalPosition);
}
void _onPanUpdate(DragUpdateDetails details) {
final currentPoint = _globalToLocal(details.globalPosition);
final distance = _distanceBetween(_lastPoint, currentPoint);
final angle = _angleBetween(_lastPoint, currentPoint);
for (double i = 0.0; i < distance; i++) {
_data.addPoint(Offset(
_lastPoint.dx + (math.sin(angle) * i),
_lastPoint.dy + (math.cos(angle) * i),
));
}
_lastPoint = currentPoint;
}
void _onPanEnd(TapUpDetails details) {
final areaRect = context.size.width * context.size.height;
double touchArea = math.pi * widget.strokeWidth * widget.strokeWidth;
double areaRevealed =
_data._points.fold(0.0, (double prev, Offset point) => touchArea);
print('areaRect $areaRect $areaRevealed');
}
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onTapUp: _onPanEnd,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
widget.reveal,
_ScratchCardLayout(
strokeWidth: widget.strokeWidth,
data: _data,
child: widget.cover,
),
],
),
);
}
}
class _ScratchCardLayout extends SingleChildRenderObjectWidget {
_ScratchCardLayout({
Key key,
this.strokeWidth = 25.0,
#required this.data,
#required this.child,
}) : super(
key: key,
child: child,
);
final Widget child;
final double strokeWidth;
final _ScratchData data;
#override
RenderObject createRenderObject(BuildContext context) {
return _ScratchCardRender(
strokeWidth: strokeWidth,
data: data,
);
}
#override
void updateRenderObject(
BuildContext context, _ScratchCardRender renderObject) {
renderObject
..strokeWidth = strokeWidth
..data = data;
}
}
class _ScratchCardRender extends RenderProxyBox {
_ScratchCardRender({
RenderBox child,
double strokeWidth,
_ScratchData data,
}) : assert(data != null),
_strokeWidth = strokeWidth,
_data = data,
super(child);
double _strokeWidth;
_ScratchData _data;
set strokeWidth(double strokeWidth) {
assert(strokeWidth != null);
if (_strokeWidth == strokeWidth) {
return;
}
_strokeWidth = strokeWidth;
markNeedsPaint();
}
set data(_ScratchData data) {
assert(data != null);
if (_data == data) {
return;
}
if (attached) {
_data.removeListener(markNeedsPaint);
data.addListener(markNeedsPaint);
}
_data = data;
markNeedsPaint();
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
_data.addListener(markNeedsPaint);
}
#override
void detach() {
_data.removeListener(markNeedsPaint);
super.detach();
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.canvas.saveLayer(offset & size, Paint());
context.paintChild(child, offset);
Paint clear = Paint()..blendMode = BlendMode.clear;
_data._points.forEach((point) =>
context.canvas.drawCircle(offset + point, _strokeWidth, clear));
context.canvas.restore();
}
}
#override
bool get alwaysNeedsCompositing => child != null;
}
class _ScratchData extends ChangeNotifier {
List<Offset> _points = [];
void addPoint(Offset offset) {
_points.add(offset);
notifyListeners();
}
}
output:-
Expected: -
If I change the color/image than-
I want to change the color/image with the new one and keep previous one.
Step 1 convert JPG/PNG to ui.Image format using this code.
ui.Image uiImage;
static Future<void> cacheImage(String asset) async {
if (maskImageMap[asset] == null) {
try {
ByteData data = await rootBundle.load(asset);
ui.Codec codec = await ui.instantiateImageCodec(
data.buffer.asUint8List(),
);
ui.FrameInfo fi = await codec.getNextFrame();
uiImage = fi.image;
} catch (e) {
print(e);
}
}
}
Step 2: Divide the image in the pixel format.
final Float64List deviceTransform = new Float64List(16)
..[0] = devicePixelRatio
..[5] = devicePixelRatio
..[10] = 1.0
..[15] = 3.5;
Step 3: Use ImageShader to give mask filter effect.
canvas.saveLayer();
Paint().shader = ImageShader(uiImage,
TileMode.repeated, TileMode.repeated, deviceTransform);
If this answer is not enough then here is my git link
A Paint Application
I want to draw Image using the CustomPaint Class. Here I am using the RenderProxyBox to draw the image and cover with one layer, and using BlendMode.clear I am removing the layer. However I want to do that for multi image.
If anyone has any idea please comment
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart: math' as math;
class DemoApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scratch Card',
home: Scaffold(
appBar: AppBar(
title: Text('Scratch Card'),
),
body: Material(
child: Center(
child: SizedBox(
width: 500.0,
height: 500.0,
child: Stack(
children: <Widget>[
ScratchCard(
cover: Stack(
fit: StackFit.expand,
children: <Widget>[
FittedBox(
child: Image.asset(
'assets/bird.jpg',
repeat: ImageRepeat.repeat,
),
),
],
),
reveal: DecoratedBox(
decoration: const BoxDecoration(color: Colors.black),
child: Center(
child:
FittedBox(child: Image.asset('assets/flower.jpg')),
),
),
strokeWidth: 15.0,
finishPercent: 50,
onComplete: () => print('The card is now clear!'),
),
],
),
),
),
),
),
);
}
}
class ScratchCard extends StatefulWidget {
const ScratchCard({
Key key,
this.cover,
this.reveal,
this.strokeWidth = 25.0,
this.finishPercent,
this.onComplete,
}) : super(key: key);
final Widget cover;
final Widget reveal;
final double strokeWidth;
final int finishPercent;
final VoidCallback onComplete;
#override
_ScratchCardState createState() => _ScratchCardState();
}
class _ScratchCardState extends State<ScratchCard> {
_ScratchData _data = _ScratchData();
Offset _lastPoint = null;
Offset _globalToLocal(Offset global) {
return (context.findRenderObject() as RenderBox).globalToLocal(global);
}
double _distanceBetween(Offset point1, Offset point2) {
return math.sqrt(math.pow(point2.dx - point1.dx, 2) +
math.pow(point2.dy - point1.dy, 2));
}
double _angleBetween(Offset point1, Offset point2) {
return math.atan2(point2.dx - point1.dx, point2.dy - point1.dy);
}
void _onPanDown(DragDownDetails details) {
_lastPoint = _globalToLocal(details.globalPosition);
}
void _onPanUpdate(DragUpdateDetails details) {
final currentPoint = _globalToLocal(details.globalPosition);
final distance = _distanceBetween(_lastPoint, currentPoint);
final angle = _angleBetween(_lastPoint, currentPoint);
for (double i = 0.0; i < distance; i++) {
_data.addPoint(Offset(
_lastPoint.dx + (math.sin(angle) * i),
_lastPoint.dy + (math.cos(angle) * i),
));
}
_lastPoint = currentPoint;
}
void _onPanEnd(TapUpDetails details) {
final areaRect = context.size.width * context.size.height;
double touchArea = math.pi * widget.strokeWidth * widget.strokeWidth;
double areaRevealed =
_data._points.fold(0.0, (double prev, Offset point) => touchArea);
print('areaRect $areaRect $areaRevealed');
}
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onTapUp: _onPanEnd,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
widget.reveal,
_ScratchCardLayout(
strokeWidth: widget.strokeWidth,
data: _data,
child: widget.cover,
),
],
),
);
}
}
class _ScratchCardLayout extends SingleChildRenderObjectWidget {
_ScratchCardLayout({
Key key,
this.strokeWidth = 25.0,
#required this.data,
#required this.child,
}) : super(
key: key,
child: child,
);
final Widget child;
final double strokeWidth;
final _ScratchData data;
#override
RenderObject createRenderObject(BuildContext context) {
return _ScratchCardRender(
strokeWidth: strokeWidth,
data: data,
);
}
#override
void updateRenderObject(
BuildContext context, _ScratchCardRender renderObject) {
renderObject
..strokeWidth = strokeWidth
..data = data;
}
}
class _ScratchCardRender extends RenderProxyBox {
_ScratchCardRender({
RenderBox child,
double strokeWidth,
_ScratchData data,
}) : assert(data != null),
_strokeWidth = strokeWidth,
_data = data,
super(child);
double _strokeWidth;
_ScratchData _data;
set strokeWidth(double strokeWidth) {
assert(strokeWidth != null);
if (_strokeWidth == strokeWidth) {
return;
}
_strokeWidth = strokeWidth;
markNeedsPaint();
}
set data(_ScratchData data) {
assert(data != null);
if (_data == data) {
return;
}
if (attached) {
_data.removeListener(markNeedsPaint);
data.addListener(markNeedsPaint);
}
_data = data;
markNeedsPaint();
}
#override
void attach(PipelineOwner owner) {
super.attach(owner);
_data.addListener(markNeedsPaint);
}
#override
void detach() {
_data.removeListener(markNeedsPaint);
super.detach();
}
#override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.canvas.saveLayer(offset & size, Paint());
context.paintChild(child, offset);
Paint clear = Paint()..blendMode = BlendMode.clear;
_data._points.forEach((point) =>
context.canvas.drawCircle(offset + point, _strokeWidth, clear));
context.canvas.restore();
}
}
#override
bool get alwaysNeedsCompositing => child != null;
}
class _ScratchData extends ChangeNotifier {
List<Offset> _points = [];
void addPoint(Offset offset) {
_points.add(offset);
notifyListeners();
}
}
Output:
Expected: