editor: keep area on pan to edge

This commit is contained in:
Thibault Deckers 2025-02-12 19:25:44 +01:00
parent b89db3bc9b
commit 95e0272afb
4 changed files with 51 additions and 46 deletions

View file

@ -117,12 +117,12 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
);
}
void _onActionChanged() => _updateImageMargin();
void _updateImageMargin() {
if (_actionNotifier.value == EditorAction.transform) {
void _onActionChanged() {
switch(_actionNotifier.value) {
case EditorAction.transform:
_transformController.reset();
_marginNotifier.value = Cropper.imageMargin;
} else {
default:
_marginNotifier.value = EdgeInsets.zero;
}
}

View file

@ -97,7 +97,7 @@ class _TransformControlPanelState extends State<TransformControlPanel> with Tick
OverlayButton(
child: StreamBuilder<Transformation>(
stream: transformController.transformationStream,
builder: (context, snapshot) {
builder: (context, _) {
return IconButton(
icon: const Icon(AIcons.apply),
onPressed: transformController.modified ? () => widget.onApply(transformController.transformation) : null,
@ -122,17 +122,17 @@ class CropControlPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final aspectRatioNotifier = context.select<TransformController, ValueNotifier<CropAspectRatio>>((v) => v.aspectRatioNotifier);
final controller = context.watch<TransformController>();
return ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemBuilder: (context, index) {
final ratio = CropAspectRatio.values[index];
void setAspectRatio() => aspectRatioNotifier.value = ratio;
void setAspectRatio() => controller.setAspectRatio(ratio);
return CaptionedButton(
iconButtonBuilder: (context, focusNode) {
return ValueListenableBuilder<CropAspectRatio>(
valueListenable: aspectRatioNotifier,
valueListenable: controller.aspectRatioNotifier,
builder: (context, selectedRatio, child) {
return IconButton(
color: ratio == selectedRatio ? Theme.of(context).colorScheme.primary : null,
@ -166,17 +166,21 @@ class RotationControlPanel extends StatelessWidget {
Expanded(
child: StreamBuilder<Transformation>(
stream: controller.transformationStream,
builder: (context, snapshot) {
final transformation = snapshot.data ?? Transformation.zero;
return Slider(
value: transformation.straightenDegrees,
builder: (context, _) {
final degrees = controller.transformation.straightenDegrees;
return SliderTheme(
data: SliderTheme.of(context).copyWith(
showValueIndicator: ShowValueIndicator.always,
),
child: Slider(
value: degrees,
min: TransformController.straightenDegreesMin,
max: TransformController.straightenDegreesMax,
divisions: 18,
onChangeStart: (v) => controller.activity = TransformActivity.straighten,
onChangeEnd: (v) => controller.activity = TransformActivity.none,
label: angleFormatter.format(transformation.straightenDegrees),
label: angleFormatter.format(degrees),
onChanged: (v) => controller.straightenDegrees = v,
),
);
},
),

View file

@ -57,6 +57,7 @@ class TransformController {
region: CropRegion.fromRect(Offset.zero & displaySize),
);
_transformationStreamController.add(_transformation);
setAspectRatio(CropAspectRatio.free);
}
void flipHorizontally() {
@ -93,6 +94,8 @@ class TransformController {
_activityStreamController.add(_activity);
}
void setAspectRatio(CropAspectRatio ratio) => aspectRatioNotifier.value = ratio;
void _onAspectRatioChanged() {
// TODO TLAD [crop] apply
}

View file

@ -437,20 +437,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
}
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) {
case TransformActivity.none:
break;
case TransformActivity.straighten:
case TransformActivity.pan:
final currentOutline = _outlineNotifier.value;
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
case TransformActivity.resize:
break;
@ -488,8 +480,26 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
if (targetOutline.isEmpty || viewState == null || viewportSize == null) return;
// ensure outline is within content
final targetRegion = _regionFromOutline(viewState, targetOutline);
var targetRegion = _regionFromOutline(viewState, targetOutline);
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
newOutline = Rect.fromLTWH(
@ -499,25 +509,13 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
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;
switch (transformController.activity) {
case TransformActivity.none:
if (oldOutline.isEmpty) {
_updateCropRegion();
}
case TransformActivity.pan:
case TransformActivity.resize:
_updateCropRegion();
break;
case TransformActivity.none:
case TransformActivity.straighten:
break;
}