@@ -92,6 +92,11 @@ class CustomImageCrop extends StatefulWidget {
92
92
/// By default, the value is `true`
93
93
final bool clipShapeOnCrop;
94
94
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
+
95
100
/// A custom image cropper widget
96
101
///
97
102
/// Uses a `CustomImageCropController` to crop the image.
@@ -126,9 +131,10 @@ class CustomImageCrop extends StatefulWidget {
126
131
this .ratio,
127
132
this .borderRadius = 0 ,
128
133
Paint ? imagePaintDuringCrop,
134
+ this .forceInsideCropArea = false ,
129
135
Key ? key,
130
- }) : this .imagePaintDuringCrop = imagePaintDuringCrop ??
131
- (Paint ()..filterQuality = FilterQuality .high),
136
+ }) : this .imagePaintDuringCrop =
137
+ imagePaintDuringCrop ?? (Paint ()..filterQuality = FilterQuality .high),
132
138
assert (
133
139
! (shape == CustomCropShape .Ratio && ratio == null ),
134
140
"If shape is set to Ratio, ratio should not be null." ,
@@ -320,82 +326,150 @@ class _CustomImageCropState extends State<CustomImageCrop>
320
326
);
321
327
}
322
328
323
- // just support 'pi/2 * n' angle for now
324
- bool _isAngleSupported () {
325
- final angle = data.angle;
326
- return angle % (pi/ 2 ) == 0 ;
327
- }
328
-
329
329
void _correctTransition (CropImageData transition, VoidCallback callback) {
330
- if (_imageAsUIImage == null ||
331
- widget.imageFit != CustomImageFit .fillCropSpace ||
332
- ! _isAngleSupported ()) {
330
+ if (! widget.forceInsideCropArea || _imageAsUIImage == null ) {
333
331
callback ();
334
332
return ;
335
333
}
336
- final startData = CropImageData (
337
- scale: data.scale,
338
- angle: data.angle,
339
- x: data.x,
340
- y: data.y,
341
- );
334
+ final startX = data.x;
335
+ final startY = data.y;
336
+ final startScale = data.scale;
342
337
callback ();
338
+ final pathRect = _path.getBounds ();
343
339
final initialImageRect = _getInitialImageRect ();
344
- final diffScale = (1 - data.scale) / 2 ;
340
+ bool isContainPath = _isContainPath (initialImageRect, pathRect, data.scale);
341
+ bool isRotated = data.angle != 0 ;
342
+
343
+ if (isContainPath) {
344
+ return ;
345
+ }
346
+
347
+ if (transition.x != 0 || transition.y != 0 ) {
348
+ if (isRotated) {
349
+ _addTransitionInternal (CropImageData (x: startX - data.x, y: startY - data.y));
350
+ } else {
351
+ final imageRect = _getImageRect (initialImageRect, data.scale);
352
+ double deltaX = min (pathRect.left - imageRect.left, 0 );
353
+ deltaX = pathRect.right > imageRect.right ? pathRect.right - imageRect.right : deltaX;
354
+ double deltaY = min (pathRect.top - imageRect.top, 0 );
355
+ deltaY = pathRect.bottom > imageRect.bottom ? pathRect.bottom - imageRect.bottom : deltaY;
356
+ _addTransitionInternal (CropImageData (x: deltaX, y: deltaY));
357
+ }
358
+ return ;
359
+ }
360
+ if (transition.angle == 0 ) {
361
+ _addTransitionInternal (CropImageData (scale: startScale / data.scale));
362
+ return ;
363
+ }
364
+ double minEdgeHalf = min (initialImageRect.width, initialImageRect.height) / 2 ;
365
+ double adaptScale = _calculateScaleAfterRotate (pathRect, startScale, initialImageRect, minEdgeHalf);
366
+ _addTransitionInternal (CropImageData (scale: adaptScale / data.scale));
367
+ }
368
+
369
+ Rect _getImageRect (Rect initialImageRect, double currentScale) {
370
+ final diffScale = (1 - currentScale) / 2 ;
345
371
final left = initialImageRect.left + diffScale * initialImageRect.width + data.x;
346
372
final top = initialImageRect.top + diffScale * initialImageRect.height + data.y;
347
- Rect imageRect = Rect .fromLTWH (left, top, data.scale * initialImageRect.width, data.scale * initialImageRect.height);
373
+ Rect imageRect = Rect .fromLTWH (
374
+ left, top, currentScale * initialImageRect.width, currentScale * initialImageRect.height);
375
+ return imageRect;
376
+ }
377
+
378
+ double _getDistanceBetweenPointAndLine (Offset point, Offset lineStart, Offset lineEnd) {
379
+ double line1Slop = (lineEnd.dy - lineStart.dy) / (lineEnd.dx - lineStart.dx);
380
+
381
+ if (line1Slop == 0 ) {
382
+ return (point.dy - lineStart.dy).abs ();
383
+ }
384
+ double line1Delta = lineEnd.dy - lineEnd.dx * line1Slop;
385
+ double line2Slop = - 1 / line1Slop;
386
+ double line2Delta = point.dy - point.dx * line2Slop;
387
+ double crossPointX = (line2Delta - line1Delta) / (line1Slop - line2Slop);
388
+ double crossPointY = line1Slop * crossPointX + line1Delta;
389
+ return (Offset (crossPointX, crossPointY) - point).distance;
390
+ }
391
+
392
+ bool _isContainPath (Rect initialImageRect, Rect pathRect, double currentScale) {
393
+ final imageRect = _getImageRect (initialImageRect, currentScale);
348
394
Offset topLeft, topRight, bottomLeft, bottomRight;
349
395
final rad = atan (imageRect.height / imageRect.width);
350
396
final len = sqrt (pow (imageRect.width / 2 , 2 ) + pow (imageRect.height / 2 , 2 ));
397
+ bool isRotated = data.angle != 0 ;
351
398
352
- if (data.angle != 0 ) {
399
+ if (isRotated ) {
353
400
final clockAngle = rad + data.angle;
354
401
final counterClockAngle = rad - data.angle;
355
402
final cosClockValue = len * cos (clockAngle);
356
403
final sinClockValue = len * sin (clockAngle);
357
404
final cosCounterClockValue = len * cos (counterClockAngle);
358
405
final sinCounterClockValue = len * sin (counterClockAngle);
359
- bottomRight =
360
- imageRect.center.translate (cosClockValue, sinClockValue);
361
- topRight =
362
- imageRect.center.translate (cosCounterClockValue, - sinCounterClockValue);
363
- topLeft =
364
- imageRect.center.translate (- cosClockValue, - sinClockValue);
365
- bottomLeft =
366
- imageRect.center.translate (- cosCounterClockValue, sinCounterClockValue);
406
+ bottomRight = imageRect.center.translate (cosClockValue, sinClockValue);
407
+ topRight = imageRect.center.translate (cosCounterClockValue, - sinCounterClockValue);
408
+ topLeft = imageRect.center.translate (- cosClockValue, - sinClockValue);
409
+ bottomLeft = imageRect.center.translate (- cosCounterClockValue, sinCounterClockValue);
410
+ } else {
411
+ bottomRight = imageRect.bottomRight;
412
+ topRight = imageRect.topRight;
413
+ topLeft = imageRect.topLeft;
414
+ bottomLeft = imageRect.bottomLeft;
415
+ }
416
+
417
+ if (widget.shape == CustomCropShape .Circle ) {
418
+ final anchor = max (pathRect.width, pathRect.height) / 2 ;
419
+ final pathCenter = pathRect.center;
420
+ return _getDistanceBetweenPointAndLine (pathCenter, topLeft, topRight) >= anchor &&
421
+ _getDistanceBetweenPointAndLine (pathCenter, topRight, bottomRight) >= anchor &&
422
+ _getDistanceBetweenPointAndLine (pathCenter, bottomLeft, bottomRight) >= anchor &&
423
+ _getDistanceBetweenPointAndLine (pathCenter, topLeft, bottomLeft) >= anchor;
424
+ }
367
425
426
+ if (isRotated) {
368
427
Path imagePath = Path ()
369
428
..moveTo (topLeft.dx, topLeft.dy)
370
429
..lineTo (topRight.dx, topRight.dy)
371
430
..lineTo (bottomRight.dx, bottomRight.dy)
372
431
..lineTo (bottomLeft.dx, bottomLeft.dy)
373
432
..close ();
374
- imageRect = imagePath.getBounds ();
433
+ return imagePath.contains (pathRect.topLeft) &&
434
+ imagePath.contains (pathRect.topRight) &&
435
+ imagePath.contains (pathRect.bottomLeft) &&
436
+ imagePath.contains (pathRect.bottomRight);
437
+ } else {
438
+ return imageRect.contains (pathRect.topLeft) &&
439
+ imageRect.contains (pathRect.topRight) &&
440
+ imageRect.contains (pathRect.bottomLeft) &&
441
+ imageRect.contains (pathRect.bottomRight);
375
442
}
376
- final pathRect = _path.getBounds ();
377
-
378
- double toPrecision (num n) => double .parse (n.toStringAsFixed (1 ));
443
+ }
379
444
380
- num compareTwoDecimal (double a, double b) => toPrecision (a) - toPrecision (b);
381
-
382
- if (compareTwoDecimal (imageRect.left, pathRect.left) <= 0 &&
383
- compareTwoDecimal (imageRect.top, pathRect.top) <= 0 &&
384
- compareTwoDecimal (imageRect.right, pathRect.right) >= 0 &&
385
- compareTwoDecimal (imageRect.bottom, pathRect.bottom) >= 0 ) {
386
- return ;
387
- }
388
- if (transition.x != 0 || transition.y != 0 || transition.angle != 0 ) {
389
- double deltaX = min (pathRect.left - imageRect.left, 0 );
390
- deltaX = pathRect.right > imageRect.right ? pathRect.right - imageRect.right : deltaX;
391
- double deltaY = min (pathRect.top - imageRect.top, 0 );
392
- deltaY = pathRect.bottom > imageRect.bottom ? pathRect.bottom - imageRect.bottom : deltaY;
393
- _addTransitionInternal (CropImageData (x: deltaX, y: deltaY));
394
- return ;
445
+ double _calculateScaleAfterRotate (
446
+ Rect pathRect, double startScale, Rect initialImageRect, double minEdgeHalf) {
447
+ final imageCenter = initialImageRect.center.translate (data.x, data.y);
448
+ final topLeftDistance = (pathRect.topLeft - imageCenter).distance;
449
+ final topRightDistance = (pathRect.topRight - imageCenter).distance;
450
+ final bottomLeftDistance = (pathRect.bottomLeft - imageCenter).distance;
451
+ final bottomRightDistance = (pathRect.bottomRight - imageCenter).distance;
452
+ final maxDistance =
453
+ max (max (max (topLeftDistance, topRightDistance), bottomLeftDistance), bottomRightDistance);
454
+ double endScale = maxDistance / minEdgeHalf;
455
+
456
+ if (startScale >= endScale) {
457
+ return endScale;
395
458
}
396
- if (transition.scale != 1.0 ) {
397
- _addTransitionInternal (CropImageData (scale: startData.scale / data.scale));
459
+ ///use binary search to find best scale which just contain path.
460
+ ///Also, we can use imageCenter、imageLine(longest one) and path vertex to calculate.
461
+ double step = 1 / minEdgeHalf;
462
+
463
+ while ((endScale - startScale).abs () > step) {
464
+ double midScale = (endScale + startScale) / 2 ;
465
+
466
+ if (_isContainPath (initialImageRect, pathRect, midScale)) {
467
+ endScale = midScale;
468
+ } else {
469
+ startScale = midScale + step;
470
+ }
398
471
}
472
+ return endScale;
399
473
}
400
474
401
475
Path _getPath ({
0 commit comments