diff --git a/README.md b/README.md index 6dbe0e1..0c91d1b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # matrix_gesture_detector -A gesture detector which detects translation, scale and rotation gestures +`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` diff --git a/example/lib/transform_demo3.dart b/example/lib/transform_demo3.dart index 5f8ef93..2c8c9b6 100644 --- a/example/lib/transform_demo3.dart +++ b/example/lib/transform_demo3.dart @@ -15,7 +15,6 @@ class _TransformDemo3State extends State { static const double radius0 = 0.0; Matrix4 matrix = Matrix4.identity(); - double angle = 0.0; double radius = radius0; Color color = color0; ValueNotifier notifier = ValueNotifier(0); @@ -42,9 +41,7 @@ class _TransformDemo3State extends State { onMatrixUpdate: (m, tm, sm, rm) { matrix = MatrixGestureDetector.compose(matrix, tm, sm, null); - var array = rm.applyToVector3Array([0, 0, 0, 1, 0, 0]); - Offset rotated = Offset(array[3] - array[0], array[4] - array[1]); - angle += rotated.direction; + var angle = MatrixGestureDetector.decomposeToValues(m).rotation; double t = (1 - cos(2 * angle)) / 2; radius = radiusTween.transform(t); diff --git a/example/lib/transform_demo4.dart b/example/lib/transform_demo4.dart index 3c7541b..a7a65d4 100644 --- a/example/lib/transform_demo4.dart +++ b/example/lib/transform_demo4.dart @@ -53,17 +53,21 @@ class TransformDemo4State extends State title: Text('Transform Demo 4'), ), body: Column( - children: makeControls() + makeMainWidget(getLabel()), + children: makeControls() + makeMainWidget(getBody()), ), ); } - String getLabel() { - String prefix = 'use your fingers to '; - if (shouldRotate && shouldScale) return prefix + 'rotate / scale'; - if (shouldRotate) return prefix + 'rotate'; - if (shouldScale) return prefix + 'scale'; - return 'you have to select at least one checkbox above'; + Body getBody() { + String lbl = 'use your fingers to '; + if (shouldRotate && shouldScale) + return Body(lbl + 'rotate / scale', Icons.crop_rotate, Color(0x6600aa00)); + if (shouldRotate) + return Body(lbl + 'rotate', Icons.crop_rotate, Color(0x6600aa00)); + if (shouldScale) + return Body(lbl + 'scale', Icons.transform, Color(0x660000aa)); + return Body('you have to select at least one checkbox above', Icons.warning, + Color(0x66aa0000)); } Animation makeFocalPointAnimation(Alignment begin, Alignment end) { @@ -106,7 +110,7 @@ class TransformDemo4State extends State ), ]; - List makeMainWidget(String label) => [ + List makeMainWidget(Body body) => [ Expanded( child: Padding( padding: const EdgeInsets.all(32.0), @@ -123,37 +127,66 @@ class TransformDemo4State extends State foregroundPainter: FocalPointPainter(focalPointAnimation), child: AnimatedBuilder( animation: notifier, - builder: (ctx, child) { - return Transform( - transform: notifier.value, - child: GridPaper( - color: Color(0xaa0000ff), - child: Container( - decoration: BoxDecoration( - border: Border.all(width: 4.0, color: Colors.blue), - borderRadius: - BorderRadius.all(Radius.circular(32.0)), - ), - child: Container( - decoration: FlutterLogoDecoration(), - padding: EdgeInsets.all(32), - alignment: Alignment(0, -0.5), - child: Text( - label, - style: Theme.of(context).textTheme.display2, - textAlign: TextAlign.center, - ), - ), - ), - ), - ); - }, + builder: (ctx, child) => makeTransform(ctx, child, body), ), ), ), ), ) ]; + + Widget makeTransform(BuildContext context, Widget child, Body body) { + return Transform( + transform: notifier.value, + child: GridPaper( + color: Color(0xaa0000ff), + child: Container( + decoration: BoxDecoration( + border: Border.all(width: 4.0, color: Color(0xaa00cc00)), + borderRadius: BorderRadius.all(Radius.circular(32.0)), + ), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 400), + transitionBuilder: (child, animation) => ScaleTransition( + scale: animation, + child: child, + alignment: focalPoint, + ), + switchInCurve: Curves.ease, + switchOutCurve: Curves.ease, + child: Stack( + key: ValueKey('$shouldRotate-$shouldScale'), + fit: StackFit.expand, + children: [ + FittedBox( + child: Icon( + body.icon, + color: body.color, + ), + ), + Container( + alignment: Alignment(0, -0.5), + child: Text( + body.label, + style: Theme.of(context).textTheme.display2, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class Body { + String label; + IconData icon; + Color color; + + Body(this.label, this.icon, this.color); } class FocalPointPainter extends CustomPainter { diff --git a/lib/matrix_gesture_detector.dart b/lib/matrix_gesture_detector.dart index 4ac0bb5..eb5e038 100644 --- a/lib/matrix_gesture_detector.dart +++ b/lib/matrix_gesture_detector.dart @@ -11,7 +11,7 @@ typedef MatrixGestureDetectorCallback = void Function( Matrix4 scaleDeltaMatrix, Matrix4 rotationDeltaMatrix); -/// A gesture detector which detects translation, scale and rotation gestures +/// [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] @@ -87,6 +87,19 @@ class MatrixGestureDetector extends StatefulWidget { if (rotationMatrix != null) matrix = rotationMatrix * matrix; return matrix; } + + /// + /// Decomposes [matrix] into [MatrixDecomposedValues.translation], + /// [MatrixDecomposedValues.scale] and [MatrixDecomposedValues.rotation] components. + /// + static MatrixDecomposedValues 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 MatrixDecomposedValues(translation, scale, rotation); + } } class _MatrixGestureDetectorState extends State { @@ -165,25 +178,46 @@ class _MatrixGestureDetectorState extends State { } Matrix4 _translate(Offset translation) { - return Matrix4.translationValues(translation.dx, translation.dy, 0.0); + 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) { - Matrix4 matrix = Matrix4.diagonal3Values(scale, scale, 1); var dx = (1 - scale) * focalPoint.dx; var dy = (1 - scale) * focalPoint.dy; - matrix.setTranslationRaw(dx, dy, 0.0); - return matrix; + + // ..[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) { - Matrix4 matrix = Matrix4.rotationZ(angle); - var cosa = cos(angle); - var sina = sin(angle); - var dx = (1 - cosa) * focalPoint.dx + sina * focalPoint.dy; - var dy = (1 - cosa) * focalPoint.dy - sina * focalPoint.dx; - matrix.setTranslationRaw(dx, dy, 0.0); - return matrix; + 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); } } @@ -201,3 +235,22 @@ class _ValueUpdater { return updated; } } + +class MatrixDecomposedValues { + /// 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; + + MatrixDecomposedValues(this.translation, this.scale, this.rotation); + + @override + String toString() { + return 'MatrixDecomposedValues(translation: $translation, scale: ${scale.toStringAsFixed(3)}, rotation: ${rotation.toStringAsFixed(3)})'; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index aa3b512..31df8b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: matrix_gesture_detector description: A gesture detector mapping translation/rotation/scale gestures to a Matrix4 object. -version: 0.0.6 +version: 0.1.0 author: pskink homepage: https://github.com/pskink/matrix_gesture_detector