magnifier: scale boundaries padding;
editor: pan fixes
This commit is contained in:
parent
62952de907
commit
d11bd21d89
13 changed files with 129 additions and 151 deletions
|
@ -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
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
// TODO TLAD [crop] use other strat
|
||||
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
|
||||
_updateCropRegion();
|
||||
}
|
||||
// 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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue