|
|
|
|
@@ -2,28 +2,20 @@
|
|
|
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
// ignore_for_file: uri_does_not_exist_in_doc_import
|
|
|
|
|
|
|
|
|
|
/// @docImport 'editable_text.dart';
|
|
|
|
|
/// @docImport 'scroll_view.dart';
|
|
|
|
|
/// @docImport 'table.dart';
|
|
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart' show clampDouble;
|
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter/physics.dart';
|
|
|
|
|
import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3;
|
|
|
|
|
|
|
|
|
|
// Examples can assume:
|
|
|
|
|
// late BuildContext context;
|
|
|
|
|
// late Offset? _childWasTappedAt;
|
|
|
|
|
// late TransformationController _transformationController;
|
|
|
|
|
// Widget child = const Placeholder();
|
|
|
|
|
|
|
|
|
|
/// A signature for widget builders that take a [Quad] of the current viewport.
|
|
|
|
|
///
|
|
|
|
|
/// See also:
|
|
|
|
|
///
|
|
|
|
|
/// * [InteractiveViewer.builder], whose builder is of this type.
|
|
|
|
|
/// * [WidgetBuilder], which is similar, but takes no viewport.
|
|
|
|
|
typedef InteractiveViewerWidgetBuilder =
|
|
|
|
|
Widget Function(BuildContext context, Quad viewport);
|
|
|
|
|
import 'package:vector_math/vector_math_64.dart' show Quad, Vector3;
|
|
|
|
|
|
|
|
|
|
/// A widget that enables pan and zoom interactions with its child.
|
|
|
|
|
///
|
|
|
|
|
@@ -428,43 +420,19 @@ class InteractiveViewer extends StatefulWidget {
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y)),
|
|
|
|
|
);
|
|
|
|
|
return Quad.points(
|
|
|
|
|
Vector3(minX, minY, 0),
|
|
|
|
|
@@ -529,7 +497,8 @@ class InteractiveViewer extends StatefulWidget {
|
|
|
|
|
|
|
|
|
|
class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
with TickerProviderStateMixin {
|
|
|
|
|
TransformationController? _transformationController;
|
|
|
|
|
late TransformationController _transformer =
|
|
|
|
|
widget.transformationController ?? TransformationController();
|
|
|
|
|
|
|
|
|
|
final GlobalKey _childKey = GlobalKey();
|
|
|
|
|
final GlobalKey _parentKey = GlobalKey();
|
|
|
|
|
@@ -611,10 +580,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Matrix4 nextMatrix = matrix.clone()
|
|
|
|
|
..translate(
|
|
|
|
|
alignedTranslation.dx,
|
|
|
|
|
alignedTranslation.dy,
|
|
|
|
|
);
|
|
|
|
|
..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1);
|
|
|
|
|
|
|
|
|
|
// Transform the viewport to determine where its four corners will be after
|
|
|
|
|
// the child has been transformed.
|
|
|
|
|
@@ -712,8 +678,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
|
|
|
|
|
// Don't allow a scale that results in an overall scale beyond min/max
|
|
|
|
|
// scale.
|
|
|
|
|
final double currentScale = _transformationController!.value
|
|
|
|
|
.getMaxScaleOnAxis();
|
|
|
|
|
final double currentScale = _transformer.value.getMaxScaleOnAxis();
|
|
|
|
|
final double totalScale = math.max(
|
|
|
|
|
currentScale * scale,
|
|
|
|
|
// Ensure that the scale cannot make the child so big that it can't fit
|
|
|
|
|
@@ -729,7 +694,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
widget.maxScale,
|
|
|
|
|
);
|
|
|
|
|
final double clampedScale = clampedTotalScale / currentScale;
|
|
|
|
|
return matrix.clone()..scale(clampedScale);
|
|
|
|
|
return matrix.clone()
|
|
|
|
|
..scaleByDouble(clampedScale, clampedScale, clampedScale, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return a new matrix representing the given matrix after applying the given
|
|
|
|
|
@@ -738,13 +704,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
if (rotation == 0) {
|
|
|
|
|
return matrix.clone();
|
|
|
|
|
}
|
|
|
|
|
final Offset focalPointScene = _transformationController!.toScene(
|
|
|
|
|
focalPoint,
|
|
|
|
|
);
|
|
|
|
|
final Offset focalPointScene = _transformer.toScene(focalPoint);
|
|
|
|
|
return matrix.clone()
|
|
|
|
|
..translate(focalPointScene.dx, focalPointScene.dy)
|
|
|
|
|
..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1)
|
|
|
|
|
..rotateZ(-rotation)
|
|
|
|
|
..translate(-focalPointScene.dx, -focalPointScene.dy);
|
|
|
|
|
..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns true iff the given _GestureType is enabled.
|
|
|
|
|
@@ -776,8 +740,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// with GestureDetector's scale gesture.
|
|
|
|
|
void _onScaleStart(ScaleStartDetails details) {
|
|
|
|
|
if (widget.isAnimating?.call() == true ||
|
|
|
|
|
(details.pointerCount < 2 &&
|
|
|
|
|
_transformationController?.value.row0.x == 1.0)) {
|
|
|
|
|
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
|
|
|
|
widget.onPanStart?.call(details);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -788,23 +751,21 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
_controller
|
|
|
|
|
..stop()
|
|
|
|
|
..reset();
|
|
|
|
|
_animation?.removeListener(_onAnimate);
|
|
|
|
|
_animation?.removeListener(_handleInertiaAnimation);
|
|
|
|
|
_animation = null;
|
|
|
|
|
}
|
|
|
|
|
if (_scaleController.isAnimating) {
|
|
|
|
|
_scaleController
|
|
|
|
|
..stop()
|
|
|
|
|
..reset();
|
|
|
|
|
_scaleAnimation?.removeListener(_onScaleAnimate);
|
|
|
|
|
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
|
|
|
|
_scaleAnimation = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_gestureType = null;
|
|
|
|
|
_currentAxis = null;
|
|
|
|
|
_scaleStart = _transformationController!.value.getMaxScaleOnAxis();
|
|
|
|
|
_referenceFocalPoint = _transformationController!.toScene(
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
_scaleStart = _transformer.value.getMaxScaleOnAxis();
|
|
|
|
|
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
|
|
|
|
_rotationStart = _currentRotation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -812,15 +773,14 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// handled with GestureDetector's scale gesture.
|
|
|
|
|
void _onScaleUpdate(ScaleUpdateDetails details) {
|
|
|
|
|
if (widget.isAnimating?.call() == true ||
|
|
|
|
|
(details.pointerCount < 2 &&
|
|
|
|
|
_transformationController?.value.row0.x == 1.0)) {
|
|
|
|
|
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
|
|
|
|
widget.onPanUpdate?.call(details);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final double scale = _transformationController!.value.getMaxScaleOnAxis();
|
|
|
|
|
final double scale = _transformer.value.getMaxScaleOnAxis();
|
|
|
|
|
_scaleAnimationFocalPoint = details.localFocalPoint;
|
|
|
|
|
final Offset focalPointScene = _transformationController!.toScene(
|
|
|
|
|
final Offset focalPointScene = _transformer.toScene(
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -846,20 +806,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// previous call to _onScaleUpdate.
|
|
|
|
|
final double desiredScale = _scaleStart! * details.scale;
|
|
|
|
|
final double scaleChange = desiredScale / scale;
|
|
|
|
|
_transformationController!.value = _matrixScale(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
scaleChange,
|
|
|
|
|
);
|
|
|
|
|
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
|
|
|
|
|
|
|
|
|
// While scaling, translate such that the user's two fingers stay on
|
|
|
|
|
// the same places in the scene. That means that the focal point of
|
|
|
|
|
// the scale should be on the same place in the scene before and after
|
|
|
|
|
// the scale.
|
|
|
|
|
final Offset focalPointSceneScaled = _transformationController!.toScene(
|
|
|
|
|
final Offset focalPointSceneScaled = _transformer.toScene(
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
_transformationController!.value = _matrixTranslate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
focalPointSceneScaled - _referenceFocalPoint!,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -868,7 +825,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// the translate came in contact with a boundary. In that case, update
|
|
|
|
|
// _referenceFocalPoint so subsequent updates happen in relation to
|
|
|
|
|
// the new effective focal point.
|
|
|
|
|
final Offset focalPointSceneCheck = _transformationController!.toScene(
|
|
|
|
|
final Offset focalPointSceneCheck = _transformer.toScene(
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
|
|
|
|
|
@@ -881,8 +838,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final double desiredRotation = _rotationStart! + details.rotation;
|
|
|
|
|
_transformationController!.value = _matrixRotate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
_transformer.value = _matrixRotate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
_currentRotation - desiredRotation,
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
@@ -902,13 +859,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// focal point before and after the movement.
|
|
|
|
|
final Offset translationChange =
|
|
|
|
|
focalPointScene - _referenceFocalPoint!;
|
|
|
|
|
_transformationController!.value = _matrixTranslate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
translationChange,
|
|
|
|
|
);
|
|
|
|
|
_referenceFocalPoint = _transformationController!.toScene(
|
|
|
|
|
details.localFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
|
|
|
|
|
}
|
|
|
|
|
widget.onInteractionUpdate?.call(details);
|
|
|
|
|
}
|
|
|
|
|
@@ -916,12 +871,11 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
// Handle the end of a gesture of _GestureType. All of pan, scale, and rotate
|
|
|
|
|
// are handled with GestureDetector's scale gesture.
|
|
|
|
|
void _onScaleEnd(ScaleEndDetails details) {
|
|
|
|
|
if (_transformationController?.value.row0.x == 1.0) {
|
|
|
|
|
if (_transformer.value.row0.x == 1.0) {
|
|
|
|
|
widget.onReset?.call();
|
|
|
|
|
}
|
|
|
|
|
if (widget.isAnimating?.call() == true ||
|
|
|
|
|
(details.pointerCount < 2 &&
|
|
|
|
|
_transformationController?.value.row0.x == 1.0)) {
|
|
|
|
|
(details.pointerCount < 2 && _transformer.value.row0.x == 1.0)) {
|
|
|
|
|
widget.onPanEnd?.call(details);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -931,8 +885,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
_rotationStart = null;
|
|
|
|
|
_referenceFocalPoint = null;
|
|
|
|
|
|
|
|
|
|
_animation?.removeListener(_onAnimate);
|
|
|
|
|
_scaleAnimation?.removeListener(_onScaleAnimate);
|
|
|
|
|
_animation?.removeListener(_handleInertiaAnimation);
|
|
|
|
|
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
|
|
|
|
_controller.reset();
|
|
|
|
|
_scaleController.reset();
|
|
|
|
|
|
|
|
|
|
@@ -947,8 +901,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
_currentAxis = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final Vector3 translationVector = _transformationController!.value
|
|
|
|
|
.getTranslation();
|
|
|
|
|
final Vector3 translationVector = _transformer.value.getTranslation();
|
|
|
|
|
final Offset translation = Offset(
|
|
|
|
|
translationVector.x,
|
|
|
|
|
translationVector.y,
|
|
|
|
|
@@ -975,21 +928,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
frictionSimulationY.finalX,
|
|
|
|
|
),
|
|
|
|
|
).animate(
|
|
|
|
|
CurvedAnimation(
|
|
|
|
|
parent: _controller,
|
|
|
|
|
curve: Curves.decelerate,
|
|
|
|
|
),
|
|
|
|
|
CurvedAnimation(parent: _controller, curve: Curves.decelerate),
|
|
|
|
|
);
|
|
|
|
|
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
|
|
|
|
|
_animation!.addListener(_onAnimate);
|
|
|
|
|
_animation!.addListener(_handleInertiaAnimation);
|
|
|
|
|
_controller.forward();
|
|
|
|
|
case _GestureType.scale:
|
|
|
|
|
if (details.scaleVelocity.abs() < 0.1) {
|
|
|
|
|
_currentAxis = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final double scale = _transformationController!.value
|
|
|
|
|
.getMaxScaleOnAxis();
|
|
|
|
|
final double scale = _transformer.value.getMaxScaleOnAxis();
|
|
|
|
|
final FrictionSimulation frictionSimulation = FrictionSimulation(
|
|
|
|
|
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
|
|
|
|
|
scale,
|
|
|
|
|
@@ -1013,7 +962,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
_scaleController.duration = Duration(
|
|
|
|
|
milliseconds: (tFinal * 1000).round(),
|
|
|
|
|
);
|
|
|
|
|
_scaleAnimation!.addListener(_onScaleAnimate);
|
|
|
|
|
_scaleAnimation!.addListener(_handleScaleAnimation);
|
|
|
|
|
_scaleController.forward();
|
|
|
|
|
case _GestureType.rotate || null:
|
|
|
|
|
break;
|
|
|
|
|
@@ -1022,20 +971,19 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
|
|
|
|
|
// Handle mousewheel and web trackpad scroll events.
|
|
|
|
|
void _receivedPointerSignal(PointerSignalEvent event) {
|
|
|
|
|
final Offset local = event.localPosition;
|
|
|
|
|
final Offset global = event.position;
|
|
|
|
|
final double scaleChange;
|
|
|
|
|
if (event is PointerScrollEvent) {
|
|
|
|
|
if (event.kind == PointerDeviceKind.trackpad &&
|
|
|
|
|
!widget.trackpadScrollCausesScale) {
|
|
|
|
|
// Trackpad scroll, so treat it as a pan.
|
|
|
|
|
widget.onInteractionStart?.call(
|
|
|
|
|
ScaleStartDetails(
|
|
|
|
|
focalPoint: event.position,
|
|
|
|
|
localFocalPoint: event.localPosition,
|
|
|
|
|
),
|
|
|
|
|
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final Offset localDelta = PointerEvent.transformDeltaViaPositions(
|
|
|
|
|
untransformedEndPosition: event.position + event.scrollDelta,
|
|
|
|
|
untransformedEndPosition: global + event.scrollDelta,
|
|
|
|
|
untransformedDelta: event.scrollDelta,
|
|
|
|
|
transform: event.transform,
|
|
|
|
|
);
|
|
|
|
|
@@ -1043,8 +991,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
if (!_gestureIsSupported(_GestureType.pan)) {
|
|
|
|
|
widget.onInteractionUpdate?.call(
|
|
|
|
|
ScaleUpdateDetails(
|
|
|
|
|
focalPoint: event.position - event.scrollDelta,
|
|
|
|
|
localFocalPoint: event.localPosition - event.scrollDelta,
|
|
|
|
|
focalPoint: global - event.scrollDelta,
|
|
|
|
|
localFocalPoint: local - event.scrollDelta,
|
|
|
|
|
focalPointDelta: -localDelta,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
@@ -1052,23 +1000,20 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Offset focalPointScene = _transformationController!.toScene(
|
|
|
|
|
event.localPosition,
|
|
|
|
|
final Offset focalPointScene = _transformer.toScene(local);
|
|
|
|
|
final Offset newFocalPointScene = _transformer.toScene(
|
|
|
|
|
local - localDelta,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final Offset newFocalPointScene = _transformationController!.toScene(
|
|
|
|
|
event.localPosition - localDelta,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
_transformationController!.value = _matrixTranslate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
newFocalPointScene - focalPointScene,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
widget.onInteractionUpdate?.call(
|
|
|
|
|
ScaleUpdateDetails(
|
|
|
|
|
focalPoint: event.position - event.scrollDelta,
|
|
|
|
|
localFocalPoint: event.localPosition - localDelta,
|
|
|
|
|
focalPoint: global - event.scrollDelta,
|
|
|
|
|
localFocalPoint: local - localDelta,
|
|
|
|
|
focalPointDelta: -localDelta,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
@@ -1086,17 +1031,14 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
widget.onInteractionStart?.call(
|
|
|
|
|
ScaleStartDetails(
|
|
|
|
|
focalPoint: event.position,
|
|
|
|
|
localFocalPoint: event.localPosition,
|
|
|
|
|
),
|
|
|
|
|
ScaleStartDetails(focalPoint: global, localFocalPoint: local),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!_gestureIsSupported(_GestureType.scale)) {
|
|
|
|
|
widget.onInteractionUpdate?.call(
|
|
|
|
|
ScaleUpdateDetails(
|
|
|
|
|
focalPoint: event.position,
|
|
|
|
|
localFocalPoint: event.localPosition,
|
|
|
|
|
focalPoint: global,
|
|
|
|
|
localFocalPoint: local,
|
|
|
|
|
scale: scaleChange,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
@@ -1104,95 +1046,75 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final Offset focalPointScene = _transformationController!.toScene(
|
|
|
|
|
event.localPosition,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
_transformationController!.value = _matrixScale(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
scaleChange,
|
|
|
|
|
);
|
|
|
|
|
final Offset focalPointScene = _transformer.toScene(local);
|
|
|
|
|
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
|
|
|
|
|
|
|
|
|
// After scaling, translate such that the event's position is at the
|
|
|
|
|
// same scene point before and after the scale.
|
|
|
|
|
final Offset focalPointSceneScaled = _transformationController!.toScene(
|
|
|
|
|
event.localPosition,
|
|
|
|
|
);
|
|
|
|
|
_transformationController!.value = _matrixTranslate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
final Offset focalPointSceneScaled = _transformer.toScene(local);
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
focalPointSceneScaled - focalPointScene,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
widget.onInteractionUpdate?.call(
|
|
|
|
|
ScaleUpdateDetails(
|
|
|
|
|
focalPoint: event.position,
|
|
|
|
|
localFocalPoint: event.localPosition,
|
|
|
|
|
focalPoint: global,
|
|
|
|
|
localFocalPoint: local,
|
|
|
|
|
scale: scaleChange,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
widget.onInteractionEnd?.call(ScaleEndDetails());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle inertia drag animation.
|
|
|
|
|
void _onAnimate() {
|
|
|
|
|
void _handleInertiaAnimation() {
|
|
|
|
|
if (!_controller.isAnimating) {
|
|
|
|
|
_currentAxis = null;
|
|
|
|
|
_animation?.removeListener(_onAnimate);
|
|
|
|
|
_animation?.removeListener(_handleInertiaAnimation);
|
|
|
|
|
_animation = null;
|
|
|
|
|
_controller.reset();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Translate such that the resulting translation is _animation.value.
|
|
|
|
|
final Vector3 translationVector = _transformationController!.value
|
|
|
|
|
.getTranslation();
|
|
|
|
|
final Vector3 translationVector = _transformer.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,
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
_transformer.toScene(_animation!.value) -
|
|
|
|
|
_transformer.toScene(translation),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle inertia scale animation.
|
|
|
|
|
void _onScaleAnimate() {
|
|
|
|
|
void _handleScaleAnimation() {
|
|
|
|
|
if (!_scaleController.isAnimating) {
|
|
|
|
|
_currentAxis = null;
|
|
|
|
|
_scaleAnimation?.removeListener(_onScaleAnimate);
|
|
|
|
|
_scaleAnimation?.removeListener(_handleScaleAnimation);
|
|
|
|
|
_scaleAnimation = null;
|
|
|
|
|
_scaleController.reset();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final double desiredScale = _scaleAnimation!.value;
|
|
|
|
|
final double scaleChange =
|
|
|
|
|
desiredScale / _transformationController!.value.getMaxScaleOnAxis();
|
|
|
|
|
final Offset referenceFocalPoint = _transformationController!.toScene(
|
|
|
|
|
desiredScale / _transformer.value.getMaxScaleOnAxis();
|
|
|
|
|
final Offset referenceFocalPoint = _transformer.toScene(
|
|
|
|
|
_scaleAnimationFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
_transformationController!.value = _matrixScale(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
scaleChange,
|
|
|
|
|
);
|
|
|
|
|
_transformer.value = _matrixScale(_transformer.value, scaleChange);
|
|
|
|
|
|
|
|
|
|
// While scaling, translate such that the user's two fingers stay on
|
|
|
|
|
// the same places in the scene. That means that the focal point of
|
|
|
|
|
// the scale should be on the same place in the scene before and after
|
|
|
|
|
// the scale.
|
|
|
|
|
final Offset focalPointSceneScaled = _transformationController!.toScene(
|
|
|
|
|
final Offset focalPointSceneScaled = _transformer.toScene(
|
|
|
|
|
_scaleAnimationFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
_transformationController!.value = _matrixTranslate(
|
|
|
|
|
_transformationController!.value,
|
|
|
|
|
_transformer.value = _matrixTranslate(
|
|
|
|
|
_transformer.value,
|
|
|
|
|
focalPointSceneScaled - referenceFocalPoint,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _onTransformationControllerChange() {
|
|
|
|
|
void _handleTransformation() {
|
|
|
|
|
// A change to the TransformationController's value is a change to the
|
|
|
|
|
// state.
|
|
|
|
|
setState(() {});
|
|
|
|
|
@@ -1201,63 +1123,36 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
|
|
|
|
|
_transformationController =
|
|
|
|
|
widget.transformationController ?? TransformationController();
|
|
|
|
|
_transformationController!.addListener(_onTransformationControllerChange);
|
|
|
|
|
_controller = AnimationController(
|
|
|
|
|
vsync: this,
|
|
|
|
|
);
|
|
|
|
|
_controller = AnimationController(vsync: this);
|
|
|
|
|
_scaleController = AnimationController(vsync: this);
|
|
|
|
|
|
|
|
|
|
_transformer.addListener(_handleTransformation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(InteractiveViewer oldWidget) {
|
|
|
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
|
// Handle all cases of needing to dispose and initialize
|
|
|
|
|
// transformationControllers.
|
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final TransformationController? newController =
|
|
|
|
|
widget.transformationController;
|
|
|
|
|
if (newController == oldWidget.transformationController) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_transformer.removeListener(_handleTransformation);
|
|
|
|
|
if (oldWidget.transformationController == null) {
|
|
|
|
|
_transformer.dispose();
|
|
|
|
|
}
|
|
|
|
|
_transformer = newController ?? TransformationController();
|
|
|
|
|
_transformer.addListener(_handleTransformation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
_scaleController.dispose();
|
|
|
|
|
_transformationController!.removeListener(
|
|
|
|
|
_onTransformationControllerChange,
|
|
|
|
|
);
|
|
|
|
|
_transformer.removeListener(_handleTransformation);
|
|
|
|
|
if (widget.transformationController == null) {
|
|
|
|
|
_transformationController!.dispose();
|
|
|
|
|
_transformer.dispose();
|
|
|
|
|
}
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
@@ -1270,7 +1165,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
childKey: _childKey,
|
|
|
|
|
clipBehavior: widget.clipBehavior,
|
|
|
|
|
constrained: widget.constrained,
|
|
|
|
|
matrix: _transformationController!.value,
|
|
|
|
|
matrix: _transformer.value,
|
|
|
|
|
alignment: widget.alignment,
|
|
|
|
|
child: widget.child!,
|
|
|
|
|
);
|
|
|
|
|
@@ -1281,7 +1176,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
assert(!widget.constrained);
|
|
|
|
|
child = LayoutBuilder(
|
|
|
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
|
|
|
final Matrix4 matrix = _transformationController!.value;
|
|
|
|
|
final Matrix4 matrix = _transformer.value;
|
|
|
|
|
return _InteractiveViewerBuilt(
|
|
|
|
|
childKey: _childKey,
|
|
|
|
|
clipBehavior: widget.clipBehavior,
|
|
|
|
|
@@ -1301,8 +1196,7 @@ class _InteractiveViewerState extends State<InteractiveViewer>
|
|
|
|
|
key: _parentKey,
|
|
|
|
|
onPointerSignal: _receivedPointerSignal,
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
behavior:
|
|
|
|
|
HitTestBehavior.translucent, // Necessary when panning off screen.
|
|
|
|
|
behavior: HitTestBehavior.opaque, // Necessary when panning off screen.
|
|
|
|
|
onScaleEnd: _onScaleEnd,
|
|
|
|
|
onScaleStart: _onScaleStart,
|
|
|
|
|
onScaleUpdate: _onScaleUpdate,
|
|
|
|
|
@@ -1338,10 +1232,7 @@ class _InteractiveViewerBuilt extends StatelessWidget {
|
|
|
|
|
Widget child = Transform(
|
|
|
|
|
transform: matrix,
|
|
|
|
|
alignment: alignment,
|
|
|
|
|
child: KeyedSubtree(
|
|
|
|
|
key: childKey,
|
|
|
|
|
child: this.child,
|
|
|
|
|
),
|
|
|
|
|
child: KeyedSubtree(key: childKey, child: this.child),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!constrained) {
|
|
|
|
|
@@ -1355,83 +1246,13 @@ class _InteractiveViewerBuilt extends StatelessWidget {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ClipRect(
|
|
|
|
|
clipBehavior: clipBehavior,
|
|
|
|
|
child: child,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A thin wrapper on [ValueNotifier] whose value is a [Matrix4] representing a
|
|
|
|
|
/// transformation.
|
|
|
|
|
///
|
|
|
|
|
/// The [value] defaults to the identity matrix, which corresponds to no
|
|
|
|
|
/// transformation.
|
|
|
|
|
///
|
|
|
|
|
/// See also:
|
|
|
|
|
///
|
|
|
|
|
/// * [InteractiveViewer.transformationController] for detailed documentation
|
|
|
|
|
/// on how to use TransformationController with [InteractiveViewer].
|
|
|
|
|
class TransformationController extends ValueNotifier<Matrix4> {
|
|
|
|
|
/// Create an instance of [TransformationController].
|
|
|
|
|
///
|
|
|
|
|
/// The [value] defaults to the identity matrix, which corresponds to no
|
|
|
|
|
/// transformation.
|
|
|
|
|
TransformationController([Matrix4? value])
|
|
|
|
|
: super(value ?? Matrix4.identity());
|
|
|
|
|
|
|
|
|
|
/// Return the scene point at the given viewport point.
|
|
|
|
|
///
|
|
|
|
|
/// A viewport point is relative to the parent while a scene point is relative
|
|
|
|
|
/// to the child, regardless of transformation. Calling toScene with a
|
|
|
|
|
/// viewport point essentially returns the scene coordinate that lies
|
|
|
|
|
/// underneath the viewport point given the transform.
|
|
|
|
|
///
|
|
|
|
|
/// The viewport transforms as the inverse of the child (i.e. moving the child
|
|
|
|
|
/// left is equivalent to moving the viewport right).
|
|
|
|
|
///
|
|
|
|
|
/// This method is often useful when determining where an event on the parent
|
|
|
|
|
/// occurs on the child. This example shows how to determine where a tap on
|
|
|
|
|
/// the parent occurred on the child.
|
|
|
|
|
///
|
|
|
|
|
/// ```dart
|
|
|
|
|
/// @override
|
|
|
|
|
/// Widget build(BuildContext context) {
|
|
|
|
|
/// return GestureDetector(
|
|
|
|
|
/// onTapUp: (TapUpDetails details) {
|
|
|
|
|
/// _childWasTappedAt = _transformationController.toScene(
|
|
|
|
|
/// details.localPosition,
|
|
|
|
|
/// );
|
|
|
|
|
/// },
|
|
|
|
|
/// child: InteractiveViewer(
|
|
|
|
|
/// transformationController: _transformationController,
|
|
|
|
|
/// child: child,
|
|
|
|
|
/// ),
|
|
|
|
|
/// );
|
|
|
|
|
/// }
|
|
|
|
|
/// ```
|
|
|
|
|
Offset toScene(Offset viewportPoint) {
|
|
|
|
|
// On viewportPoint, perform the inverse transformation of the scene to get
|
|
|
|
|
// where the point would be in the scene before the transformation.
|
|
|
|
|
final Matrix4 inverseMatrix = Matrix4.inverted(value);
|
|
|
|
|
final Vector3 untransformed = inverseMatrix.transform3(
|
|
|
|
|
Vector3(
|
|
|
|
|
viewportPoint.dx,
|
|
|
|
|
viewportPoint.dy,
|
|
|
|
|
0,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return Offset(untransformed.x, untransformed.y);
|
|
|
|
|
return ClipRect(clipBehavior: clipBehavior, child: child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A classification of relevant user gestures. Each contiguous user gesture is
|
|
|
|
|
// represented by exactly one _GestureType.
|
|
|
|
|
enum _GestureType {
|
|
|
|
|
pan,
|
|
|
|
|
scale,
|
|
|
|
|
rotate,
|
|
|
|
|
}
|
|
|
|
|
enum _GestureType { pan, scale, rotate }
|
|
|
|
|
|
|
|
|
|
// Given a velocity and drag, calculate the time at which motion will come to
|
|
|
|
|
// a stop, within the margin of effectivelyMotionless.
|
|
|
|
|
@@ -1457,32 +1278,16 @@ 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,
|
|
|
|
|
),
|
|
|
|
|
Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0),
|
|
|
|
|
),
|
|
|
|
|
inverseMatrix.transform3(
|
|
|
|
|
Vector3(
|
|
|
|
|
viewport.topRight.dx,
|
|
|
|
|
viewport.topRight.dy,
|
|
|
|
|
0.0,
|
|
|
|
|
),
|
|
|
|
|
Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0),
|
|
|
|
|
),
|
|
|
|
|
inverseMatrix.transform3(
|
|
|
|
|
Vector3(
|
|
|
|
|
viewport.bottomRight.dx,
|
|
|
|
|
viewport.bottomRight.dy,
|
|
|
|
|
0.0,
|
|
|
|
|
),
|
|
|
|
|
Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0),
|
|
|
|
|
),
|
|
|
|
|
inverseMatrix.transform3(
|
|
|
|
|
Vector3(
|
|
|
|
|
viewport.bottomLeft.dx,
|
|
|
|
|
viewport.bottomLeft.dy,
|
|
|
|
|
0.0,
|
|
|
|
|
),
|
|
|
|
|
Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
@@ -1491,9 +1296,9 @@ Quad _transformViewport(Matrix4 matrix, Rect viewport) {
|
|
|
|
|
// the given amount.
|
|
|
|
|
Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) {
|
|
|
|
|
final Matrix4 rotationMatrix = Matrix4.identity()
|
|
|
|
|
..translate(rect.size.width / 2, rect.size.height / 2)
|
|
|
|
|
..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1)
|
|
|
|
|
..rotateZ(rotation)
|
|
|
|
|
..translate(-rect.size.width / 2, -rect.size.height / 2);
|
|
|
|
|
..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1);
|
|
|
|
|
final Quad boundariesRotated = Quad.points(
|
|
|
|
|
rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)),
|
|
|
|
|
rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)),
|
|
|
|
|
@@ -1562,20 +1367,3 @@ Axis? _getPanAxis(Offset point1, Offset point2) {
|
|
|
|
|
final double y = point2.dy - point1.dy;
|
|
|
|
|
return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This enum is used to specify the behavior of the [InteractiveViewer] when
|
|
|
|
|
/// the user drags the viewport.
|
|
|
|
|
enum PanAxis {
|
|
|
|
|
/// The user can only pan the viewport along the horizontal axis.
|
|
|
|
|
horizontal,
|
|
|
|
|
|
|
|
|
|
/// The user can only pan the viewport along the vertical axis.
|
|
|
|
|
vertical,
|
|
|
|
|
|
|
|
|
|
/// The user can pan the viewport along the horizontal and vertical axes
|
|
|
|
|
/// but not diagonally.
|
|
|
|
|
aligned,
|
|
|
|
|
|
|
|
|
|
/// The user can pan the viewport freely in any direction.
|
|
|
|
|
free,
|
|
|
|
|
}
|
|
|
|
|
|