Flutter 使用 InteractiveViewer 裁剪图像

问题描述 投票:0回答:2

我想使用 InteractiveViewer 来实现我自己的图像裁剪。

步骤:

  1. 用户使用 InteractiveViewer 平移和缩放图像
  2. 用户单击按钮
  3. 获取InteractiveViewer内容的“可视”部分(边界框)
  4. 使用这个库,裁剪图像
  5. 调整裁剪后的图像大小(缩小),也许压缩它,然后上传到我的服务器

我无法理解第 3 步:获取 InteractiveViewer 内容的“可查看”部分

image flutter crop
2个回答
0
投票

我自己一直在寻找如何做到这一点!

使用 InteractiveViewerhttps://api.flutter.dev/flutter/widgets/InteractiveViewer/builder.html 让我走得很远 - 通过引用传入的 Quad,我能够获取视口。当没有缩放时它工作得很好,但是当有缩放时坐标和似乎会漂移,我无法解决发生的问题。


0
投票

经过一番努力,我成功使用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))),
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.