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: {
|
debugIntentData: {
|
||||||
IntentDataKeys.action: IntentActions.edit,
|
IntentDataKeys.action: IntentActions.edit,
|
||||||
IntentDataKeys.mimeType: 'image/*',
|
IntentDataKeys.mimeType: 'image/*',
|
||||||
IntentDataKeys.uri: 'content://media/external/images/media/183128',
|
IntentDataKeys.uri: 'content://media/external/images/media/1000064996', // landscape
|
||||||
// IntentDataKeys.uri: 'content://media/external/images/media/183534',
|
// 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/control_panel.dart';
|
||||||
import 'package:aves/widgets/editor/transform/controller.dart';
|
import 'package:aves/widgets/editor/transform/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/viewer_buttons.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:aves_model/aves_model.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -125,6 +126,7 @@ class EditorControlPanel extends StatelessWidget {
|
||||||
|
|
||||||
void _cancelAction(BuildContext context) {
|
void _cancelAction(BuildContext context) {
|
||||||
actionNotifier.value = null;
|
actionNotifier.value = null;
|
||||||
|
context.read<AvesMagnifierController>().reset();
|
||||||
context.read<TransformController>().reset();
|
context.read<TransformController>().reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ImageEditorPage extends StatefulWidget {
|
||||||
class _ImageEditorPageState extends State<ImageEditorPage> {
|
class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
|
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 ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
|
||||||
final AvesMagnifierController _magnifierController = AvesMagnifierController();
|
final AvesMagnifierController _magnifierController = AvesMagnifierController();
|
||||||
late final TransformController _transformController;
|
late final TransformController _transformController;
|
||||||
|
@ -49,7 +49,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
_actionNotifier.dispose();
|
_actionNotifier.dispose();
|
||||||
_paddingNotifier.dispose();
|
_marginNotifier.dispose();
|
||||||
_viewStateNotifier.dispose();
|
_viewStateNotifier.dispose();
|
||||||
_magnifierController.dispose();
|
_magnifierController.dispose();
|
||||||
_transformController.dispose();
|
_transformController.dispose();
|
||||||
|
@ -59,8 +59,11 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Provider<TransformController>.value(
|
body: MultiProvider(
|
||||||
value: _transformController,
|
providers: [
|
||||||
|
Provider<AvesMagnifierController>.value(value: _magnifierController),
|
||||||
|
Provider<TransformController>.value(value: _transformController),
|
||||||
|
],
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -72,7 +75,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
magnifierController: _magnifierController,
|
magnifierController: _magnifierController,
|
||||||
transformController: _transformController,
|
transformController: _transformController,
|
||||||
actionNotifier: _actionNotifier,
|
actionNotifier: _actionNotifier,
|
||||||
paddingNotifier: _paddingNotifier,
|
marginNotifier: _marginNotifier,
|
||||||
viewStateNotifier: _viewStateNotifier,
|
viewStateNotifier: _viewStateNotifier,
|
||||||
entry: widget.entry,
|
entry: widget.entry,
|
||||||
),
|
),
|
||||||
|
@ -91,7 +94,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
return Cropper(
|
return Cropper(
|
||||||
magnifierController: _magnifierController,
|
magnifierController: _magnifierController,
|
||||||
transformController: _transformController,
|
transformController: _transformController,
|
||||||
paddingNotifier: _paddingNotifier,
|
marginNotifier: _marginNotifier,
|
||||||
);
|
);
|
||||||
case null:
|
case null:
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
|
@ -101,6 +104,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
EditorControlPanel(
|
EditorControlPanel(
|
||||||
entry: widget.entry,
|
entry: widget.entry,
|
||||||
actionNotifier: _actionNotifier,
|
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) {
|
if (_actionNotifier.value == EditorAction.transform) {
|
||||||
_paddingNotifier.value = Cropper.imagePadding;
|
_marginNotifier.value = Cropper.imageMargin;
|
||||||
} else {
|
} else {
|
||||||
_paddingNotifier.value = EdgeInsets.zero;
|
_marginNotifier.value = EdgeInsets.zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class EditorImage extends StatefulWidget {
|
||||||
final AvesMagnifierController magnifierController;
|
final AvesMagnifierController magnifierController;
|
||||||
final TransformController transformController;
|
final TransformController transformController;
|
||||||
final ValueNotifier<EditorAction?> actionNotifier;
|
final ValueNotifier<EditorAction?> actionNotifier;
|
||||||
final ValueNotifier<EdgeInsets> paddingNotifier;
|
final ValueNotifier<EdgeInsets> marginNotifier;
|
||||||
final ValueNotifier<ViewState> viewStateNotifier;
|
final ValueNotifier<ViewState> viewStateNotifier;
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class EditorImage extends StatefulWidget {
|
||||||
required this.magnifierController,
|
required this.magnifierController,
|
||||||
required this.transformController,
|
required this.transformController,
|
||||||
required this.actionNotifier,
|
required this.actionNotifier,
|
||||||
required this.paddingNotifier,
|
required this.marginNotifier,
|
||||||
required this.viewStateNotifier,
|
required this.viewStateNotifier,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
});
|
});
|
||||||
|
@ -96,8 +96,8 @@ class _EditorImageState extends State<EditorImage> {
|
||||||
final canvasSize = MatrixUtils.transformRect(imageToUserMatrix, Offset.zero & mediaSize).size;
|
final canvasSize = MatrixUtils.transformRect(imageToUserMatrix, Offset.zero & mediaSize).size;
|
||||||
|
|
||||||
return ValueListenableBuilder<EdgeInsets>(
|
return ValueListenableBuilder<EdgeInsets>(
|
||||||
valueListenable: widget.paddingNotifier,
|
valueListenable: widget.marginNotifier,
|
||||||
builder: (context, padding, child) {
|
builder: (context, margin, child) {
|
||||||
return Transform(
|
return Transform(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
transform: imageToUserMatrix,
|
transform: imageToUserMatrix,
|
||||||
|
@ -106,12 +106,12 @@ class _EditorImageState extends State<EditorImage> {
|
||||||
builder: (context, action, child) {
|
builder: (context, action, child) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final viewportSize = padding.deflateSize(constraints.biggest);
|
final viewportSize = margin.deflateSize(constraints.biggest);
|
||||||
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
|
final minScale = ScaleLevel(factor: ScaleLevel.scaleForContained(viewportSize, canvasSize));
|
||||||
return AvesMagnifier(
|
return AvesMagnifier(
|
||||||
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||||
controller: widget.magnifierController,
|
controller: widget.magnifierController,
|
||||||
viewportPadding: padding,
|
viewportPadding: margin,
|
||||||
contentSize: mediaSize,
|
contentSize: mediaSize,
|
||||||
allowOriginalScaleBeyondRange: false,
|
allowOriginalScaleBeyondRange: false,
|
||||||
allowGestureScaleBeyondRange: false,
|
allowGestureScaleBeyondRange: false,
|
||||||
|
|
|
@ -81,8 +81,10 @@ class _TransformControlPanelState extends State<TransformControlPanel> with Tick
|
||||||
const SizedBox(height: padding),
|
const SizedBox(height: padding),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const OverlayButton(
|
OverlayButton(
|
||||||
child: BackButton(),
|
child: BackButton(
|
||||||
|
onPressed: widget.onCancel,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class CropRegion extends Equatable {
|
class CropRegion extends Equatable {
|
||||||
|
// region corners in image pixel coordinates
|
||||||
final Offset topLeft, topRight, bottomRight, bottomLeft;
|
final Offset topLeft, topRight, bottomRight, bottomLeft;
|
||||||
|
|
||||||
List<Offset> get corners => [topLeft, topRight, bottomRight, bottomLeft];
|
List<Offset> get corners => [topLeft, topRight, bottomRight, bottomLeft];
|
||||||
|
|
||||||
Offset get center => (topLeft + bottomRight) / 2;
|
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
|
@override
|
||||||
List<Object?> get props => [topLeft, topRight, bottomRight, bottomLeft];
|
List<Object?> get props => [topLeft, topRight, bottomRight, bottomLeft];
|
||||||
|
|
||||||
|
@ -30,7 +22,7 @@ class CropRegion extends Equatable {
|
||||||
required this.bottomLeft,
|
required this.bottomLeft,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const CropRegion zero = CropRegion(
|
static const zero = CropRegion(
|
||||||
topLeft: Offset.zero,
|
topLeft: Offset.zero,
|
||||||
topRight: Offset.zero,
|
topRight: Offset.zero,
|
||||||
bottomRight: Offset.zero,
|
bottomRight: Offset.zero,
|
||||||
|
|
|
@ -21,16 +21,16 @@ import 'package:provider/provider.dart';
|
||||||
class Cropper extends StatefulWidget {
|
class Cropper extends StatefulWidget {
|
||||||
final AvesMagnifierController magnifierController;
|
final AvesMagnifierController magnifierController;
|
||||||
final TransformController transformController;
|
final TransformController transformController;
|
||||||
final ValueNotifier<EdgeInsets> paddingNotifier;
|
final ValueNotifier<EdgeInsets> marginNotifier;
|
||||||
|
|
||||||
static const double handleDimension = kMinInteractiveDimension;
|
static const double handleDimension = kMinInteractiveDimension;
|
||||||
static const EdgeInsets imagePadding = EdgeInsets.all(kMinInteractiveDimension);
|
static const EdgeInsets imageMargin = EdgeInsets.all(kMinInteractiveDimension);
|
||||||
|
|
||||||
const Cropper({
|
const Cropper({
|
||||||
super.key,
|
super.key,
|
||||||
required this.magnifierController,
|
required this.magnifierController,
|
||||||
required this.transformController,
|
required this.transformController,
|
||||||
required this.paddingNotifier,
|
required this.marginNotifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -39,7 +39,6 @@ class Cropper extends StatefulWidget {
|
||||||
|
|
||||||
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
final ValueNotifier<Size> _viewportSizeNotifier = ValueNotifier(Size.zero);
|
|
||||||
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
|
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
|
||||||
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
|
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
|
||||||
late AnimationController _gridAnimationController;
|
late AnimationController _gridAnimationController;
|
||||||
|
@ -61,8 +60,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final initialRegion = transformation.region;
|
|
||||||
_viewportSizeNotifier.addListener(() => _initOutline(initialRegion));
|
|
||||||
_gridAnimationController = AnimationController(
|
_gridAnimationController = AnimationController(
|
||||||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
|
@ -72,7 +69,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
curve: Curves.easeOutQuad,
|
curve: Curves.easeOutQuad,
|
||||||
);
|
);
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
_initOutline(initialRegion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -84,7 +80,6 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewportSizeNotifier.dispose();
|
|
||||||
_outlineNotifier.dispose();
|
_outlineNotifier.dispose();
|
||||||
_gridDivisionNotifier.dispose();
|
_gridDivisionNotifier.dispose();
|
||||||
_gridOpacity.dispose();
|
_gridOpacity.dispose();
|
||||||
|
@ -95,7 +90,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
|
|
||||||
void _registerWidget(Cropper widget) {
|
void _registerWidget(Cropper widget) {
|
||||||
_subscriptions.add(widget.magnifierController.stateStream.listen(_onViewStateChanged));
|
_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.eventStream.listen(_onTransformEvent));
|
||||||
_subscriptions.add(widget.transformController.transformationStream.map((v) => v.orientation).distinct().listen(_onOrientationChanged));
|
_subscriptions.add(widget.transformController.transformationStream.map((v) => v.orientation).distinct().listen(_onOrientationChanged));
|
||||||
_subscriptions.add(widget.transformController.transformationStream.map((v) => v.straightenDegrees).distinct().listen(_onStraightenDegreesChanged));
|
_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) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned.fill(
|
return Positioned.fill(
|
||||||
child: ValueListenableBuilder<EdgeInsets>(
|
child: ValueListenableBuilder<EdgeInsets>(
|
||||||
valueListenable: widget.paddingNotifier,
|
valueListenable: widget.marginNotifier,
|
||||||
builder: (context, padding, child) {
|
builder: (context, margin, child) {
|
||||||
return ValueListenableBuilder<Rect>(
|
return ValueListenableBuilder<Rect>(
|
||||||
valueListenable: _outlineNotifier,
|
valueListenable: _outlineNotifier,
|
||||||
builder: (context, outline, child) {
|
builder: (context, outline, child) {
|
||||||
if (outline.isEmpty) return const SizedBox();
|
if (outline.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
final outlineVisualRect = outline.translate(padding.left, padding.top);
|
final outlineVisualRect = outline.translate(margin.left, margin.top);
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
@ -155,35 +150,35 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildVertexHandle(
|
_buildVertexHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getPosition: () => outline.topLeft,
|
getPosition: () => outline.topLeft,
|
||||||
setPosition: (v) => _handleOutline(
|
setPosition: (v) => _handleOutline(
|
||||||
topLeft: Offset(min(outline.right - minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
|
topLeft: Offset(min(outline.right - minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildVertexHandle(
|
_buildVertexHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getPosition: () => outline.topRight,
|
getPosition: () => outline.topRight,
|
||||||
setPosition: (v) => _handleOutline(
|
setPosition: (v) => _handleOutline(
|
||||||
topRight: Offset(max(outline.left + minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
|
topRight: Offset(max(outline.left + minDimension, v.dx), min(outline.bottom - minDimension, v.dy)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildVertexHandle(
|
_buildVertexHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getPosition: () => outline.bottomRight,
|
getPosition: () => outline.bottomRight,
|
||||||
setPosition: (v) => _handleOutline(
|
setPosition: (v) => _handleOutline(
|
||||||
bottomRight: Offset(max(outline.left + minDimension, v.dx), max(outline.top + minDimension, v.dy)),
|
bottomRight: Offset(max(outline.left + minDimension, v.dx), max(outline.top + minDimension, v.dy)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildVertexHandle(
|
_buildVertexHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getPosition: () => outline.bottomLeft,
|
getPosition: () => outline.bottomLeft,
|
||||||
setPosition: (v) => _handleOutline(
|
setPosition: (v) => _handleOutline(
|
||||||
bottomLeft: Offset(min(outline.right - minDimension, v.dx), max(outline.top + minDimension, v.dy)),
|
bottomLeft: Offset(min(outline.right - minDimension, v.dx), max(outline.top + minDimension, v.dy)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildEdgeHandle(
|
_buildEdgeHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.topLeft),
|
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.topLeft),
|
||||||
setEdge: (v) {
|
setEdge: (v) {
|
||||||
final left = min(outline.right - minDimension, v.left);
|
final left = min(outline.right - minDimension, v.left);
|
||||||
|
@ -194,7 +189,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildEdgeHandle(
|
_buildEdgeHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getEdge: () => Rect.fromPoints(outline.topLeft, outline.topRight),
|
getEdge: () => Rect.fromPoints(outline.topLeft, outline.topRight),
|
||||||
setEdge: (v) {
|
setEdge: (v) {
|
||||||
final top = min(outline.bottom - minDimension, v.top);
|
final top = min(outline.bottom - minDimension, v.top);
|
||||||
|
@ -205,7 +200,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildEdgeHandle(
|
_buildEdgeHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getEdge: () => Rect.fromPoints(outline.bottomRight, outline.topRight),
|
getEdge: () => Rect.fromPoints(outline.bottomRight, outline.topRight),
|
||||||
setEdge: (v) {
|
setEdge: (v) {
|
||||||
final right = max(outline.left + minDimension, v.right);
|
final right = max(outline.left + minDimension, v.right);
|
||||||
|
@ -216,7 +211,7 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildEdgeHandle(
|
_buildEdgeHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.bottomRight),
|
getEdge: () => Rect.fromPoints(outline.bottomLeft, outline.bottomRight),
|
||||||
setEdge: (v) {
|
setEdge: (v) {
|
||||||
final bottom = max(outline.top + minDimension, v.bottom);
|
final bottom = max(outline.top + minDimension, v.bottom);
|
||||||
|
@ -341,12 +336,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexHandle _buildVertexHandle({
|
VertexHandle _buildVertexHandle({
|
||||||
required EdgeInsets padding,
|
required EdgeInsets margin,
|
||||||
required ValueGetter<Offset> getPosition,
|
required ValueGetter<Offset> getPosition,
|
||||||
required ValueSetter<Offset> setPosition,
|
required ValueSetter<Offset> setPosition,
|
||||||
}) {
|
}) {
|
||||||
return VertexHandle(
|
return VertexHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getPosition: getPosition,
|
getPosition: getPosition,
|
||||||
setPosition: setPosition,
|
setPosition: setPosition,
|
||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
|
@ -355,12 +350,12 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeHandle _buildEdgeHandle({
|
EdgeHandle _buildEdgeHandle({
|
||||||
required EdgeInsets padding,
|
required EdgeInsets margin,
|
||||||
required ValueGetter<Rect> getEdge,
|
required ValueGetter<Rect> getEdge,
|
||||||
required ValueSetter<Rect> setEdge,
|
required ValueSetter<Rect> setEdge,
|
||||||
}) {
|
}) {
|
||||||
return EdgeHandle(
|
return EdgeHandle(
|
||||||
padding: padding,
|
margin: margin,
|
||||||
getEdge: getEdge,
|
getEdge: getEdge,
|
||||||
setEdge: setEdge,
|
setEdge: setEdge,
|
||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
|
@ -392,11 +387,18 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
_setOutline(_regionToContainedOutline(nextState, region));
|
_setOutline(_regionToContainedOutline(nextState, region));
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewState _viewStateForContainedRegion(ScaleBoundaries boundaries, CropRegion region) {
|
ViewState _viewStateForContainedRegion(ScaleBoundaries boundaries, CropRegion imageRegion) {
|
||||||
final regionSize = MatrixUtils.transformRect(transformation.matrix, region.outsideRect).size;
|
final matrix = transformation.matrix;
|
||||||
final nextScale = boundaries.clampScale(ScaleLevel.scaleForContained(boundaries.viewportSize, regionSize));
|
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(
|
final nextPosition = boundaries.clampPosition(
|
||||||
position: boundaries.contentToStatePosition(nextScale, region.center),
|
position: boundaries.contentToStatePosition(nextScale, imageRegion.center),
|
||||||
scale: nextScale,
|
scale: nextScale,
|
||||||
);
|
);
|
||||||
return ViewState(
|
return ViewState(
|
||||||
|
@ -447,19 +449,30 @@ class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
|
|
||||||
void _onViewStateChanged(MagnifierState state) {
|
void _onViewStateChanged(MagnifierState state) {
|
||||||
final currentOutline = _outlineNotifier.value;
|
final currentOutline = _outlineNotifier.value;
|
||||||
switch (state.source) {
|
// switch (state.source) {
|
||||||
case ChangeSource.internal:
|
// case ChangeSource.internal:
|
||||||
case ChangeSource.animation:
|
// case ChangeSource.animation:
|
||||||
_setOutline(currentOutline);
|
// _setOutline(currentOutline);
|
||||||
case ChangeSource.gesture:
|
// case ChangeSource.gesture:
|
||||||
// TODO TLAD [crop] use other strat
|
// TODO TLAD [crop] use other strat
|
||||||
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
|
_setOutline(_applyCropRatioToOutline(currentOutline, _RatioStrategy.contain));
|
||||||
_updateCropRegion();
|
_updateCropRegion();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onViewBoundariesChanged(ScaleBoundaries scaleBoundaries) {
|
void _onViewportSizeChanged(Size viewportSize) {
|
||||||
_viewportSizeNotifier.value = scaleBoundaries.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() {
|
ViewState? _getViewState() {
|
||||||
|
|
|
@ -2,14 +2,14 @@ import 'package:aves/widgets/editor/transform/cropper.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class VertexHandle extends StatefulWidget {
|
class VertexHandle extends StatefulWidget {
|
||||||
final EdgeInsets padding;
|
final EdgeInsets margin;
|
||||||
final ValueGetter<Offset> getPosition;
|
final ValueGetter<Offset> getPosition;
|
||||||
final ValueSetter<Offset> setPosition;
|
final ValueSetter<Offset> setPosition;
|
||||||
final VoidCallback onDragStart, onDragEnd;
|
final VoidCallback onDragStart, onDragEnd;
|
||||||
|
|
||||||
const VertexHandle({
|
const VertexHandle({
|
||||||
super.key,
|
super.key,
|
||||||
required this.padding,
|
required this.margin,
|
||||||
required this.getPosition,
|
required this.getPosition,
|
||||||
required this.setPosition,
|
required this.setPosition,
|
||||||
required this.onDragStart,
|
required this.onDragStart,
|
||||||
|
@ -26,13 +26,13 @@ class _VertexHandleState extends State<VertexHandle> {
|
||||||
|
|
||||||
static const double _handleDim = Cropper.handleDimension;
|
static const double _handleDim = Cropper.handleDimension;
|
||||||
|
|
||||||
EdgeInsets get padding => widget.padding;
|
EdgeInsets get margin => widget.margin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned.fromRect(
|
return Positioned.fromRect(
|
||||||
rect: Rect.fromCenter(
|
rect: Rect.fromCenter(
|
||||||
center: widget.getPosition().translate(padding.left, padding.right),
|
center: widget.getPosition().translate(margin.left, margin.right),
|
||||||
width: _handleDim,
|
width: _handleDim,
|
||||||
height: _handleDim,
|
height: _handleDim,
|
||||||
),
|
),
|
||||||
|
@ -58,14 +58,14 @@ class _VertexHandleState extends State<VertexHandle> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EdgeHandle extends StatefulWidget {
|
class EdgeHandle extends StatefulWidget {
|
||||||
final EdgeInsets padding;
|
final EdgeInsets margin;
|
||||||
final ValueGetter<Rect> getEdge;
|
final ValueGetter<Rect> getEdge;
|
||||||
final ValueSetter<Rect> setEdge;
|
final ValueSetter<Rect> setEdge;
|
||||||
final VoidCallback onDragStart, onDragEnd;
|
final VoidCallback onDragStart, onDragEnd;
|
||||||
|
|
||||||
const EdgeHandle({
|
const EdgeHandle({
|
||||||
super.key,
|
super.key,
|
||||||
required this.padding,
|
required this.margin,
|
||||||
required this.getEdge,
|
required this.getEdge,
|
||||||
required this.setEdge,
|
required this.setEdge,
|
||||||
required this.onDragStart,
|
required this.onDragStart,
|
||||||
|
@ -82,7 +82,7 @@ class _EdgeHandleState extends State<EdgeHandle> {
|
||||||
|
|
||||||
static const double _handleDim = Cropper.handleDimension;
|
static const double _handleDim = Cropper.handleDimension;
|
||||||
|
|
||||||
EdgeInsets get padding => widget.padding;
|
EdgeInsets get margin => widget.margin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -94,7 +94,7 @@ class _EdgeHandleState extends State<EdgeHandle> {
|
||||||
// vertical edge
|
// vertical edge
|
||||||
edge = Rect.fromLTWH(edge.left - _handleDim / 2, edge.top + _handleDim / 2, _handleDim, edge.height - _handleDim);
|
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(
|
return Positioned.fromRect(
|
||||||
rect: edge,
|
rect: edge,
|
||||||
|
|
|
@ -32,7 +32,7 @@ class AvesMagnifierController {
|
||||||
initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source);
|
initial = initialState ?? const MagnifierState(position: Offset.zero, scale: null, source: source);
|
||||||
previousState = initial;
|
previousState = initial;
|
||||||
_currentState = initial;
|
_currentState = initial;
|
||||||
_setState(initial);
|
reset();
|
||||||
|
|
||||||
const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: source);
|
const _initialScaleState = ScaleStateChange(state: ScaleState.initial, source: source);
|
||||||
previousScaleState = _initialScaleState;
|
previousScaleState = _initialScaleState;
|
||||||
|
@ -70,6 +70,8 @@ class AvesMagnifierController {
|
||||||
|
|
||||||
bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut;
|
bool get isZooming => scaleState.state == ScaleState.zoomedIn || scaleState.state == ScaleState.zoomedOut;
|
||||||
|
|
||||||
|
void reset() => _setState(initial);
|
||||||
|
|
||||||
void update({
|
void update({
|
||||||
Offset? position,
|
Offset? position,
|
||||||
double? scale,
|
double? scale,
|
||||||
|
|
|
@ -21,14 +21,12 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
|
||||||
|
|
||||||
Function(double? prevScale, double? nextScale, Offset nextPosition)? _animateScale;
|
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 = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
|
|
||||||
void registerDelegate(AvesMagnifier widget) {
|
void registerDelegate(AvesMagnifier widget) {
|
||||||
_subscriptions.add(widget.controller.stateStream.listen(_onMagnifierStateChanged));
|
_subscriptions.add(widget.controller.stateStream.listen(_onMagnifierStateChanged));
|
||||||
_subscriptions.add(widget.controller.scaleStateChangeStream.listen(_onScaleStateChanged));
|
_subscriptions.add(widget.controller.scaleStateChangeStream.listen(_onScaleStateChanged));
|
||||||
|
_subscriptions.add(widget.controller.scaleBoundariesStream.listen(_onScaleBoundariesChanged));
|
||||||
}
|
}
|
||||||
|
|
||||||
void unregisterDelegate(AvesMagnifier oldWidget) {
|
void unregisterDelegate(AvesMagnifier oldWidget) {
|
||||||
|
@ -38,12 +36,16 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
|
||||||
..clear();
|
..clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onScaleBoundariesChanged(ScaleBoundaries boundaries) {
|
||||||
|
initScale();
|
||||||
|
}
|
||||||
|
|
||||||
void _onScaleStateChanged(ScaleStateChange scaleStateChange) {
|
void _onScaleStateChanged(ScaleStateChange scaleStateChange) {
|
||||||
if (scaleStateChange.source == ChangeSource.internal) return;
|
if (scaleStateChange.source == ChangeSource.internal) return;
|
||||||
if (!controller.hasScaleSateChanged) return;
|
if (!controller.hasScaleSateChanged) return;
|
||||||
|
|
||||||
if (_animateScale == null || controller.isZooming) {
|
if (_animateScale == null || controller.isZooming) {
|
||||||
controller.update(scale: scale, source: scaleStateChange.source);
|
controller.update(scale: controller.scale, source: scaleStateChange.source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,34 +70,24 @@ mixin AvesMagnifierControllerDelegate on State<AvesMagnifier> {
|
||||||
|
|
||||||
void _onMagnifierStateChanged(MagnifierState state) {
|
void _onMagnifierStateChanged(MagnifierState state) {
|
||||||
final boundaries = scaleBoundaries;
|
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);
|
controller.update(position: boundaries.clampPosition(position: position, scale: currentScale), source: state.source);
|
||||||
if (controller.scale == controller.previousState.scale) return;
|
final newScale = controller.scale;
|
||||||
|
if (newScale == null || newScale == currentScale) return;
|
||||||
|
|
||||||
if (state.source == ChangeSource.internal || state.source == ChangeSource.animation) 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);
|
controller.setScaleState(newScaleState, state.source);
|
||||||
}
|
}
|
||||||
|
|
||||||
Offset get position => controller.position;
|
Offset get position => controller.position;
|
||||||
|
|
||||||
double? recalcScale() {
|
void initScale() {
|
||||||
final scaleState = controller.scaleState.state;
|
final scaleState = controller.scaleState.state;
|
||||||
final newScale = controller.getScaleForScaleState(scaleState);
|
final newScale = controller.getScaleForScaleState(scaleState);
|
||||||
markNeedsScaleRecalc = false;
|
|
||||||
setScale(newScale, ChangeSource.internal);
|
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);
|
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;
|
if (boundaries == null) return;
|
||||||
|
|
||||||
var newScaleState = ScaleState.initial;
|
var newScaleState = ScaleState.initial;
|
||||||
if (scale != boundaries.initialScale) {
|
if (controller.scale != boundaries.initialScale) {
|
||||||
newScaleState = (newScale > boundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
|
newScaleState = (newScale > boundaries.initialScale) ? ScaleState.zoomedIn : ScaleState.zoomedOut;
|
||||||
}
|
}
|
||||||
controller.setScaleState(newScaleState, source);
|
controller.setScaleState(newScaleState, source);
|
||||||
|
|
|
@ -97,8 +97,6 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
late AnimationController _positionAnimationController;
|
late AnimationController _positionAnimationController;
|
||||||
late Animation<Offset> _positionAnimation;
|
late Animation<Offset> _positionAnimation;
|
||||||
|
|
||||||
ScaleBoundaries? cachedScaleBoundaries;
|
|
||||||
|
|
||||||
static const _flingPointerKind = PointerDeviceKind.unknown;
|
static const _flingPointerKind = PointerDeviceKind.unknown;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -111,7 +109,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
// force delegate scale computing on initialization
|
// force delegate scale computing on initialization
|
||||||
// so that it does not happen lazily at the beginning of a scale animation
|
// so that it does not happen lazily at the beginning of a scale animation
|
||||||
recalcScale();
|
initScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -144,13 +142,11 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
|
|
||||||
void _registerWidget(AvesMagnifier widget) {
|
void _registerWidget(AvesMagnifier widget) {
|
||||||
registerDelegate(widget);
|
registerDelegate(widget);
|
||||||
cachedScaleBoundaries = widget.controller.scaleBoundaries;
|
|
||||||
setScaleStateUpdateAnimation(animateOnScaleStateUpdate);
|
setScaleStateUpdateAnimation(animateOnScaleStateUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(AvesMagnifier oldWidget) {
|
void _unregisterWidget(AvesMagnifier oldWidget) {
|
||||||
unregisterDelegate(oldWidget);
|
unregisterDelegate(oldWidget);
|
||||||
cachedScaleBoundaries = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleScaleAnimation() {
|
void handleScaleAnimation() {
|
||||||
|
@ -176,7 +172,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
_mayFlingLTRB = const (true, true, true, true);
|
_mayFlingLTRB = const (true, true, true, true);
|
||||||
_updateMayFling();
|
_updateMayFling();
|
||||||
|
|
||||||
_startScale = scale;
|
_startScale = controller.scale;
|
||||||
_startFocalPoint = details.localFocalPoint;
|
_startFocalPoint = details.localFocalPoint;
|
||||||
_lastViewportFocalPosition = _startFocalPoint;
|
_lastViewportFocalPosition = _startFocalPoint;
|
||||||
_dropped = false;
|
_dropped = false;
|
||||||
|
@ -214,7 +210,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY! ? (1 + spanDiff) : (1 - spanDiff)) : 1;
|
final factor = _quickScaleMoved ? (focalPointY > _quickScaleLastY! ? (1 + spanDiff) : (1 - spanDiff)) : 1;
|
||||||
_quickScaleLastDistance = distance;
|
_quickScaleLastDistance = distance;
|
||||||
_quickScaleLastY = focalPointY;
|
_quickScaleLastY = focalPointY;
|
||||||
newScale = scale! * factor;
|
newScale = controller.scale! * factor;
|
||||||
} else {
|
} else {
|
||||||
newScale = _startScale! * details.scale;
|
newScale = _startScale! * details.scale;
|
||||||
}
|
}
|
||||||
|
@ -227,7 +223,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
|
|
||||||
final viewportCenter = boundaries.viewportCenter;
|
final viewportCenter = boundaries.viewportCenter;
|
||||||
final centerContentPosition = boundaries.viewportToContentPosition(controller, 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 panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!;
|
||||||
|
|
||||||
final newPosition = boundaries.clampPosition(
|
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
|
/// Check if scale is equal to initial after scale animation update
|
||||||
void onAnimationStatusCompleted() {
|
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);
|
controller.setScaleState(ScaleState.initial, ChangeSource.animation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -446,12 +442,6 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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>(
|
return StreamBuilder<MagnifierState>(
|
||||||
stream: controller.stateStream,
|
stream: controller.stateStream,
|
||||||
initialData: controller.previousState,
|
initialData: controller.previousState,
|
||||||
|
@ -494,7 +484,7 @@ class _AvesMagnifierState extends State<AvesMagnifier> with TickerProviderStateM
|
||||||
controller.setScaleBoundaries(boundaries);
|
controller.setScaleBoundaries(boundaries);
|
||||||
|
|
||||||
// `Matrix4.scale` uses dynamic typing and can throw `UnimplementedError` on wrong types
|
// `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(
|
return Transform(
|
||||||
transform: Matrix4.identity()
|
transform: Matrix4.identity()
|
||||||
..translate(position.dx, position.dy)
|
..translate(position.dx, position.dy)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:aves_magnifier/src/controller/controller_delegate.dart';
|
import 'package:aves_magnifier/src/controller/controller_delegate.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
|
mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
|
||||||
|
@ -11,14 +10,9 @@ mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
|
||||||
|
|
||||||
EdgeHit getXEdgeHit() {
|
EdgeHit getXEdgeHit() {
|
||||||
final _boundaries = scaleBoundaries;
|
final _boundaries = scaleBoundaries;
|
||||||
final _scale = scale;
|
final _scale = controller.scale;
|
||||||
if (_boundaries == null || _scale == null) return const EdgeHit(false, false);
|
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 x = -position.dx;
|
||||||
final range = _boundaries.getXEdges(scale: _scale);
|
final range = _boundaries.getXEdges(scale: _scale);
|
||||||
return EdgeHit(x <= range.min, x >= range.max);
|
return EdgeHit(x <= range.min, x >= range.max);
|
||||||
|
@ -26,14 +20,9 @@ mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
|
||||||
|
|
||||||
EdgeHit getYEdgeHit() {
|
EdgeHit getYEdgeHit() {
|
||||||
final _boundaries = scaleBoundaries;
|
final _boundaries = scaleBoundaries;
|
||||||
final _scale = scale;
|
final _scale = controller.scale;
|
||||||
if (_boundaries == null || _scale == null) return const EdgeHit(false, false);
|
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 y = -position.dy;
|
||||||
final range = _boundaries.getYEdges(scale: _scale);
|
final range = _boundaries.getYEdges(scale: _scale);
|
||||||
return EdgeHit(y <= range.min, y >= range.max);
|
return EdgeHit(y <= range.min, y >= range.max);
|
||||||
|
|
|
@ -16,12 +16,13 @@ class ScaleBoundaries extends Equatable {
|
||||||
final ScaleLevel _initialScale;
|
final ScaleLevel _initialScale;
|
||||||
final Size viewportSize;
|
final Size viewportSize;
|
||||||
final Size contentSize;
|
final Size contentSize;
|
||||||
|
final EdgeInsets padding;
|
||||||
final Matrix4? externalTransform;
|
final Matrix4? externalTransform;
|
||||||
|
|
||||||
static const Alignment basePosition = Alignment.center;
|
static const Alignment basePosition = Alignment.center;
|
||||||
|
|
||||||
@override
|
@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({
|
const ScaleBoundaries({
|
||||||
required bool allowOriginalScaleBeyondRange,
|
required bool allowOriginalScaleBeyondRange,
|
||||||
|
@ -30,6 +31,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
required ScaleLevel initialScale,
|
required ScaleLevel initialScale,
|
||||||
required this.viewportSize,
|
required this.viewportSize,
|
||||||
required this.contentSize,
|
required this.contentSize,
|
||||||
|
this.padding = EdgeInsets.zero,
|
||||||
this.externalTransform,
|
this.externalTransform,
|
||||||
}) : _allowOriginalScaleBeyondRange = allowOriginalScaleBeyondRange,
|
}) : _allowOriginalScaleBeyondRange = allowOriginalScaleBeyondRange,
|
||||||
_minScale = minScale,
|
_minScale = minScale,
|
||||||
|
@ -43,6 +45,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
initialScale: ScaleLevel(ref: ScaleReference.contained),
|
initialScale: ScaleLevel(ref: ScaleReference.contained),
|
||||||
viewportSize: Size.zero,
|
viewportSize: Size.zero,
|
||||||
contentSize: Size.zero,
|
contentSize: Size.zero,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
);
|
);
|
||||||
|
|
||||||
ScaleBoundaries copyWith({
|
ScaleBoundaries copyWith({
|
||||||
|
@ -52,6 +55,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
ScaleLevel? initialScale,
|
ScaleLevel? initialScale,
|
||||||
Size? viewportSize,
|
Size? viewportSize,
|
||||||
Size? contentSize,
|
Size? contentSize,
|
||||||
|
EdgeInsets? padding,
|
||||||
Matrix4? externalTransform,
|
Matrix4? externalTransform,
|
||||||
}) {
|
}) {
|
||||||
return ScaleBoundaries(
|
return ScaleBoundaries(
|
||||||
|
@ -61,6 +65,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
initialScale: initialScale ?? _initialScale,
|
initialScale: initialScale ?? _initialScale,
|
||||||
viewportSize: viewportSize ?? this.viewportSize,
|
viewportSize: viewportSize ?? this.viewportSize,
|
||||||
contentSize: contentSize ?? this.contentSize,
|
contentSize: contentSize ?? this.contentSize,
|
||||||
|
padding: padding ?? this.padding,
|
||||||
externalTransform: externalTransform ?? this.externalTransform,
|
externalTransform: externalTransform ?? this.externalTransform,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +115,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
|
|
||||||
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
|
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
|
||||||
final maxX = ((positionX + 1).abs() / 2) * widthDiff;
|
final maxX = ((positionX + 1).abs() / 2) * widthDiff;
|
||||||
return EdgeRange(minX, maxX);
|
return EdgeRange(minX - padding.left, maxX + padding.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeRange getYEdges({required double scale}) {
|
EdgeRange getYEdges({required double scale}) {
|
||||||
|
@ -122,7 +127,7 @@ class ScaleBoundaries extends Equatable {
|
||||||
|
|
||||||
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
|
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
|
||||||
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
|
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
|
||||||
return EdgeRange(minY, maxY);
|
return EdgeRange(minY - padding.top, maxY + padding.bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
double clampScale(double scale) {
|
double clampScale(double scale) {
|
||||||
|
@ -142,24 +147,11 @@ class ScaleBoundaries extends Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
Offset clampPosition({required Offset position, required double scale}) {
|
Offset clampPosition({required Offset position, required double scale}) {
|
||||||
final computedWidth = contentSize.width * scale;
|
final rangeX = getXEdges(scale: scale);
|
||||||
final computedHeight = contentSize.height * scale;
|
final rangeY = getYEdges(scale: scale);
|
||||||
|
return Offset(
|
||||||
final viewportWidth = _transformedViewportSize.width;
|
position.dx.clamp(rangeX.min, rangeX.max),
|
||||||
final viewportHeight = _transformedViewportSize.height;
|
position.dy.clamp(rangeY.min, rangeY.max),
|
||||||
|
);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue