crop.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'dart:ui' as ui;
  4. import 'package:flutter/gestures.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. const _kCropGridColumnCount = 3;
  8. const _kCropGridRowCount = 3;
  9. const _kCropGridColor = Color.fromRGBO(0xd0, 0xd0, 0xd0, 0.9);
  10. const _kCropOverlayActiveOpacity = 0.3;
  11. const _kCropOverlayInactiveOpacity = 0.7;
  12. const _kCropHandleColor = Color.fromRGBO(0xd0, 0xd0, 0xd0, 1.0);
  13. const _kCropHandleSize = 10.0;
  14. const _kCropHandleHitSize = 48.0;
  15. const _kCropMinFraction = 0.1;
  16. enum _CropAction { none, moving, cropping, scaling }
  17. enum _CropHandleSide { none, topLeft, topRight, bottomLeft, bottomRight }
  18. class Crop extends StatefulWidget {
  19. final ImageProvider image;
  20. final double? aspectRatio;
  21. final double maximumScale;
  22. final bool alwaysShowGrid;
  23. final ImageErrorListener? onImageError;
  24. const Crop({
  25. Key? key,
  26. required this.image,
  27. this.aspectRatio,
  28. this.maximumScale = 2.0,
  29. this.alwaysShowGrid = false,
  30. this.onImageError,
  31. }) : super(key: key);
  32. Crop.file(
  33. File file, {
  34. Key? key,
  35. double scale = 1.0,
  36. this.aspectRatio,
  37. this.maximumScale = 2.0,
  38. this.alwaysShowGrid = false,
  39. this.onImageError,
  40. }) : image = FileImage(file, scale: scale),
  41. super(key: key);
  42. Crop.asset(
  43. String assetName, {
  44. Key? key,
  45. AssetBundle? bundle,
  46. String? package,
  47. this.aspectRatio,
  48. this.maximumScale = 2.0,
  49. this.alwaysShowGrid = false,
  50. this.onImageError,
  51. }) : image = AssetImage(assetName, bundle: bundle, package: package),
  52. super(key: key);
  53. @override
  54. State<StatefulWidget> createState() => CropState();
  55. static CropState? of(BuildContext context) =>
  56. context.findAncestorStateOfType<CropState>();
  57. }
  58. class CropState extends State<Crop> with TickerProviderStateMixin, Drag {
  59. final _surfaceKey = GlobalKey();
  60. late final AnimationController _activeController;
  61. late final AnimationController _settleController;
  62. double _scale = 1.0;
  63. double _ratio = 1.0;
  64. Rect _view = Rect.zero;
  65. Rect _area = Rect.zero;
  66. Offset _lastFocalPoint = Offset.zero;
  67. _CropAction _action = _CropAction.none;
  68. _CropHandleSide _handle = _CropHandleSide.none;
  69. late double _startScale;
  70. late Rect _startView;
  71. late Tween<Rect?> _viewTween;
  72. late Tween<double> _scaleTween;
  73. ImageStream? _imageStream;
  74. ui.Image? _image;
  75. ImageStreamListener? _imageListener;
  76. double get scale => _area.shortestSide / _scale;
  77. Rect? get area => _view.isEmpty
  78. ? null
  79. : Rect.fromLTWH(
  80. _area.left * _view.width / _scale - _view.left,
  81. _area.top * _view.height / _scale - _view.top,
  82. _area.width * _view.width / _scale,
  83. _area.height * _view.height / _scale,
  84. );
  85. bool get _isEnabled => _view.isEmpty == false && _image != null;
  86. // Saving the length for the widest area for different aspectRatio's
  87. final Map<double, double> _maxAreaWidthMap = {};
  88. // Counting pointers(number of user fingers on screen)
  89. int pointers = 0;
  90. @override
  91. void initState() {
  92. super.initState();
  93. _activeController = AnimationController(
  94. vsync: this,
  95. value: widget.alwaysShowGrid ? 1.0 : 0.0,
  96. )..addListener(() => setState(() {}));
  97. _settleController = AnimationController(vsync: this)
  98. ..addListener(_settleAnimationChanged);
  99. }
  100. @override
  101. void dispose() {
  102. final listener = _imageListener;
  103. if (listener != null) {
  104. _imageStream?.removeListener(listener);
  105. }
  106. _activeController.dispose();
  107. _settleController.dispose();
  108. super.dispose();
  109. }
  110. @override
  111. void didChangeDependencies() {
  112. super.didChangeDependencies();
  113. _getImage();
  114. }
  115. @override
  116. void didUpdateWidget(Crop oldWidget) {
  117. super.didUpdateWidget(oldWidget);
  118. if (widget.image != oldWidget.image) {
  119. _getImage();
  120. } else if (widget.aspectRatio != oldWidget.aspectRatio) {
  121. _area = _calculateDefaultArea(
  122. viewWidth: _view.width,
  123. viewHeight: _view.height,
  124. imageWidth: _image?.width,
  125. imageHeight: _image?.height,
  126. );
  127. }
  128. if (widget.alwaysShowGrid != oldWidget.alwaysShowGrid) {
  129. if (widget.alwaysShowGrid) {
  130. _activate();
  131. } else {
  132. _deactivate();
  133. }
  134. }
  135. }
  136. void _getImage({bool force = false}) {
  137. final oldImageStream = _imageStream;
  138. final newImageStream =
  139. widget.image.resolve(createLocalImageConfiguration(context));
  140. _imageStream = newImageStream;
  141. if (newImageStream.key != oldImageStream?.key || force) {
  142. final oldImageListener = _imageListener;
  143. if (oldImageListener != null) {
  144. oldImageStream?.removeListener(oldImageListener);
  145. }
  146. final newImageListener =
  147. ImageStreamListener(_updateImage, onError: widget.onImageError);
  148. _imageListener = newImageListener;
  149. newImageStream.addListener(newImageListener);
  150. }
  151. }
  152. @override
  153. Widget build(BuildContext context) => ConstrainedBox(
  154. constraints: const BoxConstraints.expand(),
  155. child: Listener(
  156. onPointerDown: (event) => pointers++,
  157. onPointerUp: (event) => pointers = 0,
  158. child: GestureDetector(
  159. key: _surfaceKey,
  160. behavior: HitTestBehavior.opaque,
  161. onScaleStart: _isEnabled ? _handleScaleStart : null,
  162. onScaleUpdate: _isEnabled ? _handleScaleUpdate : null,
  163. onScaleEnd: _isEnabled ? _handleScaleEnd : null,
  164. child: CustomPaint(
  165. painter: _CropPainter(
  166. image: _image,
  167. ratio: _ratio,
  168. view: _view,
  169. area: _area,
  170. scale: _scale,
  171. active: _activeController.value,
  172. ),
  173. ),
  174. ),
  175. ),
  176. );
  177. void _activate() {
  178. _activeController.animateTo(
  179. 1.0,
  180. curve: Curves.fastOutSlowIn,
  181. duration: const Duration(milliseconds: 250),
  182. );
  183. }
  184. void _deactivate() {
  185. if (widget.alwaysShowGrid == false) {
  186. _activeController.animateTo(
  187. 0.0,
  188. curve: Curves.fastOutSlowIn,
  189. duration: const Duration(milliseconds: 250),
  190. );
  191. }
  192. }
  193. Size? get _boundaries {
  194. final context = _surfaceKey.currentContext;
  195. if (context == null) {
  196. return null;
  197. }
  198. final size = context.size;
  199. if (size == null) {
  200. return null;
  201. }
  202. return size - const Offset(_kCropHandleSize, _kCropHandleSize) as Size;
  203. }
  204. Offset? _getLocalPoint(Offset point) {
  205. final context = _surfaceKey.currentContext;
  206. if (context == null) {
  207. return null;
  208. }
  209. final box = context.findRenderObject() as RenderBox;
  210. return box.globalToLocal(point);
  211. }
  212. void _settleAnimationChanged() {
  213. setState(() {
  214. _scale = _scaleTween.transform(_settleController.value);
  215. final nextView = _viewTween.transform(_settleController.value);
  216. if (nextView != null) {
  217. _view = nextView;
  218. }
  219. });
  220. }
  221. Rect _calculateDefaultArea({
  222. required int? imageWidth,
  223. required int? imageHeight,
  224. required double viewWidth,
  225. required double viewHeight,
  226. }) {
  227. if (imageWidth == null || imageHeight == null) {
  228. return Rect.zero;
  229. }
  230. double height;
  231. double width;
  232. if ((widget.aspectRatio ?? 1.0) < 1) {
  233. height = 1.0;
  234. width =
  235. ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) /
  236. imageWidth /
  237. viewWidth;
  238. if (width > 1.0) {
  239. width = 1.0;
  240. height = (imageWidth * viewWidth * width) /
  241. (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0));
  242. }
  243. } else {
  244. width = 1.0;
  245. height = (imageWidth * viewWidth * width) /
  246. (imageHeight * viewHeight * (widget.aspectRatio ?? 1.0));
  247. if (height > 1.0) {
  248. height = 1.0;
  249. width =
  250. ((widget.aspectRatio ?? 1.0) * imageHeight * viewHeight * height) /
  251. imageWidth /
  252. viewWidth;
  253. }
  254. }
  255. final aspectRatio = _maxAreaWidthMap[widget.aspectRatio];
  256. if (aspectRatio != null) {
  257. _maxAreaWidthMap[aspectRatio] = width;
  258. }
  259. return Rect.fromLTWH((1.0 - width) / 2, (1.0 - height) / 2, width, height);
  260. }
  261. void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
  262. final boundaries = _boundaries;
  263. if (boundaries == null) {
  264. return;
  265. }
  266. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  267. final image = imageInfo.image;
  268. setState(() {
  269. _image = image;
  270. _scale = imageInfo.scale;
  271. _ratio = max(
  272. boundaries.width / image.width,
  273. boundaries.height / image.height,
  274. );
  275. final viewWidth = boundaries.width / (image.width * _scale * _ratio);
  276. final viewHeight = boundaries.height / (image.height * _scale * _ratio);
  277. _area = _calculateDefaultArea(
  278. viewWidth: viewWidth,
  279. viewHeight: viewHeight,
  280. imageWidth: image.width,
  281. imageHeight: image.height,
  282. );
  283. _view = Rect.fromLTWH(
  284. (viewWidth - 1.0) / 2,
  285. (viewHeight - 1.0) / 2,
  286. viewWidth,
  287. viewHeight,
  288. );
  289. _scale = _minimumScale ?? 0.2;
  290. _handleScaleEnd(ScaleEndDetails());
  291. });
  292. });
  293. WidgetsBinding.instance?.ensureVisualUpdate();
  294. }
  295. _CropHandleSide _hitCropHandle(Offset? localPoint) {
  296. final boundaries = _boundaries;
  297. if (localPoint == null || boundaries == null) {
  298. return _CropHandleSide.none;
  299. }
  300. final viewRect = Rect.fromLTWH(
  301. boundaries.width * _area.left,
  302. boundaries.height * _area.top,
  303. boundaries.width * _area.width,
  304. boundaries.height * _area.height,
  305. ).deflate(_kCropHandleSize / 2);
  306. if (Rect.fromLTWH(
  307. viewRect.left - _kCropHandleHitSize / 2,
  308. viewRect.top - _kCropHandleHitSize / 2,
  309. _kCropHandleHitSize,
  310. _kCropHandleHitSize,
  311. ).contains(localPoint)) {
  312. return _CropHandleSide.topLeft;
  313. }
  314. if (Rect.fromLTWH(
  315. viewRect.right - _kCropHandleHitSize / 2,
  316. viewRect.top - _kCropHandleHitSize / 2,
  317. _kCropHandleHitSize,
  318. _kCropHandleHitSize,
  319. ).contains(localPoint)) {
  320. return _CropHandleSide.topRight;
  321. }
  322. if (Rect.fromLTWH(
  323. viewRect.left - _kCropHandleHitSize / 2,
  324. viewRect.bottom - _kCropHandleHitSize / 2,
  325. _kCropHandleHitSize,
  326. _kCropHandleHitSize,
  327. ).contains(localPoint)) {
  328. return _CropHandleSide.bottomLeft;
  329. }
  330. if (Rect.fromLTWH(
  331. viewRect.right - _kCropHandleHitSize / 2,
  332. viewRect.bottom - _kCropHandleHitSize / 2,
  333. _kCropHandleHitSize,
  334. _kCropHandleHitSize,
  335. ).contains(localPoint)) {
  336. return _CropHandleSide.bottomRight;
  337. }
  338. return _CropHandleSide.none;
  339. }
  340. void _handleScaleStart(ScaleStartDetails details) {
  341. _activate();
  342. _settleController.stop(canceled: false);
  343. _lastFocalPoint = details.focalPoint;
  344. _action = _CropAction.none;
  345. _handle = _hitCropHandle(_getLocalPoint(details.focalPoint));
  346. _startScale = _scale;
  347. _startView = _view;
  348. }
  349. Rect _getViewInBoundaries(double scale) =>
  350. Offset(
  351. max(
  352. min(
  353. _view.left,
  354. _area.left * _view.width / scale,
  355. ),
  356. _area.right * _view.width / scale - 1.0,
  357. ),
  358. max(
  359. min(
  360. _view.top,
  361. _area.top * _view.height / scale,
  362. ),
  363. _area.bottom * _view.height / scale - 1.0,
  364. ),
  365. ) &
  366. _view.size;
  367. double get _maximumScale => widget.maximumScale;
  368. double? get _minimumScale {
  369. final boundaries = _boundaries;
  370. final image = _image;
  371. if (boundaries == null || image == null) {
  372. return null;
  373. }
  374. final scaleX = boundaries.width * _area.width / (image.width * _ratio);
  375. final scaleY = boundaries.height * _area.height / (image.height * _ratio);
  376. return min(_maximumScale, max(scaleX, scaleY));
  377. }
  378. void _handleScaleEnd(ScaleEndDetails details) {
  379. _deactivate();
  380. final minimumScale = _minimumScale;
  381. if (minimumScale == null) {
  382. return;
  383. }
  384. final targetScale = _scale.clamp(minimumScale, _maximumScale);
  385. _scaleTween = Tween<double>(
  386. begin: _scale,
  387. end: targetScale,
  388. );
  389. _startView = _view;
  390. _viewTween = RectTween(
  391. begin: _view,
  392. end: _getViewInBoundaries(targetScale),
  393. );
  394. _settleController.value = 0.0;
  395. _settleController.animateTo(
  396. 1.0,
  397. curve: Curves.fastOutSlowIn,
  398. duration: const Duration(milliseconds: 350),
  399. );
  400. }
  401. void _updateArea({
  402. required _CropHandleSide cropHandleSide,
  403. double? left,
  404. double? top,
  405. double? right,
  406. double? bottom,
  407. }) {
  408. final image = _image;
  409. if (image == null) {
  410. return;
  411. }
  412. var areaLeft = _area.left + (left ?? 0.0);
  413. var areaBottom = _area.bottom + (bottom ?? 0.0);
  414. var areaTop = _area.top + (top ?? 0.0);
  415. var areaRight = _area.right + (right ?? 0.0);
  416. double width = areaRight - areaLeft;
  417. double height = (image.width * _view.width * width) /
  418. (image.height * _view.height * (widget.aspectRatio ?? 1.0));
  419. final maxAreaWidth = _maxAreaWidthMap[widget.aspectRatio];
  420. if ((height >= 1.0 || width >= 1.0) && maxAreaWidth != null) {
  421. height = 1.0;
  422. if (cropHandleSide == _CropHandleSide.bottomLeft ||
  423. cropHandleSide == _CropHandleSide.topLeft) {
  424. areaLeft = areaRight - maxAreaWidth;
  425. } else {
  426. areaRight = areaLeft + maxAreaWidth;
  427. }
  428. }
  429. // ensure minimum rectangle
  430. if (areaRight - areaLeft < _kCropMinFraction) {
  431. if (left != null) {
  432. areaLeft = areaRight - _kCropMinFraction;
  433. } else {
  434. areaRight = areaLeft + _kCropMinFraction;
  435. }
  436. }
  437. if (areaBottom - areaTop < _kCropMinFraction) {
  438. if (top != null) {
  439. areaTop = areaBottom - _kCropMinFraction;
  440. } else {
  441. areaBottom = areaTop + _kCropMinFraction;
  442. }
  443. }
  444. // adjust to aspect ratio if needed
  445. final aspectRatio = widget.aspectRatio;
  446. if (aspectRatio != null && aspectRatio > 0.0) {
  447. if (top != null) {
  448. areaTop = areaBottom - height;
  449. if (areaTop < 0.0) {
  450. areaTop = 0.0;
  451. areaBottom = height;
  452. }
  453. } else {
  454. areaBottom = areaTop + height;
  455. if (areaBottom > 1.0) {
  456. areaTop = 1.0 - height;
  457. areaBottom = 1.0;
  458. }
  459. }
  460. }
  461. // ensure to remain within bounds of the view
  462. if (areaLeft < 0.0) {
  463. areaLeft = 0.0;
  464. areaRight = _area.width;
  465. } else if (areaRight > 1.0) {
  466. areaLeft = 1.0 - _area.width;
  467. areaRight = 1.0;
  468. }
  469. if (areaTop < 0.0) {
  470. areaTop = 0.0;
  471. areaBottom = _area.height;
  472. } else if (areaBottom > 1.0) {
  473. areaTop = 1.0 - _area.height;
  474. areaBottom = 1.0;
  475. }
  476. setState(() {
  477. _area = Rect.fromLTRB(areaLeft, areaTop, areaRight, areaBottom);
  478. });
  479. }
  480. void _handleScaleUpdate(ScaleUpdateDetails details) {
  481. if (_action == _CropAction.none) {
  482. if (_handle == _CropHandleSide.none) {
  483. _action = pointers == 2 ? _CropAction.scaling : _CropAction.moving;
  484. } else {
  485. _action = _CropAction.cropping;
  486. }
  487. }
  488. if (_action == _CropAction.cropping) {
  489. final boundaries = _boundaries;
  490. if (boundaries == null) {
  491. return;
  492. }
  493. final delta = details.focalPoint - _lastFocalPoint;
  494. _lastFocalPoint = details.focalPoint;
  495. final dx = delta.dx / boundaries.width;
  496. final dy = delta.dy / boundaries.height;
  497. if (_handle == _CropHandleSide.topLeft) {
  498. _updateArea(left: dx, top: dy, cropHandleSide: _CropHandleSide.topLeft);
  499. } else if (_handle == _CropHandleSide.topRight) {
  500. _updateArea(
  501. top: dy, right: dx, cropHandleSide: _CropHandleSide.topRight);
  502. } else if (_handle == _CropHandleSide.bottomLeft) {
  503. _updateArea(
  504. left: dx, bottom: dy, cropHandleSide: _CropHandleSide.bottomLeft);
  505. } else if (_handle == _CropHandleSide.bottomRight) {
  506. _updateArea(
  507. right: dx, bottom: dy, cropHandleSide: _CropHandleSide.bottomRight);
  508. }
  509. } else if (_action == _CropAction.moving) {
  510. final image = _image;
  511. if (image == null) {
  512. return;
  513. }
  514. final delta = details.focalPoint - _lastFocalPoint;
  515. _lastFocalPoint = details.focalPoint;
  516. setState(() {
  517. _view = _view.translate(
  518. delta.dx / (image.width * _scale * _ratio),
  519. delta.dy / (image.height * _scale * _ratio),
  520. );
  521. });
  522. } else if (_action == _CropAction.scaling) {
  523. final image = _image;
  524. final boundaries = _boundaries;
  525. if (image == null || boundaries == null) {
  526. return;
  527. }
  528. setState(() {
  529. _scale = _startScale * details.scale;
  530. final dx = boundaries.width *
  531. (1.0 - details.scale) /
  532. (image.width * _scale * _ratio);
  533. final dy = boundaries.height *
  534. (1.0 - details.scale) /
  535. (image.height * _scale * _ratio);
  536. _view = Rect.fromLTWH(
  537. _startView.left + dx / 2,
  538. _startView.top + dy / 2,
  539. _startView.width,
  540. _startView.height,
  541. );
  542. });
  543. }
  544. }
  545. }
  546. class _CropPainter extends CustomPainter {
  547. final ui.Image? image;
  548. final Rect view;
  549. final double ratio;
  550. final Rect area;
  551. final double scale;
  552. final double active;
  553. _CropPainter({
  554. required this.image,
  555. required this.view,
  556. required this.ratio,
  557. required this.area,
  558. required this.scale,
  559. required this.active,
  560. });
  561. @override
  562. bool shouldRepaint(_CropPainter oldDelegate) {
  563. return oldDelegate.image != image ||
  564. oldDelegate.view != view ||
  565. oldDelegate.ratio != ratio ||
  566. oldDelegate.area != area ||
  567. oldDelegate.active != active ||
  568. oldDelegate.scale != scale;
  569. }
  570. @override
  571. void paint(Canvas canvas, Size size) {
  572. final rect = Rect.fromLTWH(
  573. _kCropHandleSize / 2,
  574. _kCropHandleSize / 2,
  575. size.width - _kCropHandleSize,
  576. size.height - _kCropHandleSize,
  577. );
  578. canvas.save();
  579. canvas.translate(rect.left, rect.top);
  580. final paint = Paint()..isAntiAlias = false;
  581. final image = this.image;
  582. if (image != null) {
  583. final src = Rect.fromLTWH(
  584. 0.0,
  585. 0.0,
  586. image.width.toDouble(),
  587. image.height.toDouble(),
  588. );
  589. final dst = Rect.fromLTWH(
  590. view.left * image.width * scale * ratio,
  591. view.top * image.height * scale * ratio,
  592. image.width * scale * ratio,
  593. image.height * scale * ratio,
  594. );
  595. canvas.save();
  596. canvas.clipRect(Rect.fromLTWH(0.0, 0.0, rect.width, rect.height));
  597. canvas.drawImageRect(image, src, dst, paint);
  598. canvas.restore();
  599. }
  600. paint.color = Color.fromRGBO(
  601. 0x0,
  602. 0x0,
  603. 0x0,
  604. _kCropOverlayActiveOpacity * active +
  605. _kCropOverlayInactiveOpacity * (1.0 - active));
  606. final boundaries = Rect.fromLTWH(
  607. rect.width * area.left,
  608. rect.height * area.top,
  609. rect.width * area.width,
  610. rect.height * area.height,
  611. );
  612. canvas.drawRect(Rect.fromLTRB(0.0, 0.0, rect.width, boundaries.top), paint);
  613. canvas.drawRect(
  614. Rect.fromLTRB(0.0, boundaries.bottom, rect.width, rect.height), paint);
  615. canvas.drawRect(
  616. Rect.fromLTRB(0.0, boundaries.top, boundaries.left, boundaries.bottom),
  617. paint);
  618. canvas.drawRect(
  619. Rect.fromLTRB(
  620. boundaries.right, boundaries.top, rect.width, boundaries.bottom),
  621. paint);
  622. if (boundaries.isEmpty == false) {
  623. _drawGrid(canvas, boundaries);
  624. _drawHandles(canvas, boundaries);
  625. }
  626. canvas.restore();
  627. }
  628. void _drawHandles(Canvas canvas, Rect boundaries) {
  629. final paint = Paint()
  630. ..isAntiAlias = true
  631. ..color = _kCropHandleColor;
  632. canvas.drawOval(
  633. Rect.fromLTWH(
  634. boundaries.left - _kCropHandleSize / 2,
  635. boundaries.top - _kCropHandleSize / 2,
  636. _kCropHandleSize,
  637. _kCropHandleSize,
  638. ),
  639. paint,
  640. );
  641. canvas.drawOval(
  642. Rect.fromLTWH(
  643. boundaries.right - _kCropHandleSize / 2,
  644. boundaries.top - _kCropHandleSize / 2,
  645. _kCropHandleSize,
  646. _kCropHandleSize,
  647. ),
  648. paint,
  649. );
  650. canvas.drawOval(
  651. Rect.fromLTWH(
  652. boundaries.right - _kCropHandleSize / 2,
  653. boundaries.bottom - _kCropHandleSize / 2,
  654. _kCropHandleSize,
  655. _kCropHandleSize,
  656. ),
  657. paint,
  658. );
  659. canvas.drawOval(
  660. Rect.fromLTWH(
  661. boundaries.left - _kCropHandleSize / 2,
  662. boundaries.bottom - _kCropHandleSize / 2,
  663. _kCropHandleSize,
  664. _kCropHandleSize,
  665. ),
  666. paint,
  667. );
  668. }
  669. void _drawGrid(Canvas canvas, Rect boundaries) {
  670. if (active == 0.0) return;
  671. final paint = Paint()
  672. ..isAntiAlias = false
  673. ..color = _kCropGridColor.withOpacity(_kCropGridColor.opacity * active)
  674. ..style = PaintingStyle.stroke
  675. ..strokeWidth = 1.0;
  676. final path = Path()
  677. ..moveTo(boundaries.left, boundaries.top)
  678. ..lineTo(boundaries.right, boundaries.top)
  679. ..lineTo(boundaries.right, boundaries.bottom)
  680. ..lineTo(boundaries.left, boundaries.bottom)
  681. ..lineTo(boundaries.left, boundaries.top);
  682. for (var column = 1; column < _kCropGridColumnCount; column++) {
  683. path
  684. ..moveTo(
  685. boundaries.left + column * boundaries.width / _kCropGridColumnCount,
  686. boundaries.top)
  687. ..lineTo(
  688. boundaries.left + column * boundaries.width / _kCropGridColumnCount,
  689. boundaries.bottom);
  690. }
  691. for (var row = 1; row < _kCropGridRowCount; row++) {
  692. path
  693. ..moveTo(boundaries.left,
  694. boundaries.top + row * boundaries.height / _kCropGridRowCount)
  695. ..lineTo(boundaries.right,
  696. boundaries.top + row * boundaries.height / _kCropGridRowCount);
  697. }
  698. canvas.drawPath(path, paint);
  699. }
  700. }