Flutter 仿图片裁剪框

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class ImagePoints extends StatefulWidget {
  // 图片路径(只接受本地路径或者网络路径)
  final String imgUrl;
  // 四个点的坐标数组(例 [Offset(100, 100),Offset(200, 100),Offset(200, 200),Offset(100, 200),])
  final List<Offset> points;
  // 点的宽高
  final double pointSize;
  // 点的颜色
  final Color pointColor;
  // 线的颜色
  final Color lineColor;
  // 线的宽度
  final double lineWidth;
  // 点移动时的回调,会返回一个参数为新的点位置,顺序为传入points的倒序
  Function? listenerPoint;

  ImagePoints({
    required this.imgUrl,
    required this.points,
    this.pointSize = 20,
    this.pointColor = Colors.green,
    this.lineColor = Colors.red,
    this.lineWidth = 5,
    this.listenerPoint,
  });

  @override
  _ImagePointsState createState() => _ImagePointsState();
}

class _ImagePointsState extends State<ImagePoints> {

 

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<ui.Image>(
      future: loadImage(widget.imgUrl),
      builder: (context, snapshot) {
        if (snapshot.data != null) {
          return CustomPaint(
              painter: ImagePainter(snapshot.data!, widget.points,
                  pointWidth: widget.pointSize,
                  lineColor: widget.lineColor,
                  lineWidth: widget.lineWidth),
              child: Stack(
                  children: _pointWidget(draggablePoints: widget.points)));
        } else {
          return SizedBox();
        }
      },
    );
  }

// 拖动角点中心点时,实际移动的还是角点,同时移动两个
  Map setDouble(i) {
    // 移动的角点下标
    List _list = [];
    // 移动的是否x轴
    bool isX = true;
    if (i == -1) {
      _list = [0, 1];
      isX = true;
    } else if (i == -2) {
      _list = [1, 2];
      isX = false;
    } else if (i == -3) {
      _list = [2, 3];
      isX = true;
    } else if (i == -4) {
      _list = [3, 0];
      isX = false;
    }

    return {'list': _list, 'isX': isX};
  }

  // 防止越出屏幕外侧
  bool _prevent({required dragga}) {
    return dragga.dx <= 0 ||
        dragga.dy <= 0 ||
        dragga.dx >= MediaQuery.of(context).size.width ||
        dragga.dy >= MediaQuery.of(context).size.height;
  }

  // 实际显示的点(覆盖了画布上绘制的点,因为无法在画布绘制的点上直接添加事件)
  List<Widget> _pointWidget({required List<Offset> draggablePoints}) {
    var _event;
    List<Widget> pointWidget = [
      ClipPath(
        clipper: MyCustomClipper(draggablePoints),
        child: Listener(
          // 拖拽点中间位置可挪动整个图框
          onPointerMove: (PointerMoveEvent event) {
            setState(() {
              for (var j = 0; j < draggablePoints.length; j++) {
                if (_event != null) {
                  var _dragga = Offset(
                    draggablePoints[j].dx +
                        (event.position.dx - _event.position.dx),
                    draggablePoints[j].dy +
                        (event.position.dy - _event.position.dy),
                  );
                  if (_prevent(dragga: _dragga)) return;
                  draggablePoints[j] = _dragga;
                }
              }
              _event = event;
            });
            if (widget.listenerPoint != null) {
              // 将新位置数组倒序抛出
              final List<Offset> reversedNumbers =
                  draggablePoints.reversed.toList();
              widget.listenerPoint!(reversedNumbers);
            }
          },
          child: Container(
            width: double.infinity,
            height: double.infinity,
            color: Colors.pink,
          ),
        ),
      )
    ];

    // 拖动角点之间的点
    void _centerPoint({required i, required event, required oldEvent}) {
      if (setDouble(i)['isX']) {
        setDouble(i)['list'].forEach((e) {
          var _dragga = Offset(draggablePoints[e].dx,
              draggablePoints[e].dy + (event.position.dy - oldEvent.position.dy));
          if (_prevent(dragga: _dragga)) return;
          draggablePoints[e] = _dragga;
        });
      } else {
        setDouble(i)['list'].forEach((e) {
          var _dragga = Offset(
              draggablePoints[e].dx + (event.position.dx - oldEvent.position.dx),
              draggablePoints[e].dy);
          if (_prevent(dragga: _dragga)) return;
          draggablePoints[e] = _dragga;
        });
      }
    }

    // 只有角点之间的点才需要传坐标点offset
    Widget _widget(i, {Offset? offset}) {
      var _oldEvent;
      return Transform.translate(
        offset: offset ??
            Offset(draggablePoints[i].dx - widget.pointSize / 2,
                draggablePoints[i].dy - widget.pointSize / 2),
        child: Listener(
          // 按下并在元素上移动时(拖拽点时)
          onPointerMove: (PointerMoveEvent event) {
            setState(() {
              // 拉动的是角点之间的点
              if (i < 0 && _oldEvent != null) {
                _centerPoint(event: event, i: i, oldEvent: _oldEvent);
              } else if (i >= 0) {
                // 拉动角点
                draggablePoints[i] = event.position;
              }
              // 传入的回调
              if (widget.listenerPoint != null) {
                // 将新位置数组倒序抛出
                final List<Offset> reversedNumbers =
                    draggablePoints.reversed.toList();
                widget.listenerPoint!(reversedNumbers);
              }
              _oldEvent = event;
            });
          },
          child: Container(
            width: widget.pointSize,
            height: widget.pointSize,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: widget.pointColor,
            ),
          ),
        ),
      );
    }

    for (var i = 0; i < draggablePoints.length; i++) {
      // 添加角点
      pointWidget.add(_widget(i));
      // 添加角点的中心点
      if (i == draggablePoints.length - 1) {
        final midPoint = Offset(
          (draggablePoints[draggablePoints.length - 1].dx +
                      draggablePoints[0].dx) /
                  2 -
              widget.pointSize / 2,
          (draggablePoints[draggablePoints.length - 1].dy +
                      draggablePoints[0].dy) /
                  2 -
              widget.pointSize / 2,
        );
        // 添加最后一个点与第一个点之间的点
        pointWidget.add(_widget(-i - 1, offset: midPoint));
      } else {
        final midPoint = Offset(
          (draggablePoints[i].dx + draggablePoints[i + 1].dx) / 2 -
              widget.pointSize / 2,
          (draggablePoints[i].dy + draggablePoints[i + 1].dy) / 2 -
              widget.pointSize / 2,
        );

        // 添加线段的中间点
        pointWidget.add(_widget(-i - 1, offset: midPoint));
      }
    }

    return pointWidget;
  }
}

// 绘制点和线
class ImagePainter extends CustomPainter {
  // 图片路径
  final ui.Image image;
  // 点坐标数组
  final List<Offset> points;
  // 点的宽度
  double pointWidth;
  // 线颜色
  Color lineColor;
  // 线宽
  double lineWidth;

  ImagePainter(
    this.image,
    this.points, {
    required this.pointWidth,
    required this.lineColor,
    required this.lineWidth,
  });

  // 在此进行实际绘图操作
  @override
  void paint(Canvas canvas, Size size) {
    // 获取画布大小
    final imageSize = Size(image.width.toDouble(), image.height.toDouble());
    final fitSize = calculateFitSize(imageSize, size);
    final fitRect = Alignment.center.inscribe(fitSize, Offset.zero & size);
    canvas.drawImageRect(image, Offset.zero & imageSize, fitRect, Paint());

    // 点的样式
    final pointStyle = Paint()
      ..color = Colors.transparent
      ..strokeWidth = pointWidth
      ..strokeCap = StrokeCap.round;
    // 线的样式
    final lineStyle = Paint()
      ..color = lineColor
      ..strokeWidth = lineWidth
      ..strokeCap = StrokeCap.round;

    for (var i = 0; i < points.length; i++) {
      // 绘制初始点
      canvas.drawPoints(PointMode.points, [points[i]], pointStyle);
      if (i == points.length - 1) {
        final midPoint = Offset(
          (points[points.length - 1].dx + points[0].dx) / 2,
          (points[points.length - 1].dy + points[0].dy) / 2,
        );
        // 将最后一个点与第一个点连成线
        canvas.drawLine(points[points.length - 1], points[0], lineStyle);
        // 添加最后一个点与第一个点之间的点
        canvas.drawCircle(midPoint, pointWidth / 2, pointStyle);
      } else {
        // 连接线
        canvas.drawLine(points[i], points[i + 1], lineStyle);
        // 添加线段的中间点
        final midPoint = Offset(
          (points[i].dx + points[i + 1].dx) / 2,
          (points[i].dy + points[i + 1].dy) / 2,
        );
        // 添加中间点
        canvas.drawCircle(midPoint, pointWidth / 2, pointStyle);
      }
    }
  }

  // 是否需要重新绘制
  @override
  bool shouldRepaint(ImagePainter oldDelegate) {
    return image != oldDelegate.image || points != oldDelegate.points;
  }
}

// 加载图片
Future<ui.Image> loadImage(String imgUrl) async {
  final ImageProvider _imageProvider;
// 是否是网络图片
  bool containsHttp = imgUrl.contains("http");
  if (containsHttp) {
    _imageProvider = NetworkImage(imgUrl);
  } else {
    _imageProvider = FileImage(File(imgUrl));
  }

  final completer = Completer<ui.Image>();
  final stream = _imageProvider.resolve(ImageConfiguration.empty);
  stream.addListener(ImageStreamListener((info, _) {
    completer.complete(info.image);
  }));
  final image = await completer.future;
  return image;
}

// 将图像调整到画布区域的大小
Size calculateFitSize(Size imageSize, Size destinationSize) {
  final srcWidth = imageSize.width;
  final srcHeight = imageSize.height;
  final destWidth = destinationSize.width;
  final destHeight = destinationSize.height;
  final srcRatio = srcWidth / srcHeight;
  final destRatio = destWidth / destHeight;

  double width;
  double height;

  if (destRatio > srcRatio) {
    width = srcWidth * destHeight / srcHeight;
    height = destHeight;
  } else {
    width = destWidth;
    height = srcHeight * destWidth / srcWidth;
  }

  return Size(width, height);
}

// 由角点绘制成面
class MyCustomClipper extends CustomClipper<Path> {
  List points;

  MyCustomClipper(this.points);
  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(points[0].dx, points[0].dy); // 左上角坐标
    path.lineTo(points[1].dx, points[1].dy); // 右上角坐标
    path.lineTo(points[2].dx, points[2].dy); // 右下角坐标
    path.lineTo(points[3].dx, points[3].dy); // 左下角坐标
    path.close(); // 闭合路径
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return true;
  }
}

猜你喜欢

转载自blog.csdn.net/weixin_48235660/article/details/133179237