I have a Stack() with an InteractiveViewer() widget, and a resizable Container() widget that draws a box on the screen. The InteractiveViewer() contains a CameraView().
The goal is to take a picture with the CameraView() and then crop the image taken according to the size of the resizable Container() widget and where the Container() is currently drawn on the CameraView(). Ultimately only taking a picture of what was displayed inside of the resizable Container() at the time at which the photo was taken.
Steps:
- Resize and move the container on the screen.
- Take the picture.
- See incorrectly cropped image.
Cropping Image Logic:
XFile file = await _controller!.takePicture();
_capturedImage = File(file.path);
var bytes = await _capturedImage!.readAsBytes();
IMG.Image? src = IMG.decodeImage(bytes);
if (src != null && _cropKey.currentContext?.size != null) {
var cropSize = min(_cropKey.currentContext!.size!.width, _cropKey.currentContext!.size!.height).toInt();
int offsetX = _cropKey.currentContext!.size!.width.toInt();
int offsetY = (_cropKey.currentContext!.size!.height / _cropKey.currentContext!.size!.width * offsetX).toInt();
IMG.Image destImage = IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);
var jpg = IMG.encodeJpg(destImage);
final Directory dir = await getTemporaryDirectory();
final String path = "${dir.path}/${_randomNonceString()}.png";
_capturedImage = await File(path).writeAsBytes(jpg);
setState(() {});
}
Source Code:
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
import 'package:extended_image/extended_image.dart';
import 'dart:io';
import 'dart:math';
import 'package:image/image.dart' as IMG;
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Camera Cropper',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CameraController? _controller;
late List<CameraDescription> _cameras;
File? _capturedImage;
final TransformationController _transformationController = TransformationController();
final GlobalKey _cropKey = GlobalKey();
@override
void initState() {
super.initState();
_getCameras();
}
_getCameras() async {
_cameras = await availableCameras();
_controller = CameraController(_cameras[0], ResolutionPreset.max);
await _controller?.initialize();
if (mounted) {
setState(() {});
}
}
String _randomNonceString([int length = 32]) {
final random = Random();
final charCodes = List<int>.generate(length, (_) {
late int codeUnit;
switch (random.nextInt(3)) {
case 0:
codeUnit = random.nextInt(10) + 48;
break;
case 1:
codeUnit = random.nextInt(26) + 65;
break;
case 2:
codeUnit = random.nextInt(26) + 97;
break;
}
return codeUnit;
});
return String.fromCharCodes(charCodes);
}
@override
void dispose() {
super.dispose();
_controller?.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: _controller == null
? const LoadingIndicator()
: Stack(
alignment: Alignment.bottomCenter,
children: [
Column(
children: [
Expanded(
child: InteractiveViewer(
constrained: false,
transformationController: _transformationController,
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: _capturedImage != null ? ExtendedImage.file(_capturedImage!) : CameraPreview(_controller!),
),
),
),
],
),
if (_capturedImage == null)
ResizableWidget(
key: _cropKey,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
child: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
size: 40,
),
onPressed: () {
setState(() {
_capturedImage = null;
});
},
),
GestureDetector(
onTap: () async {
XFile file = await _controller!.takePicture();
_capturedImage = File(file.path);
var bytes = await _capturedImage!.readAsBytes();
IMG.Image? src = IMG.decodeImage(bytes);
if (src != null && _cropKey.currentContext?.size != null) {
var cropSize = min(_cropKey.currentContext!.size!.width, _cropKey.currentContext!.size!.height).toInt();
int offsetX = _cropKey.currentContext!.size!.width.toInt();
int offsetY = (_cropKey.currentContext!.size!.height / _cropKey.currentContext!.size!.width * offsetX).toInt();
IMG.Image destImage = IMG.copyCrop(src, offsetX, offsetY, cropSize, cropSize);
var jpg = IMG.encodeJpg(destImage);
final Directory dir = await getTemporaryDirectory();
final String path = "${dir.path}/${_randomNonceString()}.png";
_capturedImage = await File(path).writeAsBytes(jpg);
setState(() {});
}
},
child: const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
),
),
TextButton(
child: Icon(
_controller!.value.flashMode == FlashMode.auto
? Icons.flash_auto
: _controller!.value.flashMode == FlashMode.off
? Icons.flash_off
: Icons.flash_on,
color: Colors.white,
size: 40,
),
onPressed: () async {
await _controller!.setFlashMode(_controller!.value.flashMode == FlashMode.auto
? FlashMode.off
: _controller!.value.flashMode == FlashMode.off
? FlashMode.always
: FlashMode.auto);
setState(() {});
},
),
],
),
)
],
),
);
}
}
class ResizableWidget extends StatefulWidget {
const ResizableWidget({Key? key}) : super(key: key);
@override
State<ResizableWidget> createState() => _ResizableWidgetState();
}
const ballDiameter = 30.0;
class _ResizableWidgetState extends State<ResizableWidget> {
double height = 200;
double width = 250;
double top = 250;
double left = 55;
void onDrag(double dx, double dy) {
var newHeight = height + dy;
var newWidth = width + dx;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
top: top,
left: left,
child: DottedBorder(
borderType: BorderType.RRect,
radius: const Radius.circular(20),
dashPattern: const [8, 8],
color: Colors.white,
strokeWidth: 2,
child: Container(
decoration: const BoxDecoration(
color: Colors.transparent,
),
height: height,
width: width,
),
),
),
// top left
Positioned(
top: top - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
var newHeight = height - 2 * mid;
var newWidth = width - 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top + mid;
left = left + mid;
});
},
),
),
// top middle
Positioned(
top: top - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newHeight = height - dy;
setState(() {
height = newHeight > 0 ? newHeight : 0;
top = top + dy;
});
},
),
),
// top right
Positioned(
top: top - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + (dy * -1)) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
// center right
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newWidth = width + dx;
setState(() {
width = newWidth > 0 ? newWidth : 0;
});
},
),
),
// bottom right
Positioned(
top: top + height - ballDiameter / 2,
left: left + width - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = (dx + dy) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
// bottom center
Positioned(
top: top + height - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newHeight = height + dy;
setState(() {
height = newHeight > 0 ? newHeight : 0;
});
},
),
),
// bottom left
Positioned(
top: top + height - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var mid = ((dx * -1) + dy) / 2;
var newHeight = height + 2 * mid;
var newWidth = width + 2 * mid;
setState(() {
height = newHeight > 0 ? newHeight : 0;
width = newWidth > 0 ? newWidth : 0;
top = top - mid;
left = left - mid;
});
},
),
),
//left center
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
var newWidth = width - dx;
setState(() {
width = newWidth > 0 ? newWidth : 0;
left = left + dx;
});
},
),
),
// center center
Positioned(
top: top + height / 2 - ballDiameter / 2,
left: left + width / 2 - ballDiameter / 2,
child: ManipulatingBall(
onDrag: (dx, dy) {
setState(() {
top = top + dy;
left = left + dx;
});
},
),
),
],
);
}
}
class ManipulatingBall extends StatefulWidget {
const ManipulatingBall({Key? key, required this.onDrag}) : super(key: key);
final Function onDrag;
@override
State<ManipulatingBall> createState() => _ManipulatingBallState();
}
class _ManipulatingBallState extends State<ManipulatingBall> {
double _initX = 0;
double _initY = 0;
_handleDrag(details) {
setState(() {
_initX = details.globalPosition.dx;
_initY = details.globalPosition.dy;
});
}
_handleUpdate(details) {
var dx = details.globalPosition.dx - _initX;
var dy = details.globalPosition.dy - _initY;
_initX = details.globalPosition.dx;
_initY = details.globalPosition.dy;
widget.onDrag(dx, dy);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _handleDrag,
onPanUpdate: _handleUpdate,
child: Container(
width: ballDiameter,
height: ballDiameter,
decoration: BoxDecoration(
color: Colors.pink.withOpacity(0.5),
shape: BoxShape.circle,
),
),
);
}
}
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(color: Colors.pink),
);
}
}
pubspec.yaml:
name: camera_cropper
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=2.18.0-130.0.dev <3.0.0'
dependencies:
flutter:
sdk: flutter
camera: ^0.9.6
image: ^3.2.0
extended_image: ^6.2.1
dotted_border: ^2.0.0+2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.1
flutter:
uses-material-design: true

