pskink / matrix_gesture_detector

A gesture detector mapping translation/rotation/scale gestures to a Matrix4 object.
Other
134 stars 101 forks source link

Initial matrix #18

Open aloisdeniel opened 4 years ago

aloisdeniel commented 4 years ago

It is pretty common to be able to edit an already transformed image.

For this use case, it would be needed to initialize the widget with an initial transform.

MatrixGestureDetector(
    initialMatrix: myMatrix
    onMatrixUpdate: ...
    child: ...
  )
ezorrio commented 4 years ago

+1 on this, might be very useful :)

R1Snake commented 4 years ago

I am searching since a week for a solution to set an initial matrix.

Thump up to get this issue done.

Has anyone a quick and dirty solution for this?

R1Snake commented 4 years ago

I added a initMatrix method for this. I created a new class called: MyMatrixGestureDetector

import 'dart:math';

import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart';

typedef MyMatrixGestureDetectorCallback = void Function( Matrix4 matrix, Matrix4 translationDeltaMatrix, Matrix4 scaleDeltaMatrix, Matrix4 rotationDeltaMatrix);

/// [MatrixGestureDetector] detects translation, scale and rotation gestures /// and combines them into [Matrix4] object that can be used by [Transform] widget /// or by low level [CustomPainter] code. You can customize types of reported /// gestures by passing [shouldTranslate], [shouldScale] and [shouldRotate] /// parameters. /// class MyMatrixGestureDetector extends StatefulWidget { /// [Matrix4] change notification callback /// final MyMatrixGestureDetectorCallback onMatrixUpdate;

/// The [child] contained by this detector. /// /// {@macro flutter.widgets.child} /// final Widget child;

/// Whether to detect translation gestures during the event processing. /// /// Defaults to true. /// final bool shouldTranslate;

final Matrix4 initMatrix;

/// Whether to detect scale gestures during the event processing. /// /// Defaults to true. /// final bool shouldScale;

/// Whether to detect rotation gestures during the event processing. /// /// Defaults to true. /// final bool shouldRotate;

/// Whether [ClipRect] widget should clip [child] widget. /// /// Defaults to true. /// final bool clipChild;

/// When set, it will be used for computing a "fixed" focal point /// aligned relative to the size of this widget. final Alignment focalPointAlignment;

const MyMatrixGestureDetector({ Key key, @required this.onMatrixUpdate, @required this.child, @required this.initMatrix, this.shouldTranslate = true, this.shouldScale = true, this.shouldRotate = true, this.clipChild = true, this.focalPointAlignment, }) : assert(onMatrixUpdate != null), assert(child != null), super(key: key);

@override _MyMatrixGestureDetectorState createState() => _MyMatrixGestureDetectorState();

/// /// Compose the matrix from translation, scale and rotation matrices - you can /// pass a null to skip any matrix from composition. /// /// If [matrix] is not null the result of the composing will be concatenated /// to that [matrix], otherwise the identity matrix will be used. /// static Matrix4 compose(Matrix4 matrix, Matrix4 translationMatrix, Matrix4 scaleMatrix, Matrix4 rotationMatrix) { if (matrix == null) matrix = Matrix4.identity(); if (translationMatrix != null) matrix = translationMatrix matrix; if (scaleMatrix != null) matrix = scaleMatrix matrix; if (rotationMatrix != null) matrix = rotationMatrix * matrix; return matrix; }

/// /// Decomposes [matrix] into [MatrixDecomposedValues.translation], /// [MatrixDecomposedValues.scale] and [MatrixDecomposedValues.rotation] components. /// static MyMatrixDecomposedValues decomposeToValues(Matrix4 matrix) { var array = matrix.applyToVector3Array([0, 0, 0, 1, 0, 0]); Offset translation = Offset(array[0], array[1]); Offset delta = Offset(array[3] - array[0], array[4] - array[1]); double scale = delta.distance; double rotation = delta.direction; return MyMatrixDecomposedValues(translation, scale, rotation); } }

