1
1
import 'dart:async' ;
2
+ import 'dart:math' ;
2
3
import 'dart:ui' as ui;
3
4
4
5
import 'package:custom_image_crop/custom_image_crop.dart' ;
@@ -91,6 +92,11 @@ class CustomImageCrop extends StatefulWidget {
91
92
/// By default, the value is `true`
92
93
final bool clipShapeOnCrop;
93
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
+
94
100
/// A custom image cropper widget
95
101
///
96
102
/// Uses a `CustomImageCropController` to crop the image.
@@ -125,6 +131,7 @@ class CustomImageCrop extends StatefulWidget {
125
131
this .ratio,
126
132
this .borderRadius = 0 ,
127
133
Paint ? imagePaintDuringCrop,
134
+ this .forceInsideCropArea = false ,
128
135
Key ? key,
129
136
}) : this .imagePaintDuringCrop = imagePaintDuringCrop ??
130
137
(Paint ()..filterQuality = FilterQuality .high),
@@ -273,7 +280,7 @@ class _CustomImageCropState extends State<CustomImageCrop>
273
280
final angle = widget.canRotate ? event.rotationAngle : 0.0 ;
274
281
275
282
if (_dataTransitionStart != null ) {
276
- addTransition (
283
+ widget.cropController. addTransition (
277
284
_dataTransitionStart! -
278
285
CropImageData (
279
286
scale: scale,
@@ -294,7 +301,199 @@ class _CustomImageCropState extends State<CustomImageCrop>
294
301
void onMoveUpdate (MoveEvent event) {
295
302
if (! widget.canMove) return ;
296
303
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;
298
497
}
299
498
300
499
Path _getPath ({
@@ -425,16 +624,14 @@ class _CustomImageCropState extends State<CustomImageCrop>
425
624
return bytes == null ? null : MemoryImage (bytes.buffer.asUint8List ());
426
625
}
427
626
627
+ void _addTransitionInternal (CropImageData transition) {
628
+ setData (data + transition);
629
+ }
630
+
428
631
@override
429
632
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);
438
635
});
439
636
}
440
637
0 commit comments