Flutter: How to implement Rotate and Pan/Move gesture for any container?

7,864

Solution 1

In scale-related events, you can use the focalPoint to calculate panning, in addition to scaling (zooming). Panning while zooming can also be supported.

Demo:

pan and zoom demo

Here's the code used for the above demo:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ZoomAndPanDemo(),
    );
  }
}

class ZoomAndPanDemo extends StatefulWidget {
  @override
  _ZoomAndPanDemoState createState() => _ZoomAndPanDemoState();
}

class _ZoomAndPanDemoState extends State<ZoomAndPanDemo> {
  Offset _offset = Offset.zero;
  Offset _initialFocalPoint = Offset.zero;
  Offset _sessionOffset = Offset.zero;

  double _scale = 1.0;
  double _initialScale = 1.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleStart: (details) {
        _initialFocalPoint = details.focalPoint;
        _initialScale = _scale;
      },
      onScaleUpdate: (details) {
        setState(() {
          _sessionOffset = details.focalPoint - _initialFocalPoint;
          _scale = _initialScale * details.scale;
        });
      },
      onScaleEnd: (details) {
        setState(() {
          _offset += _sessionOffset;
          _sessionOffset = Offset.zero;
        });
      },
      child: Transform.translate(
        offset: _offset + _sessionOffset,
        child: Transform.scale(
          scale: _scale,
          child: FlutterLogo(),
        ),
      ),
    );
  }
}

Side note: even though events like onHorizontalDragUpdate do not cause runtime exception when used with scale-related events, they still cause conflict and will result in an inferior UX.

It's also worth-noting that InteractiveViewer is a built-in Flutter widget that can handle most of your needs, so you might not need to use GestureDetector and Transform at all.

Solution 2

We can use the focalPoint field of ScaleUpdateDetails object, which we get as an argument in the onScaleUpdate function.

Solution related to the above example: We need to update the onScaleUpdate method.

onScaleUpdate: (scaleUpdates) {

      lastRotation += scaleUpdates.rotation;
      var offset = scaleUpdates.focalPoint;
      xOffset = offset.dx;
      yOffset = offset.dy;

      setState(() => _scale = _previousScale * scaleUpdates.scale);
    }

Change 'rect' field of Positioned Widget in above code.

rect: Rect.fromPoints(Offset(xOffset - 125.0, yOffset - 50.0),
              Offset(xOffset + 250.0, yOffset + 100.0))

Solution 3

Default GestureRecognizer does not support the recognition of pan/drag and scaling at the same time. I think this is the bug and it should be fixed. To implement such behavior - you need to build you own recogizer RawGestureDetector based on ImmediateMultiDragGestureRecognizer gestures.

I have already implemented class PanAndScalingGestureRecognizer here: https://gist.github.com/comm1x/8ffffd08417053043e079878b4bd8d03

So you can see full example or just copypaste and use.

Solution 4

I using Pointer Listener to implement my custom gesture detector.

For anyone facing the same problem, just take a look on package gesture_x_detector