class _MyMatrixGestureDetectorState extends State { Matrix4 translationDeltaMatrix = Matrix4.identity(); Matrix4 scaleDeltaMatrix = Matrix4.identity(); Matrix4 rotationDeltaMatrix = Matrix4.identity(); Matrix4 matrix = Matrix4.identity();

@override Widget build(BuildContext context) { Widget child = widget.clipChild ? ClipRect(child: widget.child) : widget.child; return GestureDetector( onScaleStart: onScaleStart, onScaleUpdate: onScaleUpdate, child: child, ); }

@override void initState() { super.initState(); matrix = widget.initMatrix; }

_ValueUpdater translationUpdater = _ValueUpdater( onUpdate: (oldVal, newVal) => newVal - oldVal, ); _ValueUpdater rotationUpdater = _ValueUpdater( onUpdate: (oldVal, newVal) => newVal - oldVal, ); _ValueUpdater scaleUpdater = _ValueUpdater( onUpdate: (oldVal, newVal) => newVal / oldVal, );

void onScaleStart(ScaleStartDetails details) { translationUpdater.value = details.focalPoint; rotationUpdater.value = double.nan; scaleUpdater.value = 1.0; }

void onScaleUpdate(ScaleUpdateDetails details) { //print(details); translationDeltaMatrix = Matrix4.identity(); scaleDeltaMatrix = Matrix4.identity(); rotationDeltaMatrix = Matrix4.identity();

// handle matrix translating
if (widget.shouldTranslate) {
  Offset translationDelta = translationUpdater.update(details.focalPoint);
  translationDeltaMatrix = _translate(translationDelta);
  print(matrix);
  matrix = translationDeltaMatrix * matrix;
}

Offset focalPoint;
if (widget.focalPointAlignment != null) {
  focalPoint = widget.focalPointAlignment.alongSize(context.size);
} else {
  RenderBox renderBox = context.findRenderObject();
  focalPoint = renderBox.globalToLocal(details.focalPoint);
}

// handle matrix scaling
if (widget.shouldScale && details.scale != 1.0) {
  double scaleDelta = scaleUpdater.update(details.scale);
  scaleDeltaMatrix = _scale(scaleDelta, focalPoint);

  matrix = scaleDeltaMatrix * matrix;
}

// handle matrix rotating
if (widget.shouldRotate && details.rotation != 0.0) {
  if (rotationUpdater.value.isNaN) {
    rotationUpdater.value = details.rotation;
  } else {
    double rotationDelta = rotationUpdater.update(details.rotation);
    rotationDeltaMatrix = _rotate(rotationDelta, focalPoint);
    matrix = rotationDeltaMatrix * matrix;
  }
}

widget.onMatrixUpdate(
    matrix, translationDeltaMatrix, scaleDeltaMatrix, rotationDeltaMatrix);

}

Matrix4 _translate(Offset translation) { var dx = translation.dx; var dy = translation.dy;

//  ..[0]  = 1       # x scale
//  ..[5]  = 1       # y scale
//  ..[10] = 1       # diagonal "one"
//  ..[12] = dx      # x translation
//  ..[13] = dy      # y translation
//  ..[15] = 1       # diagonal "one"
return Matrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);

}

Matrix4 _scale(double scale, Offset focalPoint) { var dx = (1 - scale) focalPoint.dx; var dy = (1 - scale) focalPoint.dy;

//  ..[0]  = scale   # x scale
//  ..[5]  = scale   # y scale
//  ..[10] = 1       # diagonal "one"
//  ..[12] = dx      # x translation
//  ..[13] = dy      # y translation
//  ..[15] = 1       # diagonal "one"
return Matrix4(scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);

}

Matrix4 _rotate(double angle, Offset focalPoint) { var c = cos(angle); var s = sin(angle); var dx = (1 - c) focalPoint.dx + s focalPoint.dy; var dy = (1 - c) focalPoint.dy - s focalPoint.dx;

//  ..[0]  = c       # x scale
//  ..[1]  = s       # y skew
//  ..[4]  = -s      # x skew
//  ..[5]  = c       # y scale
//  ..[10] = 1       # diagonal "one"
//  ..[12] = dx      # x translation
//  ..[13] = dy      # y translation
//  ..[15] = 1       # diagonal "one"
return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);

} }

typedef _OnUpdate = T Function(T oldValue, T newValue);

class _ValueUpdater { final _OnUpdate onUpdate; T value;

_ValueUpdater({this.onUpdate});

T update(T newValue) { T updated = onUpdate(value, newValue); value = newValue; return updated; } }

class MyMatrixDecomposedValues { /// Translation, in most cases useful only for matrices that are nothing but /// a translation (no scale and no rotation). final Offset translation;

/// Scaling factor. final double scale;

/// Rotation in radians, (-pi..pi) range. final double rotation;

MyMatrixDecomposedValues(this.translation, this.scale, this.rotation);

@override String toString() { return 'MatrixDecomposedValues(translation: $translation, scale: ${scale.toStringAsFixed(3)}, rotation: ${rotation.toStringAsFixed(3)})'; } }

In your build method:

return MyMatrixGestureDetector( initMatrix: widget.appearience, onMatrixUpdate: (m, tm, sm, rm) { notifier.value = m; print(m); }, child: AnimatedBuilder( animation: notifier, builder: (ctx, child) { return Transform( transform: notifier.value, child: widget.child, ); }, ), ); }