我想使用 InteractiveViewer 来实现我自己的图像裁剪。
步骤:
我无法理解第 3 步:获取 InteractiveViewer 内容的“可查看”部分
我自己一直在寻找如何做到这一点!
使用 InteractiveViewerhttps://api.flutter.dev/flutter/widgets/InteractiveViewer/builder.html 让我走得很远 - 通过引用传入的 Quad,我能够获取视口。当没有缩放时它工作得很好,但是当有缩放时坐标和似乎会漂移,我无法解决发生的问题。
经过一番努力,我成功使用Flutter的GestureDetector实现了图像裁剪。我在这里分享我的代码,希望它可以在将来帮助其他人。请注意,我对 Flutter 开发还比较陌生,并且此代码的很大一部分是在 ChatGPT 的帮助下编写的。可能有更直接的方法来实现相同的目标,并且代码的某些部分可以优化。
为了清楚地说明代码的功能,我添加了屏幕截图。蓝色框可通过蓝点拖动。定位后,单击蓝色按钮会触发一个后台进程,该进程会裁剪图像并将其发送到新屏幕进行显示
我
mport 'package:flutter/material.dart';
import 'dart:io';
import 'package:image/image.dart' as img;
import 'dart:typed_data';
class CropImage extends StatefulWidget {
final File? image;
const CropImage({Key? key, this.image}) : super(key: key);
@override
State<CropImage> createState() => _CropImageState();
}
class _CropImageState extends State<CropImage> {
late Future<List<int>> imageSize;
@override
void initState() {
super.initState();
imageSize = loadImageInfo(widget.image!);
}
List<Ball> balls = [
Ball(position: Offset(50, 50)),
Ball(position: Offset(50, 150)),
Ball(position: Offset(150, 50)),
Ball(position: Offset(150, 150)),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Crop the page'),
backgroundColor: Colors.amber[400],
actions: [
IconButton(
icon: Icon(Icons.check),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CropImageUsingImage(
balls: balls,
path: widget.image!,
imageSize: imageSize,
),
),
);
},
),
],
),
body: FutureBuilder<List<int>>(
future: imageSize,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
List<int> size = snapshot.data!;
double aspectRatio = size[0] / size[1];
return Stack(
children: [
Positioned(
left: 0,
top: 0,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.width / aspectRatio,
child: Image.file(
widget.image!,
fit: BoxFit.fill,
),
),
for (int i = 0; i < balls.length; i++)
for (int j = i + 1; j < balls.length; j++)
Line(start: balls[i].position, end: balls[j].position),
for (var ball in balls)
Positioned(
left: ball.position.dx,
top: ball.position.dy,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
ball.updatePosition(
details.delta, context, imageSize);
});
},
child: ball.build(),
),
),
Positioned(
left: 100,
bottom: 100,
child: Text(
'${MediaQuery.of(context).size.width.toStringAsFixed(0)} ${MediaQuery.of(context).size.height.toStringAsFixed(0)}',
style: TextStyle(color: Colors.red),
),
)
],
);
} else {
return CircularProgressIndicator();
}
},
),
);
}
}
class Ball {
Offset position;
double ballSize = 30;
Ball({required this.position});
void updatePosition(
Offset delta, dynamic context, Future<List<int>> imageSize) async {
List<int> size = await imageSize;
double screenWidth = MediaQuery.of(context).size.width;
double newX = (position.dx + delta.dx);
double newY = (position.dy + delta.dy);
double aspectRatio = size[0] / size[1];
newX = newX.clamp(0.0, screenWidth - 30); // Clamp within the range 0 to 1
newY = newY.clamp(
0.0,
MediaQuery.of(context).size.width / aspectRatio -
30); // Clamp within the range 0 to 1
position = Offset(newX, newY);
}
Widget build() {
return Container(
width: ballSize, // Set your desired size
height: ballSize, // Set your desired size
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
),
),
);
}
}
class Line extends StatelessWidget {
final Offset start;
final Offset end;
Line({required this.start, required this.end});
@override
Widget build(BuildContext context) {
return Positioned(
left: 0,
top: 0,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CustomPaint(
painter: LinePainter(start: start, end: end),
),
),
);
}
}
Future<List<int>> loadImageInfo(File file) async {
var decodedImage = await decodeImageFromList(file.readAsBytesSync());
int imageWidth = decodedImage.width;
int imageHeight = decodedImage.height;
return [imageWidth, imageHeight];
}
class LinePainter extends CustomPainter {
final Offset start;
final Offset end;
LinePainter({required this.start, required this.end});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 8.0;
final adjustedStart = start + const Offset(15, 15); // Adjust for the center
final adjustedEnd = end + const Offset(15, 15); // Adjust for the center
canvas.drawLine(adjustedStart, adjustedEnd, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class CropImageUsingImage extends StatelessWidget {
final List<Ball> balls;
final File path;
final Future<List<int>> imageSize;
const CropImageUsingImage({
Key? key,
required this.balls,
required this.path,
required this.imageSize,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder<List<int>>(
future: imageSize,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return buildCroppedImage(snapshot.data!, context);
} else {
return CircularProgressIndicator();
}
},
);
}
Widget buildCroppedImage(List<int> size, BuildContext context) {
final aspectRatio = size[0] / size[1];
final imageWidth = size[0].toDouble();
final imageHeight = size[1].toDouble();
final double screenWidth = MediaQuery.of(context).size.width;
final double screenHeight = screenWidth / aspectRatio;
final imgPoints = balls.map((ball) {
final double x = ball.position.dx / screenWidth * imageWidth;
final double y = ball.position.dy / screenHeight * imageHeight;
return img.Point(x, y);
}).toList();
final topLeft = imgPoints[0];
final topRight = imgPoints[2];
final bottomLeft = imgPoints[1];
final bottomRight = imgPoints[3];
final img.Image image = img.decodeJpg(path.readAsBytesSync())!;
final croppedImage = img.copyRectify(
image,
topLeft: topLeft,
topRight: topRight,
bottomLeft: bottomLeft,
bottomRight: bottomRight,
);
return Scaffold(
appBar: AppBar(
title: const Text('Cropped Image'),
),
body: Center(
child: Image.memory(Uint8List.fromList(img.encodePng(croppedImage))),
),
);
}
}