Commit 596637ad authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added scale-to-zoom gesture support to the Gallery grid demo (#6185)

parent 57d6cc42
...@@ -11,6 +11,10 @@ enum GridDemoTileStyle { ...@@ -11,6 +11,10 @@ enum GridDemoTileStyle {
twoLine twoLine
} }
typedef void BannerTapCallback(Photo photo);
const double _kMinFlingVelocity = 800.0;
class Photo { class Photo {
Photo({ this.assetName, this.title, this.caption, this.isFavorite: false }); Photo({ this.assetName, this.title, this.caption, this.isFavorite: false });
...@@ -24,7 +28,111 @@ class Photo { ...@@ -24,7 +28,111 @@ class Photo {
bool get isValid => assetName != null && title != null && caption != null && isFavorite != null; bool get isValid => assetName != null && title != null && caption != null && isFavorite != null;
} }
typedef void BannerTapCallback(Photo photo); class GridPhotoViewer extends StatefulWidget {
GridPhotoViewer({ Key key, this.photo }) : super(key: key);
final Photo photo;
@override
_GridPhotoViewerState createState() => new _GridPhotoViewerState();
}
class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProviderStateMixin {
AnimationController _controller;
double _lastScale = 1.0;
double _scale = 1.0;
Point _lastFocalPoint = Point.origin;
Point _focalPoint = Point.origin;
Animation<Point> _flingAnimation;
@override
void initState() {
super.initState();
_controller = new AnimationController(vsync: this)
..addListener(_handleFlingAnimation);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// The minimum value for the focal point is 0,0. If the size of this
// renderer's box is w,h then the maximum value of the focal point is
// (w * _scale - w)/_scale, (h * _scale - h)/_scale.
Point _clampFocalPoint(Point point) {
final RenderBox box = context.findRenderObject();
final double inverseScale = (_scale - 1.0) / _scale;
final Point bottomRight = new Point(
box.size.width * inverseScale,
box.size.height * inverseScale
);
return new Point(point.x.clamp(0.0, bottomRight.x), point.y.clamp(0.0, bottomRight.y));
}
void _handleFlingAnimation() {
setState(() {
_focalPoint = _flingAnimation.value;
_lastFocalPoint = _focalPoint;
});
}
void _handleOnScaleStart(Point focalPoint) {
setState(() {
_lastScale = 1.0;
_lastFocalPoint = focalPoint;
// The fling animation stops if an input gesture starts.
_controller.stop();
});
}
void _handleOnScaleUpdate(double scale, Point focalPoint) {
setState(() {
_scale = (_scale + (scale - _lastScale)).clamp(1.0, 3.0);
_lastScale = scale;
_focalPoint = _clampFocalPoint(_focalPoint + (_lastFocalPoint - focalPoint));
_lastFocalPoint = focalPoint;
});
}
void _handleOnScaleEnd(Velocity flingVelocity) {
final double magnitude = flingVelocity.pixelsPerSecond.distance;
if (magnitude < _kMinFlingVelocity)
return;
final Offset direction = flingVelocity.pixelsPerSecond / magnitude;
final RenderBox box = context.findRenderObject();
final double distance = (Point.origin & box.size).shortestSide;
_flingAnimation = new Tween<Point>(
begin: _focalPoint,
end: _clampFocalPoint(_focalPoint + direction * -distance)
).animate(_controller);
_controller
..value = 0.0
..fling(velocity: magnitude / 1000.0);
}
@override
Widget build(BuildContext context) {
return new LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return new GestureDetector(
onScaleStart: _handleOnScaleStart,
onScaleUpdate: _handleOnScaleUpdate,
onScaleEnd: _handleOnScaleEnd,
child: new Transform(
transform: new Matrix4.identity()
..translate(_focalPoint.x * (1.0 - _scale), _focalPoint.y * (1.0 - _scale))
..scale(_scale),
child: new ClipRect(
child: new Image.asset(config.photo.assetName, fit: ImageFit.cover)
)
)
);
}
);
}
}
class GridDemoPhotoItem extends StatelessWidget { class GridDemoPhotoItem extends StatelessWidget {
GridDemoPhotoItem({ GridDemoPhotoItem({
...@@ -51,7 +159,7 @@ class GridDemoPhotoItem extends StatelessWidget { ...@@ -51,7 +159,7 @@ class GridDemoPhotoItem extends StatelessWidget {
), ),
body: new Hero( body: new Hero(
tag: photo.tag, tag: photo.tag,
child: new Image.asset(photo.assetName, fit: ImageFit.cover) child: new GridPhotoViewer(photo: photo),
) )
); );
} }
......
...@@ -6,6 +6,7 @@ import 'arena.dart'; ...@@ -6,6 +6,7 @@ import 'arena.dart';
import 'recognizer.dart'; import 'recognizer.dart';
import 'constants.dart'; import 'constants.dart';
import 'events.dart'; import 'events.dart';
import 'velocity_tracker.dart';
/// The possible states of a [ScaleGestureRecognizer]. /// The possible states of a [ScaleGestureRecognizer].
enum ScaleState { enum ScaleState {
...@@ -35,7 +36,13 @@ typedef void GestureScaleStartCallback(Point focalPoint); ...@@ -35,7 +36,13 @@ typedef void GestureScaleStartCallback(Point focalPoint);
typedef void GestureScaleUpdateCallback(double scale, Point focalPoint); typedef void GestureScaleUpdateCallback(double scale, Point focalPoint);
/// Signature for when the pointers are no longer in contact with the screen. /// Signature for when the pointers are no longer in contact with the screen.
typedef void GestureScaleEndCallback(); typedef void GestureScaleEndCallback(Velocity flingVelocity);
bool _isFlingGesture(Velocity velocity) {
assert(velocity != null);
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
}
/// Recognizes a scale gesture. /// Recognizes a scale gesture.
/// ///
...@@ -61,12 +68,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -61,12 +68,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
double _initialSpan; double _initialSpan;
double _currentSpan; double _currentSpan;
Map<int, Point> _pointerLocations; Map<int, Point> _pointerLocations;
Map<int, VelocityTracker> _velocityTrackers = new Map<int, VelocityTracker>();
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0; double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
@override @override
void addPointer(PointerEvent event) { void addPointer(PointerEvent event) {
startTrackingPointer(event.pointer); startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = new VelocityTracker();
if (_state == ScaleState.ready) { if (_state == ScaleState.ready) {
_state = ScaleState.possible; _state = ScaleState.possible;
_initialSpan = 0.0; _initialSpan = 0.0;
...@@ -80,6 +89,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -80,6 +89,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
assert(_state != ScaleState.ready); assert(_state != ScaleState.ready);
bool configChanged = false; bool configChanged = false;
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
VelocityTracker tracker = _velocityTrackers[event.pointer];
assert(tracker != null);
tracker.addPosition(event.timeStamp, event.position);
_pointerLocations[event.pointer] = event.position; _pointerLocations[event.pointer] = event.position;
} else if (event is PointerDownEvent) { } else if (event is PointerDownEvent) {
configChanged = true; configChanged = true;
...@@ -89,12 +101,12 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -89,12 +101,12 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_pointerLocations.remove(event.pointer); _pointerLocations.remove(event.pointer);
} }
_update(configChanged); _update(configChanged, event.pointer);
stopTrackingIfPointerNoLongerDown(event); stopTrackingIfPointerNoLongerDown(event);
} }
void _update(bool configChanged) { void _update(bool configChanged, int pointer) {
int count = _pointerLocations.keys.length; int count = _pointerLocations.keys.length;
// Compute the focal point // Compute the focal point
...@@ -112,8 +124,20 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -112,8 +124,20 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (configChanged) { if (configChanged) {
_initialSpan = _currentSpan; _initialSpan = _currentSpan;
if (_state == ScaleState.started) { if (_state == ScaleState.started) {
if (onEnd != null) if (onEnd != null) {
onEnd(); VelocityTracker tracker = _velocityTrackers[pointer];
assert(tracker != null);
Velocity velocity = tracker.getVelocity();
if (velocity != null && _isFlingGesture(velocity)) {
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
onEnd(velocity);
} else {
onEnd(Velocity.zero);
}
}
_state = ScaleState.accepted; _state = ScaleState.accepted;
} }
} }
...@@ -140,7 +164,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -140,7 +164,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
void acceptGesture(int pointer) { void acceptGesture(int pointer) {
if (_state != ScaleState.accepted) { if (_state != ScaleState.accepted) {
_state = ScaleState.accepted; _state = ScaleState.accepted;
_update(false); _update(false, pointer);
} }
} }
...@@ -162,6 +186,12 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ...@@ -162,6 +186,12 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_state = ScaleState.ready; _state = ScaleState.ready;
} }
@override
void dispose() {
_velocityTrackers.clear();
super.dispose();
}
@override @override
String toStringShort() => 'scale'; String toStringShort() => 'scale';
} }
...@@ -28,7 +28,7 @@ void main() { ...@@ -28,7 +28,7 @@ void main() {
}; };
bool didEndScale = false; bool didEndScale = false;
scale.onEnd = () { scale.onEnd = (Velocity flingVelocity) {
didEndScale = true; didEndScale = true;
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment