Skip to content

Instantly share code, notes, and snippets.

@klaszlo8207
Last active August 16, 2022 00:44
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save klaszlo8207/0df97fb3ba7484e20b0061076f3be842 to your computer and use it in GitHub Desktop.
Save klaszlo8207/0df97fb3ba7484e20b0061076f3be842 to your computer and use it in GitHub Desktop.
Flutter ScalingGestureDetector for a 3D view (you can pan and zoom in/out)
import 'package:advert_app/utils/logging.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
class ScalingGestureDetector extends StatefulWidget {
final Widget child;
final void Function(Offset initialPoint) onPanStart;
final void Function(Offset initialPoint, Offset delta) onPanUpdate;
final void Function() onPanEnd;
final void Function(Offset initialFocusPoint) onScaleStart;
final void Function(Offset changedFocusPoint, double scale) onScaleUpdate;
final void Function() onScaleEnd;
final void Function(double dx) onHorizontalDragUpdate;
final void Function(double dy) onVerticalDragUpdate;
ScalingGestureDetector({
this.child,
this.onPanStart,
this.onPanUpdate,
this.onPanEnd,
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
this.onHorizontalDragUpdate,
this.onVerticalDragUpdate,
});
@override
_ScalingGestureDetectorState createState() => _ScalingGestureDetectorState();
}
class _ScalingGestureDetectorState extends State<ScalingGestureDetector> {
final List<Touch> _touches = [];
double _initialScalingDistance;
@override
Widget build(BuildContext context) {
return RawGestureDetector(
child: widget.child,
gestures: {
ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset offset) {
final touch = Touch(
offset,
(drag, details) => _onTouchUpdate(drag, details),
(drag, details) => _onTouchEnd(drag, details),
);
_onTouchStart(touch);
return touch;
};
},
),
},
);
}
void _onTouchStart(Touch touch) {
_touches.add(touch);
if (_touches.length == 1) {
if (widget.onPanStart != null) widget.onPanStart(touch._startOffset);
} else if (_touches.length == 2) {
_initialScalingDistance = (_touches[0]._currentOffset - _touches[1]._currentOffset).distance;
if (widget.onScaleStart != null) widget.onScaleStart((_touches[0]._currentOffset + _touches[1]._currentOffset) / 2);
} else {
// Do nothing/ ignore
}
}
final _DXY = 10;
void _onTouchUpdate(Touch touch, DragUpdateDetails details) {
assert(_touches.isNotEmpty);
touch._currentOffset = details.localPosition;
if (_touches.length == 1) {
if (widget.onPanUpdate != null) widget.onPanUpdate(touch._startOffset, details.localPosition - touch._startOffset);
if (widget.onHorizontalDragUpdate != null) {
final dx = (details.localPosition.dx - touch._startOffset.dx).abs();
if (dx > _DXY) widget.onHorizontalDragUpdate((details.localPosition.dx - touch._startOffset.dx).clamp(-2.0, 2.0));
}
if (widget.onVerticalDragUpdate != null) {
final dy = (details.localPosition.dy - touch._startOffset.dy).abs();
if (dy > _DXY) widget.onVerticalDragUpdate((details.localPosition.dy - touch._startOffset.dy).clamp(-2.0, 2.0));
}
} else {
// TODO average of ALL offsets, not only 2 first
var newDistance = (_touches[0]._currentOffset - _touches[1]._currentOffset).distance;
if (widget.onScaleUpdate != null) widget.onScaleUpdate((_touches[0]._currentOffset + _touches[1]._currentOffset) / 2, newDistance / _initialScalingDistance);
}
}
void _onTouchEnd(Touch touch, DragEndDetails details) {
_touches.remove(touch);
if (_touches.length == 0) {
if (widget.onPanEnd != null) widget.onPanEnd();
} else if (_touches.length == 1) {
if (widget.onScaleEnd != null) widget.onScaleEnd();
// Restart pan
_touches[0]._startOffset = _touches[0]._currentOffset;
if (widget.onPanStart != null) widget.onPanStart(_touches[0]._startOffset);
}
}
}
class Touch extends Drag {
Offset _startOffset;
Offset _currentOffset;
final void Function(Drag drag, DragUpdateDetails details) onUpdate;
final void Function(Drag drag, DragEndDetails details) onEnd;
Touch(this._startOffset, this.onUpdate, this.onEnd) {
_currentOffset = _startOffset;
}
@override
void update(DragUpdateDetails details) {
super.update(details);
onUpdate(this, details);
}
@override
void end(DragEndDetails details) {
super.end(details);
onEnd(this, details);
}
}
@Hellomik2002
Copy link

Can rotation be calculated with this widget?

EASY

import 'dart:developer';

import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
import 'dart:math' as math;

class ScalingGestureDetector extends StatefulWidget {
  final Widget child;
  final void Function(Offset initialPoint) onPanStart;
  final void Function(Offset initialPoint, Offset delta) onPanUpdate;
  final void Function() onPanEnd;

  final void Function(Offset initialFocusPoint) onScaleStart;
  final void Function(
    Offset changedFocusPoint,
    double scale,
    double rotate,
  ) onScaleUpdate;
  final void Function() onScaleEnd;

  final void Function(double dx) onHorizontalDragUpdate;
  final void Function(double dy) onVerticalDragUpdate;

