editor: keep area on pan to edge
This commit is contained in:
parent
b89db3bc9b
commit
95e0272afb
4 changed files with 51 additions and 46 deletions
|
@ -117,13 +117,13 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onActionChanged() => _updateImageMargin();
|
void _onActionChanged() {
|
||||||
|
switch(_actionNotifier.value) {
|
||||||
void _updateImageMargin() {
|
case EditorAction.transform:
|
||||||
if (_actionNotifier.value == EditorAction.transform) {
|
_transformController.reset();
|
||||||
_marginNotifier.value = Cropper.imageMargin;
|
_marginNotifier.value = Cropper.imageMargin;
|
||||||
} else {
|
default:
|
||||||
_marginNotifier.value = EdgeInsets.zero;
|
_marginNotifier.value = EdgeInsets.zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ class _TransformControlPanelState extends State<TransformControlPanel> with Tick
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
child: StreamBuilder<Transformation>(
|
child: StreamBuilder<Transformation>(
|
||||||
stream: transformController.transformationStream,
|
stream: transformController.transformationStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, _) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(AIcons.apply),
|
icon: const Icon(AIcons.apply),
|
||||||
onPressed: transformController.modified ? () => widget.onApply(transformController.transformation) : null,
|
onPressed: transformController.modified ? () => widget.onApply(transformController.transformation) : null,
|
||||||
|
@ -122,17 +122,17 @@ class CropControlPanel extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final aspectRatioNotifier = context.select<TransformController, ValueNotifier<CropAspectRatio>>((v) => v.aspectRatioNotifier);
|
final controller = context.watch<TransformController>();
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final ratio = CropAspectRatio.values[index];
|
final ratio = CropAspectRatio.values[index];
|
||||||
void setAspectRatio() => aspectRatioNotifier.value = ratio;
|
void setAspectRatio() => controller.setAspectRatio(ratio);
|
||||||
return CaptionedButton(
|
return CaptionedButton(
|
||||||
iconButtonBuilder: (context, focusNode) {
|
iconButtonBuilder: (context, focusNode) {
|
||||||
return ValueListenableBuilder<CropAspectRatio>(
|
return ValueListenableBuilder<CropAspectRatio>(
|
||||||
valueListenable: aspectRatioNotifier,
|
valueListenable: controller.aspectRatioNotifier,
|
||||||
builder: (context, selectedRatio, child) {
|
builder: (context, selectedRatio, child) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
color: ratio == selectedRatio ? Theme.of(context).colorScheme.primary : null,
|
color: ratio == selectedRatio ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
@ -166,17 +166,21 @@ class RotationControlPanel extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<Transformation>(
|
child: StreamBuilder<Transformation>(
|
||||||
stream: controller.transformationStream,
|
stream: controller.transformationStream,
|
||||||
builder: (context, snapshot) {
|
builder: (context, _) {
|
||||||
final transformation = snapshot.data ?? Transformation.zero;
|
final degrees = controller.transformation.straightenDegrees;
|
||||||
return Slider(
|
return SliderTheme(
|
||||||
value: transformation.straightenDegrees,
|
data: SliderTheme.of(context).copyWith(
|
||||||
min: TransformController.straightenDegreesMin,
|
showValueIndicator: ShowValueIndicator.always,
|
||||||
max: TransformController.straightenDegreesMax,
|
),
|
||||||
divisions: 18,
|
child: Slider(
|
||||||
onChangeStart: (v) => controller.activity = TransformActivity.straighten,
|
value: degrees,
|
||||||
onChangeEnd: (v) => controller.activity = TransformActivity.none,
|
min: TransformController.straightenDegreesMin,
|
||||||
label: angleFormatter.format(transformation.straightenDegrees),
|
max: TransformController.straightenDegreesMax,
|
||||||
onChanged: (v) => controller.straightenDegrees = v,
|
onChangeStart: (v) => controller.activity = TransformActivity.straighten,
|
||||||
|
onChangeEnd: (v) => controller.activity = TransformActivity.none,
|
||||||
|
label: angleFormatter.format(degrees),
|
||||||
|
onChanged: (v) => controller.straightenDegrees = v,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -57,6 +57,7 @@ class TransformController {
|
||||||
region: CropRegion.fromRect(Offset.zero & displaySize),
|
region: CropRegion.fromRect(Offset.zero & displaySize),
|
||||||
);
|
);
|
||||||
_transformationStreamController.add(_transformation);
|
_transformationStreamController.add(_transformation);
|
||||||
|
setAspectRatio(CropAspectRatio.free);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flipHorizontally() {
|
void flipHorizontally() {
|
||||||
|
@ -93,6 +94,8 @@ class TransformController {
|
||||||
_activityStreamController.add(_activity);
|
_activityStreamController.add(_activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAspectRatio(CropAspectRatio ratio) => aspectRatioNotifier.value = ratio;
|
||||||
|
|
||||||
void _onAspectRatioChanged() {
|
void _onAspectRatioChanged() {
|
||||||
// TODO TLAD [crop] apply
|
// TODO TLAD [crop] apply
|
||||||
}
|
}
|
||||||
|
|
|
@ -437,20 +437,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onViewStateChanged(MagnifierState state) {
|
void _onViewStateChanged(MagnifierState state) {
|
||||||
final currentOutline = _outlineNotifier.value;
|
|
||||||
|
|
||||||
// TODO TLAD [crop] use other strat
|
|
||||||
|
|
||||||
// switch (state.source) {
|
|
||||||
// case ChangeSource.internal:
|
|
||||||
// case ChangeSource.animation:
|
|
||||||
// case ChangeSource.gesture:
|
|
||||||
// }
|
|
||||||
|
|
||||||
switch (transformController.activity) {
|
switch (transformController.activity) {
|
||||||
case TransformActivity.none:
|
case TransformActivity.none:
|
||||||
|
break;
|
||||||
case TransformActivity.straighten:
|
case TransformActivity.straighten:
|
||||||
case TransformActivity.pan:
|
case TransformActivity.pan:
|
||||||
|
final currentOutline = _outlineNotifier.value;
|
||||||
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
|
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
|
||||||
case TransformActivity.resize:
|
case TransformActivity.resize:
|
||||||
break;
|
break;
|
||||||
|
@ -488,8 +480,26 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
if (targetOutline.isEmpty || viewState == null || viewportSize == null) return;
|
if (targetOutline.isEmpty || viewState == null || viewportSize == null) return;
|
||||||
|
|
||||||
// ensure outline is within content
|
// ensure outline is within content
|
||||||
final targetRegion = _regionFromOutline(viewState, targetOutline);
|
var targetRegion = _regionFromOutline(viewState, targetOutline);
|
||||||
var newOutline = _containedOutlineFromRegion(viewState, targetRegion);
|
var newOutline = _containedOutlineFromRegion(viewState, targetRegion);
|
||||||
|
final outlineWidthDelta = targetOutline.width - newOutline.width;
|
||||||
|
final outlineHeightDelta = targetOutline.height - newOutline.height;
|
||||||
|
if (outlineWidthDelta > precisionErrorTolerance || outlineHeightDelta > precisionErrorTolerance) {
|
||||||
|
// keep outline area if possible, otherwise trim
|
||||||
|
final regionToOutlineMatrix = _getRegionToOutlineMatrix(viewState);
|
||||||
|
final rect = Offset.zero & viewState.contentSize!;
|
||||||
|
final edgeRegionCorners = targetRegion.corners.where((v) => v.dx == rect.left || v.dx == rect.right || v.dy == rect.top || v.dy == rect.bottom).toSet();
|
||||||
|
final edgeOutlineCorners = edgeRegionCorners.map(regionToOutlineMatrix.transformOffset).toSet();
|
||||||
|
if (edgeOutlineCorners.isNotEmpty) {
|
||||||
|
final direction = edgeOutlineCorners.map((v) => newOutline.center - v).reduce((prev, v) => prev + v);
|
||||||
|
final movedOutline = targetOutline.shift(Offset(
|
||||||
|
outlineWidthDelta * direction.dx.sign,
|
||||||
|
outlineHeightDelta * direction.dy.sign,
|
||||||
|
));
|
||||||
|
targetRegion = _regionFromOutline(viewState, movedOutline);
|
||||||
|
newOutline = _containedOutlineFromRegion(viewState, targetRegion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ensure outline is large enough to be handled
|
// ensure outline is large enough to be handled
|
||||||
newOutline = Rect.fromLTWH(
|
newOutline = Rect.fromLTWH(
|
||||||
|
@ -499,25 +509,13 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
max(newOutline.height, minDimension),
|
max(newOutline.height, minDimension),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ensure outline is within viewport
|
|
||||||
newOutline = Rect.fromLTRB(
|
|
||||||
max(newOutline.left, 0),
|
|
||||||
max(newOutline.top, 0),
|
|
||||||
min(newOutline.right, viewportSize.width),
|
|
||||||
min(newOutline.bottom, viewportSize.height),
|
|
||||||
);
|
|
||||||
|
|
||||||
final oldOutline = _outlineNotifier.value;
|
|
||||||
_outlineNotifier.value = newOutline;
|
_outlineNotifier.value = newOutline;
|
||||||
switch (transformController.activity) {
|
switch (transformController.activity) {
|
||||||
case TransformActivity.none:
|
|
||||||
if (oldOutline.isEmpty) {
|
|
||||||
_updateCropRegion();
|
|
||||||
}
|
|
||||||
case TransformActivity.pan:
|
case TransformActivity.pan:
|
||||||
case TransformActivity.resize:
|
case TransformActivity.resize:
|
||||||
_updateCropRegion();
|
_updateCropRegion();
|
||||||
break;
|
break;
|
||||||
|
case TransformActivity.none:
|
||||||
case TransformActivity.straighten:
|
case TransformActivity.straighten:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue