From db51537896ed033c5c16a20685e6e740b165bc0e Mon Sep 17 00:00:00 2001 From: Vishesh Handa <me@vhanda.in> Date: Tue, 23 Feb 2021 10:13:14 +0100 Subject: [PATCH] Remove fork of InteractiveViewer The fixes are in flutter stable --- lib/core/interactive_viewer.dart | 1259 ------------------------------ lib/screens/graph_view.dart | 7 +- 2 files changed, 3 insertions(+), 1263 deletions(-) delete mode 100644 lib/core/interactive_viewer.dart diff --git a/lib/core/interactive_viewer.dart b/lib/core/interactive_viewer.dart deleted file mode 100644 index d69d99a5..00000000 --- a/lib/core/interactive_viewer.dart +++ /dev/null @@ -1,1259 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// @dart = 2.8 - -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; - -/// A widget that enables pan and zoom interactions with its child. -/// -/// The user can transform the child by dragging to pan or pinching to zoom. -/// -/// By default, InteractiveViewer may draw outside of its original area of the -/// screen, such as when a child is zoomed in and increases in size. However, it -/// will not receive gestures outside of its original area. To prevent -/// InteractiveViewer from drawing outside of its original size, wrap it in a -/// [ClipRect]. Or, to prevent dead areas where InteractiveViewer does not -/// receive gestures, be sure that the InteractiveViewer widget is the size of -/// the area that should be interactive. See -/// [flutter-go](https://github.com/justinmc/flutter-go) for an example of -/// robust positioning of an InteractiveViewer child that works for all screen -/// sizes and child sizes. -/// -/// The [child] must not be null. -/// -/// See also: -/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart), -/// which includes the use of InteractiveViewer. -/// -/// {@tool dartpad --template=stateless_widget_scaffold} -/// This example shows a simple Container that can be panned and zoomed. -/// -/// ```dart -/// Widget build(BuildContext context) { -/// return Center( -/// child: InteractiveViewer( -/// boundaryMargin: EdgeInsets.all(20.0), -/// minScale: 0.1, -/// maxScale: 1.6, -/// child: Container( -/// decoration: BoxDecoration( -/// gradient: LinearGradient( -/// begin: Alignment.topCenter, -/// end: Alignment.bottomCenter, -/// colors: <Color>[Colors.orange, Colors.red], -/// stops: <double>[0.0, 1.0], -/// ), -/// ), -/// ), -/// ), -/// ); -/// } -/// ``` -/// {@end-tool} -@immutable -class InteractiveViewer extends StatefulWidget { - /// Create an InteractiveViewer. - /// - /// The [child] parameter must not be null. - InteractiveViewer({ - Key key, - this.alignPanAxis = false, - this.boundaryMargin = EdgeInsets.zero, - this.constrained = true, - // These default scale values were eyeballed as reasonable limits for common - // use cases. - this.maxScale = 2.5, - this.minScale = 0.8, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, - this.panEnabled = true, - this.scaleEnabled = true, - this.transformationController, - @required this.child, - }) : assert(alignPanAxis != null), - assert(child != null), - assert(constrained != null), - assert(minScale != null), - assert(minScale > 0), - assert(minScale.isFinite), - assert(maxScale != null), - assert(maxScale > 0), - assert(!maxScale.isNaN), - assert(maxScale >= minScale), - assert(panEnabled != null), - assert(scaleEnabled != null), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. - assert((boundaryMargin.horizontal.isInfinite && - boundaryMargin.vertical.isInfinite) || - (boundaryMargin.top.isFinite && - boundaryMargin.right.isFinite && - boundaryMargin.bottom.isFinite && - boundaryMargin.left.isFinite)), - super(key: key); - - /// If true, panning is only allowed in the direction of the horizontal axis - /// or the vertical axis. - /// - /// In other words, when this is true, diagonal panning is not allowed. A - /// single gesture begun along one axis cannot also cause panning along the - /// other axis without stopping and beginning a new gesture. This is a common - /// pattern in tables where data is displayed in columns and rows. - final bool alignPanAxis; - - /// A margin for the visible boundaries of the child. - /// - /// Any transformation that results in the viewport being able to view outside - /// of the boundaries will be stopped at the boundary. The boundaries do not - /// rotate with the rest of the scene, so they are always aligned with the - /// viewport. - /// - /// To produce no boundaries at all, pass infinite [EdgeInsets], such as - /// `EdgeInsets.all(double.infinity)`. - /// - /// No edge can be NaN. - /// - /// Defaults to [EdgeInsets.zero], which results in boundaries that are the - /// exact same size and position as the [child]. - final EdgeInsets boundaryMargin; - - /// The Widget to perform the transformations on. - /// - /// Cannot be null. - final Widget child; - - /// Whether the normal size constraints at this point in the widget tree are - /// applied to the child. - /// - /// If set to false, then the child will be given infinite constraints. This - /// is often useful when a child should be bigger than the InteractiveViewer. - /// - /// Defaults to true. - /// - /// {@tool dartpad --template=stateless_widget_scaffold} - /// This example shows how to create a pannable table. Because the table is - /// larger than the entire screen, setting `constrained` to false is necessary - /// to allow it to be drawn to its full size. The parts of the table that - /// exceed the screen size can then be panned into view. - /// - /// ```dart - /// Widget build(BuildContext context) { - /// const int _rowCount = 20; - /// const int _columnCount = 3; - /// - /// return Scaffold( - /// appBar: AppBar( - /// title: const Text('Pannable Table'), - /// ), - /// body: InteractiveViewer( - /// constrained: false, - /// scaleEnabled: false, - /// child: Table( - /// columnWidths: <int, TableColumnWidth>{ - /// for (int column = 0; column < _columnCount; column += 1) - /// column: const FixedColumnWidth(300.0), - /// }, - /// children: <TableRow>[ - /// for (int row = 0; row < _rowCount; row += 1) - /// TableRow( - /// children: <Widget>[ - /// for (int column = 0; column < _columnCount; column += 1) - /// Container( - /// height: 100, - /// color: row % 2 + column % 2 == 1 ? Colors.red : Colors.green, - /// ), - /// ], - /// ), - /// ], - /// ), - /// ), - /// ); - /// } - /// ``` - /// {@end-tool} - final bool constrained; - - /// If false, the user will be prevented from panning. - /// - /// Defaults to true. - /// - /// See also: - /// - /// * [scaleEnabled], which is similar but for scale. - final bool panEnabled; - - /// If false, the user will be prevented from scaling. - /// - /// Defaults to true. - /// - /// See also: - /// - /// * [panEnabled], which is similar but for panning. - final bool scaleEnabled; - - /// The maximum allowed scale. - /// - /// The scale will be clamped between this and [minScale] inclusively. - /// - /// Defaults to 2.5. - /// - /// Cannot be null, and must be greater than zero and greater than minScale. - final double maxScale; - - /// The minimum allowed scale. - /// - /// The scale will be clamped between this and [maxScale] inclusively. - /// - /// Defaults to 0.8. - /// - /// Cannot be null, and must be a finite number greater than zero and less - /// than maxScale. - final double minScale; - - /// Called when the user ends a pan or scale gesture on the widget. - /// - /// {@template flutter.widgets.interactiveViewer.onInteraction} - /// Will be called even if the interaction is disabled with - /// [panEnabled] or [scaleEnabled]. - /// - /// A [GestureDetector] wrapping the InteractiveViewer will not respond to - /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and - /// [GestureDetector.onScaleEnd]. Use [onInteractionStart], - /// [onInteractionUpdate], and [onInteractionEnd] to respond to those - /// gestures. - /// - /// The coordinates returned in the details are viewport coordinates relative - /// to the parent. See [TransformationController.toScene] for how to - /// convert the coordinates to scene coordinates relative to the child. - /// {@endtemplate} - /// - /// See also: - /// - /// * [onInteractionStart], which handles the start of the same interaction. - /// * [onInteractionUpdate], which handles an update to the same interaction. - final GestureScaleEndCallback onInteractionEnd; - - /// Called when the user begins a pan or scale gesture on the widget. - /// - /// {@macro flutter.widgets.interactiveViewer.onInteraction} - /// - /// See also: - /// - /// * [onInteractionUpdate], which handles an update to the same interaction. - /// * [onInteractionEnd], which handles the end of the same interaction. - final GestureScaleStartCallback onInteractionStart; - - /// Called when the user updates a pan or scale gesture on the widget. - /// - /// {@macro flutter.widgets.interactiveViewer.onInteraction} - /// - /// See also: - /// - /// * [onInteractionStart], which handles the start of the same interaction. - /// * [onInteractionEnd], which handles the end of the same interaction. - final GestureScaleUpdateCallback onInteractionUpdate; - - /// A [TransformationController] for the transformation performed on the - /// child. - /// - /// Whenever the child is transformed, the [Matrix4] value is updated and all - /// listeners are notified. If the value is set, InteractiveViewer will update - /// to respect the new value. - /// - /// {@tool dartpad --template=stateful_widget_material_ticker} - /// This example shows how transformationController can be used to animate the - /// transformation back to its starting position. - /// - /// ```dart - /// final TransformationController _transformationController = TransformationController(); - /// Animation<Matrix4> _animationReset; - /// AnimationController _controllerReset; - /// - /// void _onAnimateReset() { - /// _transformationController.value = _animationReset.value; - /// if (!_controllerReset.isAnimating) { - /// _animationReset?.removeListener(_onAnimateReset); - /// _animationReset = null; - /// _controllerReset.reset(); - /// } - /// } - /// - /// void _animateResetInitialize() { - /// _controllerReset.reset(); - /// _animationReset = Matrix4Tween( - /// begin: _transformationController.value, - /// end: Matrix4.identity(), - /// ).animate(_controllerReset); - /// _animationReset.addListener(_onAnimateReset); - /// _controllerReset.forward(); - /// } - /// - /// // Stop a running reset to home transform animation. - /// void _animateResetStop() { - /// _controllerReset.stop(); - /// _animationReset?.removeListener(_onAnimateReset); - /// _animationReset = null; - /// _controllerReset.reset(); - /// } - /// - /// void _onInteractionStart(ScaleStartDetails details) { - /// // If the user tries to cause a transformation while the reset animation is - /// // running, cancel the reset animation. - /// if (_controllerReset.status == AnimationStatus.forward) { - /// _animateResetStop(); - /// } - /// } - /// - /// @override - /// void initState() { - /// super.initState(); - /// _controllerReset = AnimationController( - /// vsync: this, - /// duration: const Duration(milliseconds: 400), - /// ); - /// } - /// - /// @override - /// void dispose() { - /// _controllerReset.dispose(); - /// super.dispose(); - /// } - /// - /// @override - /// Widget build(BuildContext context) { - /// return Scaffold( - /// backgroundColor: Theme.of(context).colorScheme.primary, - /// appBar: AppBar( - /// automaticallyImplyLeading: false, - /// title: const Text('Controller demo'), - /// ), - /// body: Center( - /// child: InteractiveViewer( - /// boundaryMargin: EdgeInsets.all(double.infinity), - /// transformationController: _transformationController, - /// minScale: 0.1, - /// maxScale: 1.0, - /// onInteractionStart: _onInteractionStart, - /// child: Container( - /// decoration: BoxDecoration( - /// gradient: LinearGradient( - /// begin: Alignment.topCenter, - /// end: Alignment.bottomCenter, - /// colors: <Color>[Colors.orange, Colors.red], - /// stops: <double>[0.0, 1.0], - /// ), - /// ), - /// ), - /// ), - /// ), - /// persistentFooterButtons: [ - /// IconButton( - /// onPressed: _animateResetInitialize, - /// tooltip: 'Reset', - /// color: Theme.of(context).colorScheme.surface, - /// icon: const Icon(Icons.replay), - /// ), - /// ], - /// ); - /// } - /// ``` - /// {@end-tool} - /// - /// See also: - /// - /// * [ValueNotifier], the parent class of TransformationController. - /// * [TextEditingController] for an example of another similar pattern. - final TransformationController transformationController; - - /// Returns the closest point to the given point on the given line segment. - @visibleForTesting - 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(); - - // In this case, l1 == l2. - if (lengthSquared == 0) { - return l1; - } - - // Calculate how far down the line segment the closest point is and return - // the point. - final Vector3 l1P = point - l1; - final Vector3 l1L2 = l2 - l1; - final double fraction = - (l1P.dot(l1L2) / lengthSquared).clamp(0.0, 1.0).toDouble(); - return l1 + l1L2 * fraction; - } - - /// Given a quad, return its axis aligned bounding box. - @visibleForTesting - 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), - ); - } - - /// Returns true iff the point is inside the rectangle given by the Quad, - /// inclusively. - /// Algorithm from https://math.stackexchange.com/a/190373. - @visibleForTesting - 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; - } - - /// Get the point inside (inclusively) the given Quad that is nearest to the - /// given Vector3. - @visibleForTesting - static Vector3 getNearestPointInside(Vector3 point, Quad quad) { - // If the point is inside the axis aligned bounding box, then it's ok where - // it is. - if (pointIsInside(point, quad)) { - return point; - } - - // Otherwise, return the nearest point on the quad. - final List<Vector3> closestPoints = <Vector3>[ - InteractiveViewer.getNearestPointOnLine(point, quad.point0, quad.point1), - InteractiveViewer.getNearestPointOnLine(point, quad.point1, quad.point2), - InteractiveViewer.getNearestPointOnLine(point, quad.point2, quad.point3), - InteractiveViewer.getNearestPointOnLine(point, quad.point3, quad.point0), - ]; - double minDistance = double.infinity; - 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 - _InteractiveViewerState createState() => _InteractiveViewerState(); -} - -class _InteractiveViewerState extends State<InteractiveViewer> - with TickerProviderStateMixin { - TransformationController _transformationController; - - final GlobalKey _childKey = GlobalKey(); - final GlobalKey _parentKey = GlobalKey(); - Animation<Offset> _animation; - AnimationController _controller; - Axis _panAxis; // Used with alignPanAxis. - Offset _referenceFocalPoint; // Point where the current gesture began. - double _scaleStart; // Scale value at start of scaling gesture. - double _rotationStart = 0.0; // Rotation at start of rotation gesture. - double _currentRotation = 0.0; // Rotation of _transformationController.value. - _GestureType _gestureType; - - // TODO(justinmc): Add rotateEnabled parameter to the widget and remove this - // hardcoded value when the rotation feature is implemented. - // https://github.com/flutter/flutter/issues/57698 - final bool _rotateEnabled = false; - - // Used as the coefficient of friction in the inertial translation animation. - // This value was eyeballed to give a feel similar to Google Photos. - static const double _kDrag = 0.0000135; - - // The _boundaryRect is calculated by adding the boundaryMargin to the size of - // the child. - Rect get _boundaryRect { - assert(_childKey.currentContext != null); - assert(!widget.boundaryMargin.left.isNaN); - assert(!widget.boundaryMargin.right.isNaN); - assert(!widget.boundaryMargin.top.isNaN); - assert(!widget.boundaryMargin.bottom.isNaN); - - final RenderBox childRenderBox = - _childKey.currentContext.findRenderObject() as RenderBox; - final Size childSize = childRenderBox.size; - final Rect boundaryRect = - widget.boundaryMargin.inflateRect(Offset.zero & childSize); - // Boundaries that are partially infinite are not allowed because Matrix4's - // rotation and translation methods don't handle infinites well. - 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; - } - - // The Rect representing the child's parent. - Rect get _viewport { - assert(_parentKey.currentContext != null); - final RenderBox parentRenderBox = - _parentKey.currentContext.findRenderObject() as RenderBox; - return Offset.zero & parentRenderBox.size; - } - - // Return a new matrix representing the given matrix after applying the given - // translation. - Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { - if (translation == Offset.zero) { - return matrix.clone(); - } - - final Offset alignedTranslation = widget.alignPanAxis && _panAxis != null - ? _alignAxis(translation, _panAxis) - : translation; - - final Matrix4 nextMatrix = matrix.clone() - ..translate( - alignedTranslation.dx, - alignedTranslation.dy, - ); - - // Transform the viewport to determine where its four corners will be after - // the child has been transformed. - final Quad nextViewport = _transformViewport(nextMatrix, _viewport); - - // If the boundaries are infinite, then no need to check if the translation - // fits within them. - if (_boundaryRect.isInfinite) { - return nextMatrix; - } - - // Expand the boundaries with rotation. This prevents the problem where a - // mismatch in orientation between the viewport and boundaries effectively - // limits translation. With this approach, all points that are visible with - // no rotation are visible after rotation. - final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation( - _boundaryRect, - _currentRotation, - ); - - // If the given translation fits completely within the boundaries, allow it. - final Offset offendingDistance = - _exceedsBy(boundariesAabbQuad, nextViewport); - if (offendingDistance == Offset.zero) { - return nextMatrix; - } - - // Desired translation goes out of bounds, so translate to the nearest - // in-bounds point instead. - final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix); - final double currentScale = matrix.getMaxScaleOnAxis(); - final Offset correctedTotalTranslation = Offset( - nextTotalTranslation.dx - offendingDistance.dx * currentScale, - nextTotalTranslation.dy - offendingDistance.dy * currentScale, - ); - // TODO(justinmc): This needs some work to handle rotation properly. The - // idea is that the boundaries are axis aligned (boundariesAabbQuad), but - // calculating the translation to put the viewport inside that Quad is more - // complicated than this when rotated. - // https://github.com/flutter/flutter/issues/57698 - final Matrix4 correctedMatrix = matrix.clone() - ..setTranslation(Vector3( - correctedTotalTranslation.dx, - correctedTotalTranslation.dy, - 0.0, - )); - - // Double check that the corrected translation fits. - final Quad correctedViewport = - _transformViewport(correctedMatrix, _viewport); - final Offset offendingCorrectedDistance = - _exceedsBy(boundariesAabbQuad, correctedViewport); - if (offendingCorrectedDistance == Offset.zero) { - return correctedMatrix; - } - - // If the corrected translation doesn't fit in either direction, don't allow - // any translation at all. This happens when the viewport is larger than the - // entire boundary. - if (offendingCorrectedDistance.dx != 0.0 && - offendingCorrectedDistance.dy != 0.0) { - return matrix.clone(); - } - - // Otherwise, allow translation in only the direction that fits. This - // happens when the viewport is larger than the boundary in one direction. - final Offset unidirectionalCorrectedTotalTranslation = Offset( - offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, - offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, - ); - return matrix.clone() - ..setTranslation(Vector3( - unidirectionalCorrectedTotalTranslation.dx, - unidirectionalCorrectedTotalTranslation.dy, - 0.0, - )); - } - - // Return a new matrix representing the given matrix after applying the given - // scale. - Matrix4 _matrixScale(Matrix4 matrix, double scale) { - if (scale == 1.0) { - return matrix.clone(); - } - assert(scale != 0.0); - - // Don't allow a scale that results in an overall scale beyond min/max - // scale. - final double currentScale = - _transformationController.value.getMaxScaleOnAxis(); - final double totalScale = currentScale * scale; - final double clampedTotalScale = totalScale.clamp( - widget.minScale, - widget.maxScale, - ) as double; - final double clampedScale = clampedTotalScale / currentScale; - final Matrix4 nextMatrix = matrix.clone()..scale(clampedScale); - - // Ensure that the scale cannot make the child so big that it can't fit - // inside the boundaries (in either direction). - final double minScale = math.max( - _viewport.width / _boundaryRect.width, - _viewport.height / _boundaryRect.height, - ); - if (clampedTotalScale < minScale) { - final double minCurrentScale = minScale / currentScale; - return matrix.clone()..scale(minCurrentScale); - } - - return nextMatrix; - } - - // Return a new matrix representing the given matrix after applying the given - // rotation. - Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { - if (rotation == 0) { - return matrix.clone(); - } - final Offset focalPointScene = _transformationController.toScene( - focalPoint, - ); - return matrix.clone() - ..translate(focalPointScene.dx, focalPointScene.dy) - ..rotateZ(-rotation) - ..translate(-focalPointScene.dx, -focalPointScene.dy); - } - - // Returns true iff the given _GestureType is enabled. - bool _gestureIsSupported(_GestureType gestureType) { - switch (gestureType) { - case _GestureType.rotate: - return _rotateEnabled; - - case _GestureType.scale: - return widget.scaleEnabled; - - case _GestureType.pan: - default: - return widget.panEnabled; - } - } - - // Decide which type of gesture this is by comparing the amount of scale - // and rotation in the gesture, if any. Scale starts at 1 and rotation - // starts at 0. Pan will have no scale and no rotation because it uses only one - // finger. - _GestureType _getGestureType(ScaleUpdateDetails details) { - final double scale = !widget.scaleEnabled ? 1.0 : details.scale; - final double rotation = !_rotateEnabled ? 0.0 : details.rotation; - if ((scale - 1).abs() > rotation.abs()) { - return _GestureType.scale; - } else if (rotation != 0.0) { - return _GestureType.rotate; - } else { - return _GestureType.pan; - } - } - - // Handle the start of a gesture. All of pan, scale, and rotate are handled - // with GestureDetector's scale gesture. - void _onScaleStart(ScaleStartDetails details) { - if (widget.onInteractionStart != null) { - widget.onInteractionStart(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, - ); - _rotationStart = _currentRotation; - } - - // Handle an update to an ongoing gesture. All of pan, scale, and rotate are - // handled with GestureDetector's scale gesture. - void _onScaleUpdate(ScaleUpdateDetails details) { - final double scale = _transformationController.value.getMaxScaleOnAxis(); - if (widget.onInteractionUpdate != null) { - widget.onInteractionUpdate(ScaleUpdateDetails( - focalPoint: _transformationController.toScene( - details.localFocalPoint, - ), - scale: details.scale, - rotation: details.rotation, - )); - } - final Offset focalPointScene = _transformationController.toScene( - details.localFocalPoint, - ); - - if (_gestureType == _GestureType.pan) { - // When a gesture first starts, it sometimes has no change in scale and - // rotation despite being a two-finger gesture. Here the gesture is - // allowed to be reinterpreted as its correct type after originally - // being marked as a pan. - _gestureType = _getGestureType(details); - } else { - _gestureType ??= _getGestureType(details); - } - if (!_gestureIsSupported(_gestureType)) { - return; - } - - switch (_gestureType) { - case _GestureType.scale: - assert(_scaleStart != null); - // details.scale gives us the amount to change the scale as of the - // start of this gesture, so calculate the amount to scale as of the - // previous call to _onScaleUpdate. - final double desiredScale = _scaleStart * details.scale; - final double scaleChange = desiredScale / scale; - _transformationController.value = _matrixScale( - _transformationController.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( - details.localFocalPoint, - ); - _transformationController.value = _matrixTranslate( - _transformationController.value, - focalPointSceneScaled - _referenceFocalPoint, - ); - - // details.localFocalPoint should now be at the same location as the - // original _referenceFocalPoint point. If it's not, that's because - // 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( - details.localFocalPoint, - ); - if (_round(_referenceFocalPoint) != _round(focalPointSceneCheck)) { - _referenceFocalPoint = focalPointSceneCheck; - } - return; - - case _GestureType.rotate: - if (details.rotation == 0.0) { - return; - } - final double desiredRotation = _rotationStart + details.rotation; - _transformationController.value = _matrixRotate( - _transformationController.value, - _currentRotation - desiredRotation, - details.localFocalPoint, - ); - _currentRotation = desiredRotation; - return; - - case _GestureType.pan: - assert(_referenceFocalPoint != null); - // details may have a change in scale here when scaleEnabled is false. - // In an effort to keep the behavior similar whether or not scaleEnabled - // is true, these gestures are thrown away. - if (details.scale != 1.0) { - return; - } - _panAxis ??= _getPanAxis(_referenceFocalPoint, focalPointScene); - // Translate so that the same point in the scene is underneath the - // focal point before and after the movement. - final Offset translationChange = focalPointScene - _referenceFocalPoint; - _transformationController.value = _matrixTranslate( - _transformationController.value, - translationChange, - ); - _referenceFocalPoint = _transformationController.toScene( - details.localFocalPoint, - ); - return; - } - } - - // 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 (widget.onInteractionEnd != null) { - widget.onInteractionEnd(details); - } - _scaleStart = null; - _rotationStart = null; - _referenceFocalPoint = null; - - _animation?.removeListener(_onAnimate); - _controller.reset(); - - if (!_gestureIsSupported(_gestureType)) { - _panAxis = null; - return; - } - - // If the scale ended with enough velocity, animate inertial movement. - 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(); - } - - // Handle mousewheel scroll events. - void _receivedPointerSignal(PointerSignalEvent event) { - if (!_gestureIsSupported(_GestureType.scale)) { - return; - } - if (event is PointerScrollEvent) { - final RenderBox childRenderBox = - _childKey.currentContext.findRenderObject() as RenderBox; - final Size childSize = childRenderBox.size; - final double scaleChange = 1.0 - event.scrollDelta.dy / childSize.height; - if (scaleChange == 0.0) { - return; - } - final Offset focalPointScene = _transformationController.toScene( - event.localPosition, - ); - _transformationController.value = _matrixScale( - _transformationController.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, - focalPointSceneScaled - focalPointScene, - ); - } - } - - // Handle inertia drag animation. - void _onAnimate() { - if (!_controller.isAnimating) { - _panAxis = null; - _animation?.removeListener(_onAnimate); - _animation = null; - _controller.reset(); - return; - } - // Translate such that the resulting translation is _animation.value. - 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 _onTransformationControllerChange() { - // A change to the TransformationController's value is a change to the - // state. - setState(() {}); - } - - @override - void initState() { - super.initState(); - - _transformationController = - widget.transformationController ?? TransformationController(); - _transformationController.addListener(_onTransformationControllerChange); - _controller = AnimationController( - vsync: this, - ); - } - - @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); - } - } - } - - @override - void dispose() { - _controller.dispose(); - _transformationController.removeListener(_onTransformationControllerChange); - if (widget.transformationController == null) { - _transformationController.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child = Transform( - transform: _transformationController.value, - child: KeyedSubtree( - key: _childKey, - child: widget.child, - ), - ); - - if (!widget.constrained) { - child = ClipRect( - child: OverflowBox( - alignment: Alignment.topLeft, - minWidth: 0.0, - minHeight: 0.0, - maxWidth: double.infinity, - maxHeight: double.infinity, - child: child, - ), - ); - } - - // A GestureDetector allows the detection of panning and zooming gestures on - // the child. - return Listener( - key: _parentKey, - onPointerSignal: _receivedPointerSignal, - child: GestureDetector( - behavior: HitTestBehavior.opaque, // Necessary when panning off screen. - onScaleEnd: _onScaleEnd, - onScaleStart: _onScaleStart, - onScaleUpdate: _onScaleUpdate, - 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 - /// void 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); - } -} - -// A classification of relevant user gestures. Each contiguous user gesture is -// represented by exactly one _GestureType. -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. -double _getFinalTime(double velocity, double drag) { - const double effectivelyMotionless = 10.0; - return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); -} - -// Return the translation from the given Matrix4 as an Offset. -Offset _getMatrixTranslation(Matrix4 matrix) { - final Vector3 nextTranslation = matrix.getTranslation(); - return Offset(nextTranslation.x, nextTranslation.y); -} - -// Transform the four corners of the viewport by the inverse of the given -// matrix. This gives the viewport after the child has been transformed by the -// given matrix. The viewport transforms as the inverse of the child (i.e. -// moving the child left is equivalent to moving the viewport right). -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, - )), - ); -} - -// Find the axis aligned bounding box for the rect rotated about its center by -// the given amount. -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 InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); -} - -// Return the amount that viewport lies outside of boundary. If the viewport -// is completely contained within the boundary (inclusively), then returns -// Offset.zero. -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 = - InteractiveViewer.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); -} - -// Round the output values. This works around a precision problem where -// values that should have been zero were given as within 10^-10 of zero. -Offset _round(Offset offset) { - return Offset( - double.parse(offset.dx.toStringAsFixed(9)), - double.parse(offset.dy.toStringAsFixed(9)), - ); -} - -// Align the given offset to the given axis by allowing movement only in the -// axis direction. -Offset _alignAxis(Offset offset, Axis axis) { - switch (axis) { - case Axis.horizontal: - return Offset(offset.dx, 0.0); - case Axis.vertical: - default: - return Offset(0.0, offset.dy); - } -} - -// Given two points, return the axis where the distance between the points is -// greatest. If they are equal, return null. -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; -} diff --git a/lib/screens/graph_view.dart b/lib/screens/graph_view.dart index da869a06..b7500022 100644 --- a/lib/screens/graph_view.dart +++ b/lib/screens/graph_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:gitjournal/core/graph.dart'; -import 'package:gitjournal/core/interactive_viewer.dart' as fork; import 'package:gitjournal/core/notes_folder_fs.dart'; class GraphViewScreen extends StatefulWidget { @@ -56,7 +55,7 @@ class GraphView extends StatefulWidget { class _GraphViewState extends State<GraphView> { final nodeSize = 50.0; - fork.TransformationController transformationController; + TransformationController transformationController; @override void initState() { @@ -67,7 +66,7 @@ class _GraphViewState extends State<GraphView> { setState(() {}); }); - transformationController = fork.TransformationController(); + transformationController = TransformationController(); } Offset _getLocationPosition(Offset globalPos) { @@ -143,7 +142,7 @@ class _GraphViewState extends State<GraphView> { ), ); - return fork.InteractiveViewer( + return InteractiveViewer( child: view, panEnabled: true, constrained: false,