  ScalingGestureDetector({
    this.child,
    this.onPanStart,
    this.onPanUpdate,
    this.onPanEnd,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.onHorizontalDragUpdate,
    this.onVerticalDragUpdate,
  });

  @override
  _ScalingGestureDetectorState createState() => _ScalingGestureDetectorState();
}

class _ScalingGestureDetectorState extends State<ScalingGestureDetector> {
  final List<Touch> _touches = [];
  double _initialScalingDistance;
  double initialRoutate;

  @override
  Widget build(BuildContext context) {
    return RawGestureDetector(
      child: widget.child,
      gestures: {
        ImmediateMultiDragGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<
                ImmediateMultiDragGestureRecognizer>(
          () => ImmediateMultiDragGestureRecognizer(),
          (ImmediateMultiDragGestureRecognizer instance) {
            instance.onStart = (Offset offset) {
              final touch = Touch(
                offset,
                (drag, details) => _onTouchUpdate(drag, details),
                (drag, details) => _onTouchEnd(drag, details),
              );
              _onTouchStart(touch);
              return touch;
            };
          },
        ),
      },
    );
  }

  void _onTouchStart(Touch touch) {
    _touches.add(touch);
    if (_touches.length == 1) {
      if (widget.onPanStart != null) widget.onPanStart(touch._startOffset);
    } else if (_touches.length == 2) {
      _initialScalingDistance =
          (_touches[0]._currentOffset - _touches[1]._currentOffset).distance;

      final _xDis =
          _touches[0]._currentOffset.dx - _touches[1]._currentOffset.dx;
      final _yDis =
          _touches[1]._currentOffset.dy - _touches[0]._currentOffset.dy;

      log(_yDis.toString(), name: 'Y');
      log(_xDis.toString(), name: 'X');

      final double isIt = (_xDis < 0.0) ? 0.0 : math.pi;
      initialRoutate = math.atan(_yDis / _xDis) + isIt;
      log(initialRoutate.toString(), name: 'Rotate');

      if (widget.onScaleStart != null) {
        widget.onScaleStart(
            (_touches[0]._currentOffset + _touches[1]._currentOffset) / 2);
      }
    } else {
      // Do nothing/ ignore
    }
  }

  final _DXY = 10;

  void _onTouchUpdate(Touch touch, DragUpdateDetails details) {
    assert(_touches.isNotEmpty);
    touch._currentOffset = details.localPosition;

    if (_touches.length == 1) {
      if (widget.onPanUpdate != null)
        widget.onPanUpdate(
            touch._startOffset, details.localPosition - touch._startOffset);

      if (widget.onHorizontalDragUpdate != null) {
        final dx = (details.localPosition.dx - touch._startOffset.dx).abs();
        if (dx > _DXY)
          widget.onHorizontalDragUpdate(
              (details.localPosition.dx - touch._startOffset.dx)
                  .clamp(-2.0, 2.0));
      }

      if (widget.onVerticalDragUpdate != null) {
        final dy = (details.localPosition.dy - touch._startOffset.dy).abs();
        if (dy > _DXY)
          widget.onVerticalDragUpdate(
              (details.localPosition.dy - touch._startOffset.dy)
                  .clamp(-2.0, 2.0));
      }
    } else {
      // TODO average of ALL offsets, not only 2 first
      var newDistance =
          (_touches[0]._currentOffset - _touches[1]._currentOffset).distance;
      // log((initialRoutate / (2 * math.pi) * 360).toString());

      final _xDis =
          _touches[0]._currentOffset.dx - _touches[1]._currentOffset.dx;
      final _yDis =
          _touches[1]._currentOffset.dy - _touches[0]._currentOffset.dy;

      // log(_yDis.toString(), name: 'Y');
      // log(_xDis.toString(), name: 'X');
      final double isIt = (_xDis < 0.0) ? 0.0 : math.pi;
      final _currentRoutate = math.atan(_yDis / _xDis) + isIt;

      // log(initialRoutate.toString(), name: 'Rotate');

      if (widget.onScaleUpdate != null) {
        widget.onScaleUpdate(
            (_touches[0]._currentOffset + _touches[1]._currentOffset) / 2,
            newDistance / _initialScalingDistance,
            initialRoutate - _currentRoutate);
      }
    }
  }

  void _onTouchEnd(Touch touch, DragEndDetails details) {
    _touches.remove(touch);
    if (_touches.length == 0) {
      if (widget.onPanEnd != null) widget.onPanEnd();
    } else if (_touches.length == 1) {
      if (widget.onScaleEnd != null) widget.onScaleEnd();

      // Restart pan
      _touches[0]._startOffset = _touches[0]._currentOffset;
      if (widget.onPanStart != null)
        widget.onPanStart(_touches[0]._startOffset);
    }
  }
}

class Touch extends Drag {
  Offset _startOffset;
  Offset _currentOffset;

  final void Function(Drag drag, DragUpdateDetails details) onUpdate;
  final void Function(Drag drag, DragEndDetails details) onEnd;

  Touch(this._startOffset, this.onUpdate, this.onEnd) {
    _currentOffset = _startOffset;
  }

  @override
  void update(DragUpdateDetails details) {
    super.update(details);
    onUpdate(this, details);
  }

  @override
  void end(DragEndDetails details) {
    super.end(details);
    onEnd(this, details);
  }
}

@duongtruong12
Copy link

i got reset offset to 0.0 everytime i drag widget again. Can you help me please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment