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,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;
} }
} }

View file

@ -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,
),
); );
}, },
), ),

View file

@ -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
} }

View file

@ -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;
} }