magnifier: scale boundaries padding;

editor: pan fixes
This commit is contained in:
Thibault Deckers 2025-02-08 19:32:35 +01:00
parent 62952de907
commit d11bd21d89
13 changed files with 129 additions and 151 deletions

View file

@ -11,7 +11,7 @@ void main() => mainCommon(
debugIntentData: {
IntentDataKeys.action: IntentActions.edit,
IntentDataKeys.mimeType: 'image/*',
IntentDataKeys.uri: 'content://media/external/images/media/183128',
// IntentDataKeys.uri: 'content://media/external/images/media/183534',
IntentDataKeys.uri: 'content://media/external/images/media/1000064996', // landscape
// IntentDataKeys.uri: 'content://media/external/images/media/1000064754', // portrait
},
);

View file

@ -7,6 +7,7 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/editor/transform/control_panel.dart';
import 'package:aves/widgets/editor/transform/controller.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -125,6 +126,7 @@ class EditorControlPanel extends StatelessWidget {
void _cancelAction(BuildContext context) {
actionNotifier.value = null;
context.read<AvesMagnifierController>().reset();
context.read<TransformController>().reset();
}

View file

@ -30,7 +30,7 @@ class ImageEditorPage extends StatefulWidget {
class _ImageEditorPageState extends State<ImageEditorPage> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
final ValueNotifier<EdgeInsets> _paddingNotifier = ValueNotifier(EdgeInsets.zero);
final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero);
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
final AvesMagnifierController _magnifierController = AvesMagnifierController();
late final TransformController _transformController;
@ -49,7 +49,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
..forEach((sub) => sub.cancel())
..clear();
_actionNotifier.dispose();
_paddingNotifier.dispose();
_marginNotifier.dispose();
_viewStateNotifier.dispose();
_magnifierController.dispose();
_transformController.dispose();
@ -59,8 +59,11 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Provider<TransformController>.value(
value: _transformController,
body: MultiProvider(
providers: [
Provider<AvesMagnifierController>.value(value: _magnifierController),
Provider<TransformController>.value(value: _transformController),
],
child: SafeArea(
child: Column(
children: [
@ -72,7 +75,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
magnifierController: _magnifierController,
transformController: _transformController,
actionNotifier: _actionNotifier,
paddingNotifier: _paddingNotifier,
marginNotifier: _marginNotifier,
viewStateNotifier: _viewStateNotifier,
entry: widget.entry,
),
@ -91,7 +94,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
return Cropper(
magnifierController: _magnifierController,
transformController: _transformController,
paddingNotifier: _paddingNotifier,
marginNotifier: _marginNotifier,
);
case null:
return const SizedBox();
@ -101,6 +104,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
],
),
),
const Divider(height: 0),
EditorControlPanel(
entry: widget.entry,
actionNotifier: _actionNotifier,
@ -113,13 +117,13 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
);
}
void _onActionChanged() => _updateImagePadding();
void _onActionChanged() => _updateImageMargin();
void _updateImagePadding() {
void _updateImageMargin() {
if (_actionNotifier.value == EditorAction.transform) {
_paddingNotifier.value = Cropper.imagePadding;
_marginNotifier.value = Cropper.imageMargin;
} else {
_paddingNotifier.value = EdgeInsets.zero;
_marginNotifier.value = EdgeInsets.zero;
}
}

View file

@ -17,7 +17,7 @@ class EditorImage extends StatefulWidget {
final AvesMagnifierController magnifierController;
final TransformController transformController;
final ValueNotifier<EditorAction?> actionNotifier;
final ValueNotifier<EdgeInsets> paddingNotifier;
final ValueNotifier<EdgeInsets> marginNotifier;
final ValueNotifier<ViewState> viewStateNotifier;
final AvesEntry entry;
@ -26,7 +26,7 @@ class EditorImage extends StatefulWidget {
required this.magnifierController,
required this.transformController,
required this.actionNotifier,
required this.paddingNotifier,
required this.marginNotifier,
required this.viewStateNotifier,
required this.entry,
});
@ -96,8 +96,8 @@ class _EditorImageState extends State<EditorImage> {
final canvasSize = MatrixUtils.transformRect(imageToUserMatrix, Offset.zero & mediaSize).size;
return ValueListenableBuilder<EdgeInsets>(
valueListenable: widget.paddingNotifier,
builder: (context, padding, child) {
valueListenable: widget.marginNotifier,
builder: (context, margin, child) {
return Transform(
alignment: Alignment.center,
transform: imageToUserMatrix,
@ -106,12 +106,12 @@ class _EditorImageState extends State<EditorImage> {
builder: (context, action, child) {
return LayoutBuilder(
builder: (context, constraints) {
final viewportSize = padding.deflateSize(constraints.biggest);
final viewportSize = margin.deflateSize(constraints.biggest);
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
return AvesMagnifier(
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
controller: widget.magnifierController,
viewportPadding: padding,
viewportPadding: margin,
contentSize: mediaSize,
allowOriginalScaleBeyondRange: false,
allowGestureScaleBeyondRange: false,

View file

@ -81,8 +81,10 @@ class _TransformControlPanelState extends State<TransformControlPanel> with Tick
const SizedBox(height: padding),
Row(
children: [
const OverlayButton(
child: BackButton(),
OverlayButton(
child: BackButton(
onPressed: widget.onCancel,
),
),
Expanded(
child: TabBar(

View file

@ -1,25 +1,17 @@
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
@immutable
class CropRegion extends Equatable {
// region corners in image pixel coordinates
final Offset topLeft, topRight, bottomRight, bottomLeft;
List<Offset> get corners => [topLeft, topRight, bottomRight, bottomLeft];
Offset get center => (topLeft + bottomRight) / 2;
Rect get outsideRect {
final xMin = corners.map((v) => v.dx).min;
final xMax = corners.map((v) => v.dx).max;
final yMin = corners.map((v) => v.dy).min;
final yMax = corners.map((v) => v.dy).max;
return Rect.fromPoints(Offset(xMin, yMin), Offset(xMax, yMax));
}
@override
List<Object?> get props => [topLeft, topRight, bottomRight, bottomLeft];
@ -30,7 +22,7 @@ class CropRegion extends Equatable {
required this.bottomLeft,
});
static const CropRegion zero = CropRegion(
static const zero = CropRegion(
topLeft: Offset.zero,
topRight: Offset.zero,
bottomRight: Offset.zero,

View file

@ -21,16 +21,16 @@ import 'package:provider/provider.dart';
class Cropper extends StatefulWidget {
final AvesMagnifierController magnifierController;
final TransformController transformController;
final ValueNotifier<EdgeInsets> paddingNotifier;
final ValueNotifier<EdgeInsets> marginNotifier;
static const double handleDimension = kMinInteractiveDimension;
static const EdgeInsets imagePadding = EdgeInsets.all(kMinInteractiveDimension);
static const EdgeInsets imageMargin = EdgeInsets.all(kMinInteractiveDimension);
const Cropper({
super.key,
required this.magnifierController,
required this.transformController,
required this.paddingNotifier,
required this.marginNotifier,
});
@override
@ -39,7 +39,6 @@ class Cropper extends StatefulWidget {
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<Size> _viewportSizeNotifier = ValueNotifier(Size.zero);
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
late AnimationController _gridAnimationController;
@ -61,8 +60,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
final initialRegion = transformation.region;
_viewportSizeNotifier.addListener(() => _initOutline(initialRegion));
_gridAnimationController = AnimationController(
duration: context.read<DurationsData>().viewerOverlayAnimation,
vsync: this,
@ -72,7 +69,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
curve: Curves.easeOutQuad,
);
_registerWidget(widget);
_initOutline(initialRegion);
}
@override
@ -84,7 +80,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
@override
void dispose() {
_viewportSizeNotifier.dispose();
_outlineNotifier.dispose();
_gridDivisionNotifier.dispose();
_gridOpacity.dispose();
@ -95,7 +90,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
void _registerWidget(Cropper widget) {
_subscriptions.add(widget.magnifierController.stateStream.listen(_onViewStateChanged));
_subscriptions.add(widget.magnifierController.scaleBoundariesStream.listen(_onViewBoundariesChanged));
_subscriptions.add(widget.magnifierController.scaleBoundariesStream.map((v) => v.viewportSize).listen(_onViewportSizeChanged));
_subscriptions.add(widget.transformController.eventStream.listen(_onTransformEvent));
_subscriptions.add(widget.transformController.transformationStream.map((v) => v.orientation).distinct().listen(_onOrientationChanged));
_subscriptions.add(widget.transformController.transformationStream.map((v) => v.straightenDegrees).distinct().listen(_onStraightenDegreesChanged));
@ -113,14 +108,14 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
Widget build(BuildContext context) {
return Positioned.fill(
child: ValueListenableBuilder<EdgeInsets>(
valueListenable: widget.paddingNotifier,
builder: (context, padding, child) {
valueListenable: widget.marginNotifier,
builder: (context, margin, child) {
return ValueListenableBuilder<Rect>(
valueListenable: _outlineNotifier,
builder: (context, outline, child) {
if (outline.isEmpty) return const SizedBox();
final outlineVisualRect = outline.translate(padding.left, padding.top);
final outlineVisualRect = outline.translate(margin.left, margin.top);
return Stack(
children: [
Positioned.fill(
@ -155,35 +150,35 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
),
),
_buildVertexHandle(
padding: padding,
margin: margin,
getPosition: () => outline.topLeft,
setPosition: (v) => _handleOutline(
topLeft: Offset(min(outline.right - minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
),
),
_buildVertexHandle(
padding: padding,
margin: margin,
getPosition: () => outline.topRight,
setPosition: (v) => _handleOutline(
topRight: Offset(max(outline.left + minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
),
),
_buildVertexHandle(
padding: padding,
margin: margin,
getPosition: () => outline.bottomRight,
setPosition: (v) => _handleOutline(
bottomRight: Offset(max(outline.left + minDimension, v.dx), max(outline.top + minDimension, v.dy)),
),
),
_buildVertexHandle(
padding: padding,
margin: margin,
getPosition: () => outline.bottomLeft,
setPosition: (v) => _handleOutline(
bottomLeft: Offset(min(outline.right - minDimension, v.dx), max(outline.top + minDimension, v.dy)),
),
),
_buildEdgeHandle(
padding: padding,
margin: margin,
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.topLeft),
setEdge: (v) {
final left = min(outline.right - minDimension, v.left);
@ -194,7 +189,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
},
),
_buildEdgeHandle(
padding: padding,
margin: margin,
getEdge: () => Rect.fromPoints(outline.topLeft, outline.topRight),
setEdge: (v) {
final top = min(outline.bottom - minDimension, v.top);
@ -205,7 +200,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
},
),
_buildEdgeHandle(
padding: padding,
margin: margin,
getEdge: () => Rect.fromPoints(outline.bottomRight, outline.topRight),
setEdge: (v) {
final right = max(outline.left + minDimension, v.right);
@ -216,7 +211,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
},
),
_buildEdgeHandle(
padding: padding,
margin: margin,
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.bottomRight),
setEdge: (v) {
final bottom = max(outline.top + minDimension, v.bottom);
@ -341,12 +336,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
}
VertexHandle _buildVertexHandle({
required EdgeInsets padding,
required EdgeInsets margin,
required ValueGetter<Offset> getPosition,
required ValueSetter<Offset> setPosition,
}) {
return VertexHandle(
padding: padding,
margin: margin,
getPosition: getPosition,
setPosition: setPosition,
onDragStart: _onDragStart,
@ -355,12 +350,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
}
EdgeHandle _buildEdgeHandle({
required EdgeInsets padding,
required EdgeInsets margin,
required ValueGetter<Rect> getEdge,
required ValueSetter<Rect> setEdge,
}) {
return EdgeHandle(
padding: padding,
margin: margin,
getEdge: getEdge,
setEdge: setEdge,
onDragStart: _onDragStart,
@ -392,11 +387,18 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
_setOutline(_regionToContainedOutline(nextState, region));
}
ViewState _viewStateForContainedRegion(ScaleBoundaries boundaries, CropRegion region) {
final regionSize = MatrixUtils.transformRect(transformation.matrix, region.outsideRect).size;
final nextScale = boundaries.clampScale(ScaleLevel.scaleForContained(boundaries.viewportSize, regionSize));
ViewState _viewStateForContainedRegion(ScaleBoundaries boundaries, CropRegion imageRegion) {
final matrix = transformation.matrix;
final displayRegion = imageRegion.corners.map(matrix.transformOffset).toSet();
final xMin = displayRegion.map((v) => v.dx).min;
final xMax = displayRegion.map((v) => v.dx).max;
final yMin = displayRegion.map((v) => v.dy).min;
final yMax = displayRegion.map((v) => v.dy).max;
final displayRegionSize = Size(xMax - xMin, yMax - yMin);
final nextScale = boundaries.clampScale(ScaleLevel.scaleForContained(boundaries.viewportSize, displayRegionSize));
final nextPosition = boundaries.clampPosition(
position: boundaries.contentToStatePosition(nextScale, region.center),
position: boundaries.contentToStatePosition(nextScale, imageRegion.center),
scale: nextScale,
);
return ViewState(
@ -447,19 +449,30 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
void _onViewStateChanged(MagnifierState state) {
final currentOutline = _outlineNotifier.value;
switch (state.source) {
case ChangeSource.internal:
case ChangeSource.animation:
_setOutline(currentOutline);
case ChangeSource.gesture:
// switch (state.source) {
// case ChangeSource.internal:
// case ChangeSource.animation:
// _setOutline(currentOutline);
// case ChangeSource.gesture:
// TODO TLAD [crop] use other strat
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
_updateCropRegion();
}
// }
}
void _onViewBoundariesChanged(ScaleBoundaries scaleBoundaries) {
_viewportSizeNotifier.value = scaleBoundaries.viewportSize;
void _onViewportSizeChanged(Size viewportSize) {
_initOutline(transformation.region);
final boundaries = magnifierController.scaleBoundaries;
if (boundaries != null) {
final double xPadding = (viewportSize.width - minDimension) / 2;
final double yPadding = (viewportSize.height - minDimension) / 2;
magnifierController.setScaleBoundaries(
boundaries.copyWith(
padding: EdgeInsets.symmetric(horizontal: xPadding, vertical: yPadding),
),
);
}
}
ViewState? _getViewState() {

View file

@ -2,14 +2,14 @@ import 'package:aves/widgets/editor/transform/cropper.dart';
import 'package:flutter/material.dart';
class VertexHandle extends StatefulWidget {
final EdgeInsets padding;
final EdgeInsets margin;
final ValueGetter<Offset> getPosition;
final ValueSetter<Offset> setPosition;
final VoidCallback onDragStart, onDragEnd;
const VertexHandle({
super.key,
required this.padding,
required this.margin,
required this.getPosition,
required this.setPosition,
required this.onDragStart,
@ -26,13 +26,13 @@ class _VertexHandleState extends State<VertexHandle> {
static const double _handleDim = Cropper.handleDimension;
EdgeInsets get padding => widget.padding;
EdgeInsets get margin => widget.margin;
@override
Widget build(BuildContext context) {
return Positioned.fromRect(
rect: Rect.fromCenter(
center: widget.getPosition().translate(padding.left, padding.right),
center: widget.getPosition().translate(margin.left, margin.right),
width: _handleDim,
height: _handleDim,
),
@ -58,14 +58,14 @@ class _VertexHandleState extends State<VertexHandle> {
}
class EdgeHandle extends StatefulWidget {
final EdgeInsets padding;
final EdgeInsets margin;
final ValueGetter<Rect> getEdge;
final ValueSetter<Rect> setEdge;
final VoidCallback onDragStart, onDragEnd;
const EdgeHandle({
super.key,
required this.padding,
required this.margin,
required this.getEdge,
required this.setEdge,
required this.onDragStart,
@ -82,7 +82,7 @@ class _EdgeHandleState extends State<EdgeHandle> {
static const double _handleDim = Cropper.handleDimension;
EdgeInsets get padding => widget.padding;
EdgeInsets get margin => widget.margin;
@override
Widget build(BuildContext context) {
@ -94,7 +94,7 @@ class _EdgeHandleState extends State<EdgeHandle> {
// vertical edge
edge = Rect.fromLTWH(edge.left - _handleDim / 2, edge.top + _handleDim / 2, _handleDim, edge.height - _handleDim);
}
edge = edge.translate(padding.left, padding.right);
edge = edge.translate(margin.left, margin.right);
return Positioned.fromRect(
rect: edge,

View file

@ -32,7 +32,7 @@ class AvesMagnifierController {
initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source);
previousState = initial;
_currentState = initial;
_setState(initial);
reset();
const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: source);
previousScaleState = _initialScaleState;
@ -70,6 +70,8 @@ class AvesMagnifierController {
bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut;
void reset() => _setState(initial);
void update({
Offset? position,
double? scale,

View file

@ -21,14 +21,12 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale;
/// Mark if scale need recalculation, useful for scale boundaries changes.
bool markNeedsScaleRecalc = true;
final List<StreamSubscription> _subscriptions = [];
void registerDelegate(AvesMagnifier widget) {
_subscriptions.add(widget.controller.stateStream.listen(_onMagnifierStateChanged));
_subscriptions.add(widget.controller.scaleStateChangeStream.listen(_onScaleStateChanged));
_subscriptions.add(widget.controller.scaleBoundariesStream.listen(_onScaleBoundariesChanged));
}
void unregisterDelegate(AvesMagnifier oldWidget) {
@ -38,12 +36,16 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
..clear();
}
void _onScaleBoundariesChanged(ScaleBoundaries boundaries) {
initScale();
}
void _onScaleStateChanged(ScaleStateChange scaleStateChange) {
if (scaleStateChange.source == ChangeSource.internal) return;
if (!controller.hasScaleSateChanged) return;
if (_animateScale == null || controller.isZooming) {
controller.update(scale: scale, source: scaleStateChange.source);
controller.update(scale: controller.scale, source: scaleStateChange.source);
return;
}
@ -68,34 +70,24 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
void _onMagnifierStateChanged(MagnifierState state) {
final boundaries = scaleBoundaries;
if (boundaries == null) return;
final currentScale = controller.scale;
if (boundaries == null || currentScale == null) return;
controller.update(position: boundaries.clampPosition(position: position, scale: scale!), source: state.source);
if (controller.scale == controller.previousState.scale) return;
controller.update(position: boundaries.clampPosition(position: position, scale: currentScale), source: state.source);
final newScale = controller.scale;
if (newScale == null || newScale == currentScale) return;
if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) return;
final newScaleState = (scale! > boundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
final newScaleState = (newScale > boundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
controller.setScaleState(newScaleState, state.source);
}
Offset get position => controller.position;
double? recalcScale() {
void initScale() {
final scaleState = controller.scaleState.state;
final newScale = controller.getScaleForScaleState(scaleState);
markNeedsScaleRecalc = false;
setScale(newScale, ChangeSource.internal);
return newScale;
}
double? get scale {
final scaleState = controller.scaleState.state;
final needsRecalc = markNeedsScaleRecalc && !(scaleState == ScaleState.zoomedIn || scaleState == ScaleState.zoomedOut);
final scaleExistsOnController = controller.scale != null;
if (needsRecalc || !scaleExistsOnController) {
return recalcScale();
}
return controller.scale;
}
void setScale(double? scale, ChangeSource source) => controller.update(scale: scale, source: source);
@ -105,7 +97,7 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
if (boundaries == null) return;
var newScaleState = ScaleState.initial;
if (scale != boundaries.initialScale) {
if (controller.scale != boundaries.initialScale) {
newScaleState = (newScale > boundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
}
controller.setScaleState(newScaleState, source);

View file

@ -97,8 +97,6 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
late AnimationController _positionAnimationController;
late Animation<Offset> _positionAnimation;
ScaleBoundaries? cachedScaleBoundaries;
static const _flingPointerKind = PointerDeviceKind.unknown;
@override
@ -111,7 +109,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
_registerWidget(widget);
// force delegate scale computing on initialization
// so that it does not happen lazily at the beginning of a scale animation
recalcScale();
initScale();
}
@override
@ -144,13 +142,11 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
void _registerWidget(AvesMagnifier widget) {
registerDelegate(widget);
cachedScaleBoundaries = widget.controller.scaleBoundaries;
setScaleStateUpdateAnimation(animateOnScaleStateUpdate);
}
void _unregisterWidget(AvesMagnifier oldWidget) {
unregisterDelegate(oldWidget);
cachedScaleBoundaries = null;
}
void handleScaleAnimation() {
@ -176,7 +172,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
_mayFlingLTRB = const (true, true, true, true);
_updateMayFling();
_startScale = scale;
_startScale = controller.scale;
_startFocalPoint = details.localFocalPoint;
_lastViewportFocalPosition = _startFocalPoint;
_dropped = false;
@ -214,7 +210,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY! ? (1 + spanDiff) : (1 - spanDiff)) : 1;
_quickScaleLastDistance = distance;
_quickScaleLastY = focalPointY;
newScale = scale! * factor;
newScale = controller.scale! * factor;
} else {
newScale = _startScale! * details.scale;
}
@ -227,7 +223,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
final viewportCenter = boundaries.viewportCenter;
final centerContentPosition = boundaries.viewportToContentPosition(controller, viewportCenter);
final scalePositionDelta = (scaleFocalPoint - viewportCenter) * (scale! / newScale - 1);
final scalePositionDelta = (scaleFocalPoint - viewportCenter) * (controller.scale! / newScale - 1);
final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!;
final newPosition = boundaries.clampPosition(
@ -434,7 +430,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
/// Check if scale is equal to initial after scale animation update
void onAnimationStatusCompleted() {
if (controller.scaleState.state != ScaleState.initial && scale == scaleBoundaries?.initialScale) {
if (controller.scaleState.state != ScaleState.initial && controller.scale == scaleBoundaries?.initialScale) {
controller.setScaleState(ScaleState.initial, ChangeSource.animation);
}
}
@ -446,12 +442,6 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
@override
Widget build(BuildContext context) {
// Check if we need a recalc on the scale
if (widget.controller.scaleBoundaries != cachedScaleBoundaries) {
markNeedsScaleRecalc = true;
cachedScaleBoundaries = widget.controller.scaleBoundaries;
}
return StreamBuilder<MagnifierState>(
stream: controller.stateStream,
initialData: controller.previousState,
@ -494,7 +484,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
controller.setScaleBoundaries(boundaries);
// `Matrix4.scale` uses dynamic typing and can throw `UnimplementedError` on wrong types
final double effectiveScale = (applyScale ? scale : null) ?? 1.0;
final double effectiveScale = (applyScale ? controller.scale : null) ?? 1.0;
return Transform(
transform: Matrix4.identity()
..translate(position.dx, position.dy)

View file

@ -1,6 +1,5 @@
import 'package:aves_magnifier/src/controller/controller_delegate.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
@ -11,14 +10,9 @@ mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
EdgeHit getXEdgeHit() {
final _boundaries = scaleBoundaries;
final _scale = scale;
final _scale = controller.scale;
if (_boundaries == null || _scale == null) return const EdgeHit(false, false);
final contentWidth = _boundaries.contentSize.width * _scale;
final viewportWidth = _boundaries.viewportSize.width;
if (viewportWidth + precisionErrorTolerance >= contentWidth) {
return const EdgeHit(true, true);
}
final x = -position.dx;
final range = _boundaries.getXEdges(scale: _scale);
return EdgeHit(x <= range.min, x >= range.max);
@ -26,14 +20,9 @@ mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
EdgeHit getYEdgeHit() {
final _boundaries = scaleBoundaries;
final _scale = scale;
final _scale = controller.scale;
if (_boundaries == null || _scale == null) return const EdgeHit(false, false);
final contentHeight = _boundaries.contentSize.height * _scale;
final viewportHeight = _boundaries.viewportSize.height;
if (viewportHeight + precisionErrorTolerance >= contentHeight) {
return const EdgeHit(true, true);
}
final y = -position.dy;
final range = _boundaries.getYEdges(scale: _scale);
return EdgeHit(y <= range.min, y >= range.max);

View file

@ -16,12 +16,13 @@ class ScaleBoundaries extends Equatable {
final ScaleLevel _initialScale;
final Size viewportSize;
final Size contentSize;
final EdgeInsets padding;
final Matrix4? externalTransform;
static const Alignment basePosition = Alignment.center;
@override
List<Object?> get props => [_allowOriginalScaleBeyondRange, _minScale, _maxScale, _initialScale, viewportSize, contentSize, externalTransform];
List<Object?> get props => [_allowOriginalScaleBeyondRange, _minScale, _maxScale, _initialScale, viewportSize, contentSize, padding, externalTransform];
const ScaleBoundaries({
required bool allowOriginalScaleBeyondRange,
@ -30,6 +31,7 @@ class ScaleBoundaries extends Equatable {
required ScaleLevel initialScale,
required this.viewportSize,
required this.contentSize,
this.padding = EdgeInsets.zero,
this.externalTransform,
}) : _allowOriginalScaleBeyondRange = allowOriginalScaleBeyondRange,
_minScale = minScale,
@ -43,6 +45,7 @@ class ScaleBoundaries extends Equatable {
initialScale: ScaleLevel(ref: ScaleReference.contained),
viewportSize: Size.zero,
contentSize: Size.zero,
padding: EdgeInsets.zero,
);
ScaleBoundaries copyWith({
@ -52,6 +55,7 @@ class ScaleBoundaries extends Equatable {
ScaleLevel? initialScale,
Size? viewportSize,
Size? contentSize,
EdgeInsets? padding,
Matrix4? externalTransform,
}) {
return ScaleBoundaries(
@ -61,6 +65,7 @@ class ScaleBoundaries extends Equatable {
initialScale: initialScale ?? _initialScale,
viewportSize: viewportSize ?? this.viewportSize,
contentSize: contentSize ?? this.contentSize,
padding: padding ?? this.padding,
externalTransform: externalTransform ?? this.externalTransform,
);
}
@ -110,7 +115,7 @@ class ScaleBoundaries extends Equatable {
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
final maxX = ((positionX + 1).abs() / 2) * widthDiff;
return EdgeRange(minX, maxX);
return EdgeRange(minX - padding.left, maxX + padding.right);
}
EdgeRange getYEdges({required double scale}) {
@ -122,7 +127,7 @@ class ScaleBoundaries extends Equatable {
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
return EdgeRange(minY, maxY);
return EdgeRange(minY - padding.top, maxY + padding.bottom);
}
double clampScale(double scale) {
@ -142,24 +147,11 @@ class ScaleBoundaries extends Equatable {
}
Offset clampPosition({required Offset position, required double scale}) {
final computedWidth = contentSize.width * scale;
final computedHeight = contentSize.height * scale;
final viewportWidth = _transformedViewportSize.width;
final viewportHeight = _transformedViewportSize.height;
var finalX = 0.0;
if (viewportWidth < computedWidth) {
final range = getXEdges(scale: scale);
finalX = position.dx.clamp(range.min, range.max);
}
var finalY = 0.0;
if (viewportHeight < computedHeight) {
final range = getYEdges(scale: scale);
finalY = position.dy.clamp(range.min, range.max);
}
return Offset(finalX, finalY);
final rangeX = getXEdges(scale: scale);
final rangeY = getYEdges(scale: scale);
return Offset(
position.dx.clamp(rangeX.min, rangeX.max),
position.dy.clamp(rangeY.min, rangeY.max),
);
}
}