zoom_widget/lib/zoom_widget.dart

1007 lines
36 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3, Matrix4;
typedef ZoomWidgetBuilder = Widget Function(BuildContext context, Quad viewport);
@immutable
class Zoom extends StatefulWidget {
Zoom({
this.backgroundColor = Colors.grey,
this.canvasColor = Colors.white,
this.centerOnScale = true,
required this.child,
this.colorScrollBars = Colors.black12,
this.doubleTapAnimDuration = const Duration(milliseconds: 300),
this.doubleTapScaleChange = 1.1,
this.doubleTapZoom = true,
this.enableScroll = true,
this.initPosition,
this.initScale,
this.initTotalZoomOut = false,
Key? key,
this.maxScale = 2.5,
this.minScale = 1.0,
this.maxZoomHeight,
this.maxZoomWidth,
this.onPositionUpdate,
this.onScaleUpdate,
this.onPanUpPosition,
this.onMinZoom,
this.onTap,
this.opacityScrollBars = 0.5,
this.radiusScrollBars = 4,
this.scrollWeight = 10,
this.transformationController,
this.zoomSensibility = 1.0,
}) : assert(maxScale > 0),
assert(!maxScale.isNaN),
super(key: key);
final Color backgroundColor;
final Color canvasColor;
final bool centerOnScale;
final Widget child;
final Color colorScrollBars;
final Duration doubleTapAnimDuration;
final double doubleTapScaleChange;
final bool doubleTapZoom;
final bool enableScroll;
final Offset? initPosition;
final double? initScale;
final bool initTotalZoomOut;
final double maxScale;
final double minScale;
final double? maxZoomHeight;
final double? maxZoomWidth;
final Function(Offset)? onPositionUpdate;
final Function(double, double)? onScaleUpdate;
final Function(Offset)? onPanUpPosition;
final Function(bool)? onMinZoom;
final Function()? onTap;
final double opacityScrollBars;
final double radiusScrollBars;
final double scrollWeight;
final TransformationController? transformationController;
final double zoomSensibility;
static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + math.pow(l2.y - l1.y, 2.0).toDouble();
if (lengthSquared == 0) {
return l1;
}
final Vector3 l1P = point - l1;
final Vector3 l1L2 = l2 - l1;
final double fraction = (l1P.dot(l1L2) / lengthSquared).clamp(0.0, 1.0);
return l1 + l1L2 * fraction;
}
static Quad getAxisAlignedBoundingBox(Quad quad) {
final double minX = math.min(quad.point0.x, math.min(quad.point1.x, math.min(quad.point2.x, quad.point3.x)));
final double minY = math.min(quad.point0.y, math.min(quad.point1.y, math.min(quad.point2.y, quad.point3.y)));
final double maxX = math.max(quad.point0.x, math.max(quad.point1.x, math.max(quad.point2.x, quad.point3.x)));
final double maxY = math.max(quad.point0.y, math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y)));
return Quad.points(Vector3(minX, minY, 0), Vector3(maxX, minY, 0), Vector3(maxX, maxY, 0), Vector3(minX, maxY, 0));
}
static bool pointIsInside(Vector3 point, Quad quad) {
final Vector3 aM = point - quad.point0;
final Vector3 aB = quad.point1 - quad.point0;
final Vector3 aD = quad.point3 - quad.point0;
final double aMAB = aM.dot(aB);
final double aBAB = aB.dot(aB);
final double aMAD = aM.dot(aD);
final double aDAD = aD.dot(aD);
return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD;
}
static Vector3 getNearestPointInside(Vector3 point, Quad quad) {
if (pointIsInside(point, quad)) {
return point;
}
final List<Vector3> closestPoints = <Vector3>[
Zoom.getNearestPointOnLine(point, quad.point0, quad.point1),
Zoom.getNearestPointOnLine(point, quad.point1, quad.point2),
Zoom.getNearestPointOnLine(point, quad.point2, quad.point3),
Zoom.getNearestPointOnLine(point, quad.point3, quad.point0),
];
double minDistance = double.infinity;
late Vector3 closestOverall;
for (final Vector3 closePoint in closestPoints) {
final double distance = math.sqrt(math.pow(point.x - closePoint.x, 2) + math.pow(point.y - closePoint.y, 2));
if (distance < minDistance) {
minDistance = distance;
closestOverall = closePoint;
}
}
return closestOverall;
}
@override
State<Zoom> createState() => _ZoomState();
}
class _ZoomState extends State<Zoom> with TickerProviderStateMixin, WidgetsBindingObserver {
TransformationController? _transformationController;
final GlobalKey _childKey = GlobalKey();
final GlobalKey _parentKey = GlobalKey();
Animation<Offset>? _animation;
late AnimationController _controller;
Animation<double>? _scaleAnimation;
late AnimationController _scaleController;
Axis? _panAxis;
Offset? _referenceFocalPoint;
double? _scaleStart;
_GestureType? _gestureType;
ValueNotifier<_ScrollBarData> verticalScrollNotifier = ValueNotifier(_ScrollBarData(length: 0, position: 0));
ValueNotifier<_ScrollBarData> horizontalScrollNotifier = ValueNotifier(_ScrollBarData(length: 0, position: 0));
Size parentSize = Size.zero;
Size childSize = Size.zero;
Orientation? _orientation;
Offset? _doubleTapFocalPoint;
bool doubleTapZoomIn = true;
bool firstDraw = true;
static const double _kDrag = 0.0000135;
Rect get _boundaryRect {
assert(_childKey.currentContext != null);
final Rect boundaryRect = EdgeInsets.zero.inflateRect(Offset.zero & childSize);
assert(!boundaryRect.isEmpty, "Zoom's child must have nonzero dimensions.");
assert(
boundaryRect.isFinite ||
(boundaryRect.left.isInfinite &&
boundaryRect.top.isInfinite &&
boundaryRect.right.isInfinite &&
boundaryRect.bottom.isInfinite),
'boundaryRect must either be infinite in all directions or finite in all directions.',
);
return boundaryRect;
}
Rect get _viewport {
assert(_parentKey.currentContext != null);
final RenderBox parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox;
return Offset.zero & parentRenderBox.size;
}
double _getScrollPercent(Matrix4 matrix, {required _ScrollType scrollType}) {
switch (scrollType) {
case _ScrollType.horizontal:
return _getMatrixTranslation(matrix).dx.abs() /
((childSize.width * matrix.getMaxScaleOnAxis()) - parentSize.width) *
100.0;
case _ScrollType.vertical:
return _getMatrixTranslation(matrix).dy.abs() /
((childSize.height * matrix.getMaxScaleOnAxis()) - parentSize.height) *
100.0;
}
}
double _getScrollBarLength(Matrix4 matrix, {required _ScrollType scrollType}) {
double percent = 0;
switch (scrollType) {
case _ScrollType.horizontal:
percent = (parentSize.width / (childSize.width * matrix.getMaxScaleOnAxis()));
return parentSize.width * percent;
case _ScrollType.vertical:
percent = (parentSize.height / (childSize.height * matrix.getMaxScaleOnAxis()));
return parentSize.height * percent;
}
}
void onDisabledScrolls() {
if (horizontalScrollNotifier.value.length == 0 &&
horizontalScrollNotifier.value.position == 0 &&
verticalScrollNotifier.value.length == 0 &&
verticalScrollNotifier.value.position == 0) {
widget.onMinZoom?.call(true);
} else {
widget.onMinZoom?.call(false);
}
}
void _updateScroll(Matrix4 matrix) {
if (childSize.width * matrix.getMaxScaleOnAxis() > parentSize.width + (parentSize.width * 0.01)) {
var horizontalPercent = _getScrollPercent(matrix, scrollType: _ScrollType.horizontal);
final horizontalLength = _getScrollBarLength(matrix, scrollType: _ScrollType.horizontal);
horizontalScrollNotifier.value = _ScrollBarData(
length: horizontalLength,
position: (horizontalPercent / 100.0) * (parentSize.width - horizontalLength),
);
} else {
horizontalScrollNotifier.value = _ScrollBarData(length: 0, position: 0);
onDisabledScrolls();
}
if (childSize.height * matrix.getMaxScaleOnAxis() > parentSize.height + (parentSize.height * 0.01)) {
final verticalPercent = _getScrollPercent(matrix, scrollType: _ScrollType.vertical);
final verticalLength = _getScrollBarLength(matrix, scrollType: _ScrollType.vertical);
verticalScrollNotifier.value = _ScrollBarData(
length: verticalLength,
position: (verticalPercent / 100) * (parentSize.height - verticalLength),
);
} else {
verticalScrollNotifier.value = _ScrollBarData(length: 0, position: 0);
onDisabledScrolls();
}
}
Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation, {bool fixOffset = false}) {
if (translation == Offset.zero) {
return matrix.clone();
}
final Offset alignedTranslation = translation;
final Matrix4 nextMatrix = matrix.clone()..translate(alignedTranslation.dx, alignedTranslation.dy);
final Quad nextViewport = _transformViewport(nextMatrix, _viewport);
if (_boundaryRect.isInfinite && !fixOffset) {
_updateScroll(nextMatrix);
widget.onPositionUpdate?.call(_getMatrixTranslation(nextMatrix));
return nextMatrix;
}
final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(_boundaryRect, 0.0);
final Offset offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport);
if (offendingDistance == Offset.zero) {
_updateScroll(nextMatrix);
widget.onPositionUpdate?.call(_getMatrixTranslation(nextMatrix));
return nextMatrix;
}
final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix);
final double currentScale = matrix.getMaxScaleOnAxis();
final Offset correctedTotalTranslation = Offset(
nextTotalTranslation.dx - offendingDistance.dx * currentScale,
nextTotalTranslation.dy - offendingDistance.dy * currentScale,
);
final Matrix4 correctedMatrix = matrix.clone()
..setTranslation(Vector3(correctedTotalTranslation.dx, correctedTotalTranslation.dy, 0.0));
final Quad correctedViewport = _transformViewport(correctedMatrix, _viewport);
final Offset offendingCorrectedDistance = _exceedsBy(boundariesAabbQuad, correctedViewport);
if (offendingCorrectedDistance == Offset.zero && !fixOffset) {
_updateScroll(correctedMatrix);
widget.onPositionUpdate?.call(_getMatrixTranslation(correctedMatrix));
return correctedMatrix;
}
if (offendingCorrectedDistance.dx != 0.0 &&
offendingCorrectedDistance.dy != 0.0 &&
!fixOffset &&
(childSize.width > parentSize.width || childSize.height > parentSize.height)) {
return matrix.clone();
}
final Offset unidirectionalCorrectedTotalTranslation = Offset(
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
);
final verticalMidLength = (parentSize.height - childSize.height * matrix.getMaxScaleOnAxis()) / 2;
final horizontalMidLength = (parentSize.width - (childSize.width * matrix.getMaxScaleOnAxis())) / 2;
double horizontalMid = 0;
double verticalMid = 0;
void calculateMids(bool sizeCondition) {
if (sizeCondition) {
verticalMid = verticalMidLength;
if (childSize.width < parentSize.width) {
horizontalMid = horizontalMidLength;
}
} else {
horizontalMid = horizontalMidLength;
if (childSize.height < parentSize.height) {
verticalMid = verticalMidLength;
}
}
}
if (childSize.width == childSize.height) {
calculateMids(parentSize.height > parentSize.width);
} else {
calculateMids(childSize.height < childSize.width);
}
final midMatrix = matrix.clone()
..setTranslation(
Vector3(
unidirectionalCorrectedTotalTranslation.dx +
(widget.centerOnScale
? horizontalMid < 0
? 0
: horizontalMid
: 0),
unidirectionalCorrectedTotalTranslation.dy +
(widget.centerOnScale
? verticalMid < 0
? 0
: verticalMid
: 0),
0.0,
),
);
_updateScroll(midMatrix);
widget.onPositionUpdate?.call(_getMatrixTranslation(midMatrix));
return midMatrix;
}
Matrix4 _matrixScale(Matrix4 matrix, double scale, {bool fixScale = false}) {
double sensibleScale =
scale > 1.0 ? 1.0 + ((scale - 1.0) * widget.zoomSensibility) : 1.0 - ((1.0 - scale) * widget.zoomSensibility);
if (scale == 1.0) {
return matrix.clone();
}
assert(scale != 0.0);
final nextScale = (matrix.clone()..scale(sensibleScale)).getMaxScaleOnAxis();
if (nextScale < widget.minScale) {
// 如果即将缩放到的比例小于最小值,则拒绝此次缩放操作,返回当前矩阵
return matrix.clone();
}
if (childSize.width == childSize.height) {
if (parentSize.height > parentSize.width) {
if ((childSize.width * nextScale) < parentSize.width && nextScale < 1.0) {
return matrix.clone();
}
} else {
if ((childSize.height * nextScale) < parentSize.height && nextScale < 1.0) {
return matrix.clone();
}
}
} else {
if (childSize.height < childSize.width) {
if ((childSize.width * nextScale) < parentSize.width && nextScale < 1.0) {
return matrix.clone();
}
} else {
if ((childSize.height * nextScale) < parentSize.height && nextScale < 1.0) {
return matrix.clone();
}
}
}
if (matrix.getMaxScaleOnAxis() > widget.maxScale && sensibleScale > 1) {
return matrix.clone();
}
final newMatrix = matrix.clone()..scale(fixScale ? scale : sensibleScale.abs());
widget.onScaleUpdate?.call(fixScale ? scale : sensibleScale.abs(), newMatrix.getMaxScaleOnAxis());
return newMatrix;
}
bool _gestureIsSupported(_GestureType? gestureType) {
switch (gestureType) {
case _GestureType.scale:
return true;
case _GestureType.pan:
case null:
return true;
}
}
_GestureType _getGestureType(ScaleUpdateDetails details) {
final double scale = details.scale;
if ((scale - 1).abs() != 0) {
return _GestureType.scale;
} else {
return _GestureType.pan;
}
}
void _onScaleStart(ScaleStartDetails details) {
if (_controller.isAnimating) {
_controller.stop();
_controller.reset();
_animation?.removeListener(_onAnimate);
_animation = null;
}
_gestureType = null;
_panAxis = null;
_scaleStart = _transformationController!.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformationController!.toScene(details.localFocalPoint);
}
void _onScaleUpdate(ScaleUpdateDetails details) {
final double scale = _transformationController!.value.getMaxScaleOnAxis();
final Offset focalPointScene = _transformationController!.toScene(details.localFocalPoint);
if (_gestureType == _GestureType.pan) {
_gestureType = _getGestureType(details);
} else {
_gestureType ??= _getGestureType(details);
}
switch (_gestureType!) {
case _GestureType.scale:
assert(_scaleStart != null);
final double desiredScale = _scaleStart! * details.scale;
final double scaleChange = desiredScale / scale;
_transformationController!.value = _matrixScale(_transformationController!.value, scaleChange);
final Offset focalPointSceneScaled = _transformationController!.toScene(details.localFocalPoint);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - _referenceFocalPoint!,
);
final Offset focalPointSceneCheck = _transformationController!.toScene(details.localFocalPoint);
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
_referenceFocalPoint = focalPointSceneCheck;
}
break;
case _GestureType.pan:
assert(_referenceFocalPoint != null);
_panAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene);
final Offset translationChange = focalPointScene - _referenceFocalPoint!;
_transformationController!.value = _matrixTranslate(_transformationController!.value, translationChange);
_referenceFocalPoint = _transformationController!.toScene(details.localFocalPoint);
break;
}
}
void _onScaleEnd(ScaleEndDetails details) {
_scaleStart = null;
_animation?.removeListener(_onAnimate);
_controller.reset();
if (!_gestureIsSupported(_gestureType)) {
_panAxis = null;
return;
}
if (_gestureType != _GestureType.pan || details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
_panAxis = null;
return;
}
final Vector3 translationVector = _transformationController!.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
_kDrag,
translation.dx,
details.velocity.pixelsPerSecond.dx,
);
final FrictionSimulation frictionSimulationY = FrictionSimulation(
_kDrag,
translation.dy,
details.velocity.pixelsPerSecond.dy,
);
final double tFinal = _getFinalTime(details.velocity.pixelsPerSecond.distance, _kDrag);
_animation = Tween<Offset>(
begin: translation,
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
).animate(CurvedAnimation(parent: _controller, curve: Curves.decelerate));
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_controller.forward();
}
void _receivedPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent) {
if (event.scrollDelta.dy == 0.0) {
return;
}
final double scaleChange = math.exp(-event.scrollDelta.dy / 200);
final Offset focalPointScene = _transformationController!.toScene(event.localPosition);
_transformationController!.value = _matrixScale(_transformationController!.value, scaleChange);
final Offset focalPointSceneScaled = _transformationController!.toScene(event.localPosition);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - focalPointScene,
);
}
}
void _onDoubleTap() {
if (!_scaleController.isAnimating && widget.doubleTapZoom) {
doubleTapZoomIn = _transformationController!.value.getMaxScaleOnAxis() < widget.maxScale;
_scaleAnimation = Tween<double>(
begin: _transformationController!.value.getMaxScaleOnAxis(),
end: widget.maxScale,
).animate(CurvedAnimation(parent: _scaleController, curve: Curves.decelerate));
_scaleController.duration = doubleTapZoomIn
? Duration(milliseconds: 100 + widget.doubleTapAnimDuration.inMilliseconds)
: widget.doubleTapAnimDuration;
_scaleAnimation!.addListener(_onAnimateScale);
_scaleController.forward();
}
}
void _onAnimate() {
if (!_controller.isAnimating) {
_panAxis = null;
_animation?.removeListener(_onAnimate);
_animation = null;
_controller.reset();
return;
}
final Vector3 translationVector = _transformationController!.value.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final Offset translationScene = _transformationController!.toScene(translation);
final Offset animationScene = _transformationController!.toScene(_animation!.value);
final Offset translationChangeScene = animationScene - translationScene;
_transformationController!.value = _matrixTranslate(_transformationController!.value, translationChangeScene);
}
void _onAnimateScale() {
if (!_scaleController.isAnimating) {
_scaleAnimation?.removeListener(_onAnimateScale);
_scaleAnimation = null;
_scaleController.reset();
return;
}
double scaleChange;
if (widget.doubleTapScaleChange < 1.0) {
scaleChange = doubleTapZoomIn ? 1.01 : 0.99;
} else {
scaleChange = doubleTapZoomIn ? widget.doubleTapScaleChange : 1 - (widget.doubleTapScaleChange - 1);
}
final Offset focalPointScene = _transformationController!.toScene(_doubleTapFocalPoint ?? Offset.zero);
_transformationController!.value = _matrixScale(_transformationController!.value, scaleChange, fixScale: true);
final Offset focalPointSceneScaled = _transformationController!.toScene(_doubleTapFocalPoint ?? Offset.zero);
Offset diference = focalPointSceneScaled - focalPointScene;
_transformationController!.value = _matrixTranslate(_transformationController!.value, diference);
}
void _onTransformationControllerChange() {
// [修复] 当外部 controller 更新时,调用 _updateScroll 来同步滚动条状态。
// 增加 mounted 和 context 的检查是为了确保在调用时 Widget 已经完成布局,
// 因为 _updateScroll 依赖于 child 和 parent 的尺寸,这让代码更健壮。
if (mounted && _parentKey.currentContext != null && _childKey.currentContext != null) {
_updateScroll(_transformationController!.value);
}
setState(() {});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_transformationController = widget.transformationController ?? TransformationController();
_transformationController!.addListener(_onTransformationControllerChange);
_controller = AnimationController(vsync: this);
_scaleController = AnimationController(vsync: this);
}
@override
void didChangeMetrics() {
setState(() {
recalculateSizes();
});
}
@override
void didUpdateWidget(Zoom oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.transformationController == null) {
if (widget.transformationController != null) {
_transformationController!.removeListener(_onTransformationControllerChange);
_transformationController!.dispose();
_transformationController = widget.transformationController;
_transformationController!.addListener(_onTransformationControllerChange);
}
} else {
if (widget.transformationController == null) {
_transformationController!.removeListener(_onTransformationControllerChange);
_transformationController = TransformationController();
_transformationController!.addListener(_onTransformationControllerChange);
} else if (widget.transformationController != oldWidget.transformationController) {
_transformationController!.removeListener(_onTransformationControllerChange);
_transformationController = widget.transformationController;
_transformationController!.addListener(_onTransformationControllerChange);
}
}
}
@override
void dispose() {
_controller.dispose();
_scaleController.dispose();
_transformationController!.removeListener(_onTransformationControllerChange);
WidgetsBinding.instance.removeObserver(this);
if (widget.transformationController == null) {
_transformationController!.dispose();
}
super.dispose();
}
void fixScale(double scale) {
_transformationController!.value = _matrixScale(_transformationController!.value, scale, fixScale: true);
_transformationController!.toScene(_referenceFocalPoint ?? Offset.zero);
_transformationController!.toScene(_referenceFocalPoint ?? Offset.zero);
}
void recalculateSizes() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_parentKey.currentContext?.findRenderObject() == null) {
return;
}
final RenderBox parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox;
parentSize = parentRenderBox.size;
final RenderBox childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox;
childSize = childRenderBox.size;
double scale = 0;
final currentScale = _transformationController!.value.getMaxScaleOnAxis();
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
Offset(-0.01, -0.01),
fixOffset: true,
);
if (childSize.width == childSize.height) {
if (childSize.width > parentSize.width &&
((childSize.width * currentScale) < parentSize.width ||
(childSize.height * currentScale) < parentSize.height)) {
scale = parentSize.width / (childSize.width * currentScale);
fixScale(scale);
}
} else {
if (childSize.width > childSize.height) {
if (childSize.width > parentSize.width && (childSize.width * currentScale) < parentSize.width) {
scale = parentSize.width / (childSize.width * currentScale);
fixScale(scale);
}
} else {
if (childSize.height > parentSize.height && (childSize.height * currentScale) < parentSize.height) {
scale = parentSize.height / (childSize.height * currentScale);
fixScale(scale);
}
}
}
_transformationController!.value = _matrixTranslate(_transformationController!.value, Offset(-0.01, -0.01));
void fitChild(bool condition) {
if (condition) {
_transformationController!.value = _matrixScale(
_transformationController!.value,
parentSize.height / childSize.height,
fixScale: true,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
Offset(-0.01, -0.01),
fixOffset: true,
);
} else {
_transformationController!.value = _matrixScale(
_transformationController!.value,
parentSize.width / childSize.width,
fixScale: true,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
Offset(-0.01, -0.01),
fixOffset: true,
);
}
}
if (widget.initTotalZoomOut) {
if (firstDraw && (childSize.width > parentSize.width || childSize.height > parentSize.height)) {
if (childSize.width == childSize.height) {
fitChild(parentSize.width > parentSize.height);
} else {
fitChild(childSize.width < childSize.height);
}
firstDraw = false;
}
} else {
if (widget.initScale != null) {
_transformationController!.value = _matrixScale(
_transformationController!.value,
widget.initScale ?? 0.0,
fixScale: true,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
Offset(-0.01, -0.01),
fixOffset: true,
);
}
if (widget.initPosition != null) {
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
widget.initPosition ?? Offset.zero,
);
_referenceFocalPoint = _transformationController!.toScene(widget.initPosition ?? Offset.zero);
}
}
});
}
@override
Widget build(BuildContext context) {
Widget child;
child = _ZoomBuilt(
childKey: _childKey,
constrained: false,
matrix: _transformationController!.value,
child: Listener(
onPointerUp: (event) {
if (widget.onPanUpPosition != null) {
widget.onPanUpPosition!(event.localPosition);
}
},
child: (widget.maxZoomWidth == null || widget.maxZoomHeight == null)
? Container(color: widget.canvasColor, child: widget.child)
: Center(
child: Container(
width: widget.maxZoomWidth,
height: widget.maxZoomHeight,
color: widget.canvasColor,
child: widget.child,
),
),
),
);
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
recalculateSizes();
return true;
},
child: OrientationBuilder(
builder: (context, orientation) {
if (_orientation != orientation) {
_orientation = orientation;
recalculateSizes();
}
double opacity = widget.opacityScrollBars < 0
? 0
: widget.opacityScrollBars > 1
? 1
: widget.opacityScrollBars;
return ClipRect(
child: Container(
color: widget.backgroundColor,
child: Listener(
key: _parentKey,
onPointerSignal: _receivedPointerSignal,
onPointerDown: (PointerDownEvent event) {
_doubleTapFocalPoint = event.localPosition;
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onScaleEnd: _onScaleEnd,
onScaleStart: _onScaleStart,
onScaleUpdate: _onScaleUpdate,
onDoubleTap: _onDoubleTap,
onTap: widget.onTap,
child: widget.enableScroll
? Stack(
children: [
child,
ValueListenableBuilder<_ScrollBarData>(
valueListenable: horizontalScrollNotifier,
builder: (_, scrollData, __) {
return scrollData.length == 0
? Container()
: Positioned(
top: parentSize.height - widget.scrollWeight,
left: scrollData.position,
child: Container(
decoration: BoxDecoration(
color: widget.colorScrollBars.withAlpha((opacity * 255).toInt()),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(widget.radiusScrollBars),
topRight: Radius.circular(widget.radiusScrollBars),
),
),
height: widget.scrollWeight,
width: scrollData.length,
),
);
},
),
ValueListenableBuilder<_ScrollBarData>(
valueListenable: verticalScrollNotifier,
builder: (_, scrollData, __) {
return Positioned(
left: parentSize.width - widget.scrollWeight,
top: scrollData.position,
child: Container(
decoration: BoxDecoration(
color: widget.colorScrollBars.withAlpha((opacity * 255).toInt()),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(widget.radiusScrollBars),
bottomLeft: Radius.circular(widget.radiusScrollBars),
),
),
height: scrollData.length,
width: widget.scrollWeight,
),
);
},
),
],
)
: child,
),
),
),
);
},
),
);
}
}
class _ZoomBuilt extends StatelessWidget {
const _ZoomBuilt({
Key? key,
required this.child,
required this.childKey,
required this.constrained,
required this.matrix,
}) : super(key: key);
final Widget child;
final GlobalKey childKey;
final bool constrained;
final Matrix4 matrix;
@override
Widget build(BuildContext context) {
Widget child = Transform(
transform: matrix,
child: KeyedSubtree(key: childKey, child: this.child),
);
if (!constrained) {
child = OverflowBox(
alignment: Alignment.topLeft,
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: child,
);
}
return child;
}
}
class TransformationController extends ValueNotifier<Matrix4> {
TransformationController([Matrix4? value]) : super(value ?? Matrix4.identity());
Offset toScene(Offset viewportPoint) {
final Matrix4 inverseMatrix = Matrix4.inverted(value);
final Vector3 untransformed = inverseMatrix.transform3(Vector3(viewportPoint.dx, viewportPoint.dy, 0));
return Offset(untransformed.x, untransformed.y);
}
}
enum _GestureType { pan, scale }
enum _ScrollType { horizontal, vertical }
class _ScrollBarData {
_ScrollBarData({required this.length, required this.position});
final double position;
final double length;
}
double _getFinalTime(double velocity, double drag) {
const double effectivelyMotionless = 10.0;
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
Offset _getMatrixTranslation(Matrix4 matrix) {
final Vector3 nextTranslation = matrix.getTranslation();
return Offset(nextTranslation.x, nextTranslation.y);
}
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
final Matrix4 inverseMatrix = matrix.clone()..invert();
return Quad.points(
inverseMatrix.transform3(Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0)),
inverseMatrix.transform3(Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0)),
inverseMatrix.transform3(Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0)),
inverseMatrix.transform3(Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0)),
);
}
Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
final Matrix4 rotationMatrix = Matrix4.identity()
..translate(rect.size.width / 2, rect.size.height / 2)
..rotateZ(rotation)
..translate(-rect.size.width / 2, -rect.size.height / 2);
final Quad boundariesRotated = Quad.points(
rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)),
rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)),
);
return Zoom.getAxisAlignedBoundingBox(boundariesRotated);
}
Offset _exceedsBy(Quad boundary, Quad viewport) {
final List<Vector3> viewportPoints = <Vector3>[viewport.point0, viewport.point1, viewport.point2, viewport.point3];
Offset largestExcess = Offset.zero;
for (final Vector3 point in viewportPoints) {
final Vector3 pointInside = Zoom.getNearestPointInside(point, boundary);
final Offset excess = Offset(pointInside.x - point.x, pointInside.y - point.y);
if (excess.dx.abs() > largestExcess.dx.abs()) {
largestExcess = Offset(excess.dx, largestExcess.dy);
}
if (excess.dy.abs() > largestExcess.dy.abs()) {
largestExcess = Offset(largestExcess.dx, excess.dy);
}
}
return _round(largestExcess);
}
Offset _round(Offset offset) {
return Offset(double.parse(offset.dx.toStringAsFixed(9)), double.parse(offset.dy.toStringAsFixed(9)));
}
Axis? _getPanAxis(Offset point1, Offset point2) {
if (point1 == point2) {
return null;
}
final double x = point2.dx - point1.dx;
final double y = point2.dy - point1.dy;
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
}