Skip to content

Commit ec342f6

Browse files
Merge pull request #45 from YumengNevix/main
optimize fillcropscpace imagefit
2 parents 75def6b + bff8ba3 commit ec342f6

File tree

4 files changed

+220
-13
lines changed

4 files changed

+220
-13
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ To create a rectangular crop area with a 16:9 aspect ratio, use `[`Ratio(width:
7171
### borderRadius
7272
The radius for rounded corners of the cropping area.
7373

74+
### forceInsideCropArea
75+
Whether image area must cover clip path.
76+
7477

7578
# Controller Methods
7679

lib/src/calculators/calculate_crop_fit_params.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math';
2+
13
import 'package:custom_image_crop/src/models/params_model.dart';
24
import 'package:custom_image_crop/src/widgets/custom_image_crop_widget.dart';
35

@@ -25,11 +27,13 @@ CropFitParams calculateCropFitParams({
2527
if (screenWidth <= screenHeight * aspectRatio) {
2628
cropSizeWidth = screenWidth * cropPercentage;
2729
cropSizeHeight = cropSizeWidth / aspectRatio;
28-
defaultScale = cropSizeWidth / imageWidth;
30+
defaultScale =
31+
cropSizeWidth * max(imageWidth / imageHeight, 1.0) / imageWidth;
2932
} else {
3033
cropSizeHeight = screenHeight * cropPercentage;
3134
cropSizeWidth = cropSizeHeight * aspectRatio;
32-
defaultScale = cropSizeHeight / imageHeight;
35+
defaultScale =
36+
cropSizeHeight * max(imageHeight / imageWidth, 1.0) / imageHeight;
3337
}
3438
break;
3539

lib/src/calculators/calculate_on_crop_params.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ OnCropParams caclulateOnCropParams({
2828

2929
switch (imageFit) {
3030
case CustomImageFit.fillCropSpace:
31+
double cropScale;
3132
if (screenWidth > screenHeight * aspectRatio) {
3233
uiSize = screenHeight;
3334
cropSizeMax = imageHeight.toDouble();
35+
cropScale = max(cropSizeMax / imageWidth, 1.0);
3436
} else {
3537
uiSize = screenWidth;
3638
cropSizeMax = imageWidth.toDouble();
39+
cropScale = max(cropSizeMax / imageHeight, 1.0);
3740
}
3841
translateScale = cropSizeMax / (uiSize * cropPercentage);
39-
scale = dataScale;
42+
scale = dataScale * cropScale;
4043
break;
4144

4245
case CustomImageFit.fitCropSpace:

lib/src/widgets/custom_image_crop_widget.dart

Lines changed: 207 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:math';
23
import 'dart:ui' as ui;
34

45
import 'package:custom_image_crop/custom_image_crop.dart';
@@ -91,6 +92,11 @@ class CustomImageCrop extends StatefulWidget {
9192
/// By default, the value is `true`
9293
final bool clipShapeOnCrop;
9394

95+
/// Whether image area must cover clip path
96+
/// By default, the value is `false`
97+
/// If use CustomCropShape.circle, the cropped image may have white blank.
98+
final bool forceInsideCropArea;
99+
94100
/// A custom image cropper widget
95101
///
96102
/// Uses a `CustomImageCropController` to crop the image.
@@ -125,6 +131,7 @@ class CustomImageCrop extends StatefulWidget {
125131
this.ratio,
126132
this.borderRadius = 0,
127133
Paint? imagePaintDuringCrop,
134+
this.forceInsideCropArea = false,
128135
Key? key,
129136
}) : this.imagePaintDuringCrop = imagePaintDuringCrop ??
130137
(Paint()..filterQuality = FilterQuality.high),
@@ -273,7 +280,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
273280
final angle = widget.canRotate ? event.rotationAngle : 0.0;
274281

275282
if (_dataTransitionStart != null) {
276-
addTransition(
283+
widget.cropController.addTransition(
277284
_dataTransitionStart! -
278285
CropImageData(
279286
scale: scale,
@@ -294,7 +301,199 @@ class _CustomImageCropState extends State<CustomImageCrop>
294301
void onMoveUpdate(MoveEvent event) {
295302
if (!widget.canMove) return;
296303

297-
addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
304+
widget.cropController
305+
.addTransition(CropImageData(x: event.delta.dx, y: event.delta.dy));
306+
}
307+
308+
Rect _getInitialImageRect() {
309+
assert(_imageAsUIImage != null);
310+
final image = _imageAsUIImage!;
311+
final cropFitParams = calculateCropFitParams(
312+
cropPercentage: widget.cropPercentage,
313+
imageFit: widget.imageFit,
314+
imageHeight: image.height,
315+
imageWidth: image.width,
316+
screenHeight: _height,
317+
screenWidth: _width,
318+
aspectRatio: (widget.ratio?.width ?? 1) / (widget.ratio?.height ?? 1),
319+
);
320+
final initialWidth = _imageAsUIImage!.width * cropFitParams.additionalScale;
321+
final initialHeight =
322+
_imageAsUIImage!.height * cropFitParams.additionalScale;
323+
return Rect.fromLTWH(
324+
(_width - initialWidth) / 2,
325+
(_height - initialHeight) / 2,
326+
initialWidth,
327+
initialHeight,
328+
);
329+
}
330+
331+
void _correctTransition(CropImageData transition, VoidCallback callback) {
332+
if (!widget.forceInsideCropArea || _imageAsUIImage == null) {
333+
callback();
334+
return;
335+
}
336+
final startX = data.x;
337+
final startY = data.y;
338+
callback();
339+
final pathRect = _path.getBounds();
340+
final initialImageRect = _getInitialImageRect();
341+
bool isContainPath = _isContainPath(initialImageRect, pathRect, data.scale);
342+
bool isRotated = data.angle != 0;
343+
344+
if (isContainPath) {
345+
return;
346+
}
347+
348+
if (transition.x != 0 || transition.y != 0) {
349+
if (isRotated) {
350+
_addTransitionInternal(
351+
CropImageData(x: startX - data.x, y: startY - data.y));
352+
} else {
353+
final imageRect = _getImageRect(initialImageRect, data.scale);
354+
double deltaX = min(pathRect.left - imageRect.left, 0);
355+
deltaX = pathRect.right > imageRect.right
356+
? pathRect.right - imageRect.right
357+
: deltaX;
358+
double deltaY = min(pathRect.top - imageRect.top, 0);
359+
deltaY = pathRect.bottom > imageRect.bottom
360+
? pathRect.bottom - imageRect.bottom
361+
: deltaY;
362+
_addTransitionInternal(CropImageData(x: deltaX, y: deltaY));
363+
}
364+
return;
365+
}
366+
double minEdgeHalf =
367+
min(initialImageRect.width, initialImageRect.height) / 2;
368+
double adaptScale = _calculateScaleAfterRotate(
369+
pathRect, data.scale, initialImageRect, minEdgeHalf);
370+
_addTransitionInternal(CropImageData(scale: adaptScale / data.scale));
371+
}
372+
373+
Rect _getImageRect(Rect initialImageRect, double currentScale) {
374+
final diffScale = (1 - currentScale) / 2;
375+
final left =
376+
initialImageRect.left + diffScale * initialImageRect.width + data.x;
377+
final top =
378+
initialImageRect.top + diffScale * initialImageRect.height + data.y;
379+
Rect imageRect = Rect.fromLTWH(
380+
left,
381+
top,
382+
currentScale * initialImageRect.width,
383+
currentScale * initialImageRect.height);
384+
return imageRect;
385+
}
386+
387+
double _getDistanceBetweenPointAndLine(
388+
Offset point, Offset lineStart, Offset lineEnd) {
389+
if (lineEnd.dy == lineStart.dy) {
390+
return (point.dy - lineStart.dy).abs();
391+
}
392+
if (lineEnd.dx == lineStart.dx) {
393+
return (point.dx - lineStart.dx).abs();
394+
}
395+
double line1Slop =
396+
(lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
397+
double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
398+
double line2Slop = -1 / line1Slop;
399+
double line2Delta = point.dy - point.dx * line2Slop;
400+
double crossPointX = (line2Delta - line1Delta) / (line1Slop - line2Slop);
401+
double crossPointY = line1Slop * crossPointX + line1Delta;
402+
return (Offset(crossPointX, crossPointY) - point).distance;
403+
}
404+
405+
bool _isContainPath(
406+
Rect initialImageRect, Rect pathRect, double currentScale) {
407+
final imageRect = _getImageRect(initialImageRect, currentScale);
408+
Offset topLeft, topRight, bottomLeft, bottomRight;
409+
final rad = atan(imageRect.height / imageRect.width);
410+
final len =
411+
sqrt(pow(imageRect.width / 2, 2) + pow(imageRect.height / 2, 2));
412+
bool isRotated = data.angle != 0;
413+
414+
if (isRotated) {
415+
final clockAngle = rad + data.angle;
416+
final counterClockAngle = rad - data.angle;
417+
final cosClockValue = len * cos(clockAngle);
418+
final sinClockValue = len * sin(clockAngle);
419+
final cosCounterClockValue = len * cos(counterClockAngle);
420+
final sinCounterClockValue = len * sin(counterClockAngle);
421+
bottomRight = imageRect.center.translate(cosClockValue, sinClockValue);
422+
topRight = imageRect.center
423+
.translate(cosCounterClockValue, -sinCounterClockValue);
424+
topLeft = imageRect.center.translate(-cosClockValue, -sinClockValue);
425+
bottomLeft = imageRect.center
426+
.translate(-cosCounterClockValue, sinCounterClockValue);
427+
} else {
428+
bottomRight = imageRect.bottomRight;
429+
topRight = imageRect.topRight;
430+
topLeft = imageRect.topLeft;
431+
bottomLeft = imageRect.bottomLeft;
432+
}
433+
434+
if (widget.shape == CustomCropShape.Circle) {
435+
final anchor = max(pathRect.width, pathRect.height) / 2;
436+
final pathCenter = pathRect.center;
437+
return _getDistanceBetweenPointAndLine(pathCenter, topLeft, topRight) >=
438+
anchor &&
439+
_getDistanceBetweenPointAndLine(pathCenter, topRight, bottomRight) >=
440+
anchor &&
441+
_getDistanceBetweenPointAndLine(
442+
pathCenter, bottomLeft, bottomRight) >=
443+
anchor &&
444+
_getDistanceBetweenPointAndLine(pathCenter, topLeft, bottomLeft) >=
445+
anchor;
446+
}
447+
448+
if (isRotated) {
449+
Path imagePath = Path()
450+
..moveTo(topLeft.dx, topLeft.dy)
451+
..lineTo(topRight.dx, topRight.dy)
452+
..lineTo(bottomRight.dx, bottomRight.dy)
453+
..lineTo(bottomLeft.dx, bottomLeft.dy)
454+
..close();
455+
return imagePath.contains(pathRect.topLeft) &&
456+
imagePath.contains(pathRect.topRight) &&
457+
imagePath.contains(pathRect.bottomLeft) &&
458+
imagePath.contains(pathRect.bottomRight);
459+
} else {
460+
return imageRect.contains(pathRect.topLeft) &&
461+
imageRect.contains(pathRect.topRight) &&
462+
imageRect.contains(pathRect.bottomLeft) &&
463+
imageRect.contains(pathRect.bottomRight);
464+
}
465+
}
466+
467+
double _calculateScaleAfterRotate(Rect pathRect, double startScale,
468+
Rect initialImageRect, double minEdgeHalf) {
469+
final imageCenter = initialImageRect.center.translate(data.x, data.y);
470+
final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
471+
final topRightDistance = (pathRect.topRight - imageCenter).distance;
472+
final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
473+
final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
474+
final maxDistance = max(
475+
max(max(topLeftDistance, topRightDistance), bottomLeftDistance),
476+
bottomRightDistance);
477+
double endScale = maxDistance / minEdgeHalf;
478+
479+
if (startScale >= endScale) {
480+
return endScale;
481+
}
482+
483+
///use binary search to find best scale which just contain path.
484+
///Also, we can use imageCenter、imageLine(longest one) and path vertex to calculate.
485+
double step = 1 / minEdgeHalf;
486+
487+
while ((endScale - startScale).abs() > step) {
488+
double midScale = (endScale + startScale) / 2;
489+
490+
if (_isContainPath(initialImageRect, pathRect, midScale)) {
491+
endScale = midScale;
492+
} else {
493+
startScale = midScale + step;
494+
}
495+
}
496+
return endScale;
298497
}
299498

300499
Path _getPath({
@@ -425,16 +624,14 @@ class _CustomImageCropState extends State<CustomImageCrop>
425624
return bytes == null ? null : MemoryImage(bytes.buffer.asUint8List());
426625
}
427626

627+
void _addTransitionInternal(CropImageData transition) {
628+
setData(data + transition);
629+
}
630+
428631
@override
429632
void addTransition(CropImageData transition) {
430-
setState(() {
431-
data += transition;
432-
// For now, this will do. The idea is that we create
433-
// a path from the data and check if when we combine
434-
// that with the crop path that the resulting path
435-
// overlap the hole (crop). So we check if all pixels
436-
// from the crop contain pixels from the original image
437-
data.scale = data.scale.clamp(0.1, 10.0);
633+
_correctTransition(transition, () {
634+
_addTransitionInternal(transition);
438635
});
439636
}
440637

0 commit comments

Comments
 (0)