Supports (tap, double-tap, scale(start, update, end), move(start, update, end) and long-press. All types can be used simultaneously.

Example:

import 'package:flutter/material.dart';
import 'package:gesture_x_detector/gesture_x_detector.dart';

void main() {
  runApp(
    MaterialApp(
      home: XGestureExample(),
    ),
  );
}

class XGestureExample extends StatefulWidget {
  @override
  _XGestureExampleState createState() => _XGestureExampleState();
}

class _XGestureExampleState extends State<XGestureExample> {
  String lastEventName = 'Tap on screen';

  @override
  Widget build(BuildContext context) {
    return XGestureDetector(
      child: Material(
        child: Center(
          child: Text(
            lastEventName,
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
      doubleTapTimeConsider: 300,
      longPressTimeConsider: 350,
      onTap: onTap,
      onDoubleTap: onDoubleTap,
      onLongPress: onLongPress,
      onMoveStart: onMoveStart,
      onMoveEnd: onMoveEnd,
      onMoveUpdate: onMoveUpdate,
      onScaleStart: onScaleStart,
      onScaleUpdate: onScaleUpdate,
      onScaleEnd: onScaleEnd,
      bypassTapEventOnDoubleTap: false,
    );
  }

  void onScaleEnd() {
    setLastEventName('onScaleEnd');
    print('onScaleEnd');
  }

  void onScaleUpdate(changedFocusPoint, scale) {
    setLastEventName('onScaleUpdate');
    print(
        'onScaleUpdate - changedFocusPoint:  $changedFocusPoint ; scale: $scale');
  }

  void onScaleStart(initialFocusPoint) {
    setLastEventName('onScaleStart');
    print('onScaleStart - initialFocusPoint: ' + initialFocusPoint.toString());
  }

  void onMoveUpdate(localPos, position, localDelta, delta) {
    setLastEventName('onMoveUpdate');
    print('onMoveUpdate - pos: ' + localPos.toString());
  }

  void onMoveEnd(pointer, localPos, position) {
    setLastEventName('onMoveEnd');
    print('onMoveEnd - pos: ' + localPos.toString());
  }

  void onMoveStart(pointer, localPos, position) {
    setLastEventName('onMoveStart');
    print('onMoveStart - pos: ' + localPos.toString());
  }

  void onLongPress(pointer, localPos, position) {
    setLastEventName('onLongPress');
    print('onLongPress - pos: ' + localPos.toString());
  }

  void onDoubleTap(localPos, position) {
    setLastEventName('onDoubleTap');
    print('onDoubleTap - pos: ' + localPos.toString());
  }

  void onTap(pointer, localPos, position) {
    setLastEventName('onTap');
    print('onTap - pos: ' + localPos.toString());
  }

  void setLastEventName(String eventName) {
    setState(() {
      lastEventName = eventName;
    });
  }
}
Share:
7,864
Ankur Prakash
Author by

Ankur Prakash

Strength: Data structures and Algorithms Performance optimization of the app Client interaction and understanding requirememnts Using best design patterns and Technology for the app (DB, UI, Cost effective)

Updated on December 09, 2022

Comments

  • Ankur Prakash
    Ankur Prakash over 1 year

    I have implemented the Scale gesture for the container. Also, I have added onHorizontalDragUpdate and onVerticalDragUpdate. But when I try to add both, I get an exception saying can't implement both with Scale gesture. Even for Pan gesture, it throws the same exception. Below is my code:

    import 'package:flutter/material.dart';
    import 'package:vector_math/vector_math_64.dart' hide Colors;
     import 'dart: math' as math;
    
    class HomeScreen extends StatefulWidget {
      @override
      State<StatefulWidget> createState() {
        return HomeState();
      }
    }
    
    class HomeState extends State<HomeScreen> {
    
      double _scale = 1.0;
      double _previousScale;
      var yOffset = 400.0;
      var xOffset = 50.0;
      var rotation = 0.0;
      var lastRotation = 0.0;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.amber,
          body: Stack(
            children: <Widget>[
              stackContainer(),
            ],
          ),
        );
      }
    
      Widget stackContainer() {
    
            return Stack(
              children: <Widget>[
                Positioned.fromRect(
                  rect: Rect.fromPoints( Offset(xOffset, yOffset),
                      Offset(xOffset+250.0, yOffset+100.0)),
                  child: GestureDetector(
                    onScaleStart: (scaleDetails) {
                      _previousScale = _scale;
                      print(' scaleStarts = ${scaleDetails.focalPoint}');
                    },
                    onScaleUpdate: (scaleUpdates){
                      //ScaleUpdateDetails
                      rotation += lastRotation - scaleUpdates.rotation;
                      lastRotation = scaleUpdates.rotation;
                      print("lastRotation = $lastRotation");
                      print(' scaleUpdates = ${scaleUpdates.scale} rotation = ${scaleUpdates.rotation}');
                      setState(() => _scale = _previousScale * scaleUpdates.scale);
                    },
                    onScaleEnd: (scaleEndDetails) {
                      _previousScale = null;
                      print(' scaleEnds = ${scaleEndDetails.velocity}');
                    },
                    child:
                    Transform(
                      transform: Matrix4.diagonal3( Vector3(_scale, _scale, _scale))..rotateZ(rotation * math.pi/180.0),
                  alignment: FractionalOffset.center,
                  child: Container(
                    color: Colors.red,
                  ),
                )
                ,
              ),
            ),
          ],
        );
      }
    }
    

    I wanted to move around the red subview and rotate along with the scale.