Commit 9bad312a authored by Ian Hickson's avatar Ian Hickson

RenderFractionalTranslation

- Add RenderFractionalTranslation, a render box that does a
  translation based on a FractionalOffset.

- Make FractionalOffset more like Offset
  - dx/dy instead of x/y
  - add /, ~/, %
  - add .zero

- Add alongOffset and alongSize to FractionalOffset so that you can
  easily apply FractionalOffset to Offsets and Sizes. (Better name
  suggestions welcome.)

- Add transformHitTests boolean to RenderTransform (also on
  RenderFractionalTranslation), and to classes based on it.

- Remove the fade from Dismissable. We can add it back using the
  builder-with-child pattern like Draggable if we need it. See #1003
  for tha feature request.

- Rename a bunch of variables in dismissable.dart.

- Change the test for dismissable to not handle leftwards dismisses
  one pixel different from rightwards dismisses, and cleaned up the
  resulting effect on the test (mostly making sure we had the right
  number of pumps, with comments explaining what each one was).

Fixes #174.
parent a9ddbb4e
...@@ -510,36 +510,55 @@ void paintImage({ ...@@ -510,36 +510,55 @@ void paintImage({
/// FractionalOffset(1.0, 0.0) represents the top right of the Size, /// FractionalOffset(1.0, 0.0) represents the top right of the Size,
/// FractionalOffset(0.0, 1.0) represents the bottom left of the Size, /// FractionalOffset(0.0, 1.0) represents the bottom left of the Size,
class FractionalOffset { class FractionalOffset {
const FractionalOffset(this.x, this.y); const FractionalOffset(this.dx, this.dy);
final double x; final double dx;
final double y; final double dy;
static const FractionalOffset zero = const FractionalOffset(0.0, 0.0);
FractionalOffset operator -() {
return new FractionalOffset(-dx, -dy);
}
FractionalOffset operator -(FractionalOffset other) { FractionalOffset operator -(FractionalOffset other) {
return new FractionalOffset(x - other.x, y - other.y); return new FractionalOffset(dx - other.dx, dy - other.dy);
} }
FractionalOffset operator +(FractionalOffset other) { FractionalOffset operator +(FractionalOffset other) {
return new FractionalOffset(x + other.x, y + other.y); return new FractionalOffset(dx + other.dx, dy + other.dy);
} }
FractionalOffset operator *(double other) { FractionalOffset operator *(double other) {
return new FractionalOffset(x * other, y * other); return new FractionalOffset(dx * other, dy * other);
}
FractionalOffset operator /(double other) {
return new FractionalOffset(dx / other, dy / other);
}
FractionalOffset operator ~/(double other) {
return new FractionalOffset((dx ~/ other).toDouble(), (dy ~/ other).toDouble());
}
FractionalOffset operator %(double other) {
return new FractionalOffset(dx % other, dy % other);
}
Offset alongOffset(Offset other) {
return new Offset(dx * other.dx, dy * other.dy);
}
Offset alongSize(Size other) {
return new Offset(dx * other.width, dy * other.height);
} }
bool operator ==(dynamic other) { bool operator ==(dynamic other) {
if (other is! FractionalOffset) if (other is! FractionalOffset)
return false; return false;
final FractionalOffset typedOther = other; final FractionalOffset typedOther = other;
return x == typedOther.x && return dx == typedOther.dx &&
y == typedOther.y; dy == typedOther.dy;
} }
int get hashCode => hashValues(x, y); int get hashCode => hashValues(dx, dy);
static FractionalOffset lerp(FractionalOffset a, FractionalOffset b, double t) { static FractionalOffset lerp(FractionalOffset a, FractionalOffset b, double t) {
if (a == null && b == null) if (a == null && b == null)
return null; return null;
if (a == null) if (a == null)
return new FractionalOffset(b.x * t, b.y * t); return new FractionalOffset(b.dx * t, b.dy * t);
if (b == null) if (b == null)
return new FractionalOffset(b.x * (1.0 - t), b.y * (1.0 - t)); return new FractionalOffset(b.dx * (1.0 - t), b.dy * (1.0 - t));
return new FractionalOffset(ui.lerpDouble(a.x, b.x, t), ui.lerpDouble(a.y, b.y, t)); return new FractionalOffset(ui.lerpDouble(a.dx, b.dx, t), ui.lerpDouble(a.dy, b.dy, t));
} }
String toString() => '$runtimeType($x, $y)'; String toString() => '$runtimeType($dx, $dy)';
} }
/// A background image for a box. /// A background image for a box.
...@@ -919,8 +938,8 @@ class _BoxDecorationPainter extends BoxPainter { ...@@ -919,8 +938,8 @@ class _BoxDecorationPainter extends BoxPainter {
rect: rect, rect: rect,
image: image, image: image,
colorFilter: backgroundImage.colorFilter, colorFilter: backgroundImage.colorFilter,
alignX: backgroundImage.alignment?.x, alignX: backgroundImage.alignment?.dx,
alignY: backgroundImage.alignment?.y, alignY: backgroundImage.alignment?.dy,
fit: backgroundImage.fit, fit: backgroundImage.fit,
repeat: backgroundImage.repeat repeat: backgroundImage.repeat
); );
......
...@@ -216,8 +216,8 @@ class RenderImage extends RenderBox { ...@@ -216,8 +216,8 @@ class RenderImage extends RenderBox {
image: _image, image: _image,
colorFilter: _colorFilter, colorFilter: _colorFilter,
fit: _fit, fit: _fit,
alignX: _alignment?.x, alignX: _alignment?.dx,
alignY: _alignment?.y, alignY: _alignment?.dy,
centerSlice: _centerSlice, centerSlice: _centerSlice,
repeat: _repeat repeat: _repeat
); );
......
...@@ -851,10 +851,11 @@ class RenderTransform extends RenderProxyBox { ...@@ -851,10 +851,11 @@ class RenderTransform extends RenderProxyBox {
Matrix4 transform, Matrix4 transform,
Offset origin, Offset origin,
FractionalOffset alignment, FractionalOffset alignment,
this.transformHitTests: true,
RenderBox child RenderBox child
}) : super(child) { }) : super(child) {
assert(transform != null); assert(transform != null);
assert(alignment == null || (alignment.x != null && alignment.y != null)); assert(alignment == null || (alignment.dx != null && alignment.dy != null));
this.transform = transform; this.transform = transform;
this.alignment = alignment; this.alignment = alignment;
this.origin = origin; this.origin = origin;
...@@ -881,13 +882,21 @@ class RenderTransform extends RenderProxyBox { ...@@ -881,13 +882,21 @@ class RenderTransform extends RenderProxyBox {
FractionalOffset get alignment => _alignment; FractionalOffset get alignment => _alignment;
FractionalOffset _alignment; FractionalOffset _alignment;
void set alignment (FractionalOffset newAlignment) { void set alignment (FractionalOffset newAlignment) {
assert(newAlignment == null || (newAlignment.x != null && newAlignment.y != null)); assert(newAlignment == null || (newAlignment.dx != null && newAlignment.dy != null));
if (_alignment == newAlignment) if (_alignment == newAlignment)
return; return;
_alignment = newAlignment; _alignment = newAlignment;
markNeedsPaint(); markNeedsPaint();
} }
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// applyPaintTransform(), and therefore localToGlobal() and globalToLocal(),
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
// Note the lack of a getter for transform because Matrix4 is not immutable // Note the lack of a getter for transform because Matrix4 is not immutable
Matrix4 _transform; Matrix4 _transform;
...@@ -942,25 +951,29 @@ class RenderTransform extends RenderProxyBox { ...@@ -942,25 +951,29 @@ class RenderTransform extends RenderProxyBox {
Matrix4 result = new Matrix4.identity(); Matrix4 result = new Matrix4.identity();
if (_origin != null) if (_origin != null)
result.translate(_origin.dx, _origin.dy); result.translate(_origin.dx, _origin.dy);
if (_alignment != null) Offset translation;
result.translate(_alignment.x * size.width, _alignment.y * size.height); if (_alignment != null) {
translation = _alignment.alongSize(size);
result.translate(translation.dx, translation.dy);
}
result.multiply(_transform); result.multiply(_transform);
if (_alignment != null) if (_alignment != null)
result.translate(-_alignment.x * size.width, -_alignment.y * size.height); result.translate(-translation.dx, -translation.dy);
if (_origin != null) if (_origin != null)
result.translate(-_origin.dx, -_origin.dy); result.translate(-_origin.dx, -_origin.dy);
return result; return result;
} }
bool hitTest(HitTestResult result, { Point position }) { bool hitTest(HitTestResult result, { Point position }) {
if (transformHitTests) {
Matrix4 inverse = new Matrix4.zero(); Matrix4 inverse = new Matrix4.zero();
// TODO(abarth): Check the determinant for degeneracy. // TODO(abarth): Check the determinant for degeneracy.
inverse.copyInverse(_effectiveTransform); inverse.copyInverse(_effectiveTransform);
Vector3 position3 = new Vector3(position.x, position.y, 0.0); Vector3 position3 = new Vector3(position.x, position.y, 0.0);
Vector3 transformed3 = inverse.transform3(position3); Vector3 transformed3 = inverse.transform3(position3);
Point transformed = new Point(transformed3.x, transformed3.y); position = new Point(transformed3.x, transformed3.y);
return super.hitTest(result, position: transformed); }
return super.hitTest(result, position: position);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
...@@ -985,6 +998,65 @@ class RenderTransform extends RenderProxyBox { ...@@ -985,6 +998,65 @@ class RenderTransform extends RenderProxyBox {
settings.addAll(debugDescribeTransform(_transform)); settings.addAll(debugDescribeTransform(_transform));
settings.add('origin: $origin'); settings.add('origin: $origin');
settings.add('alignment: $alignment'); settings.add('alignment: $alignment');
settings.add('transformHitTests: $transformHitTests');
}
}
/// Applies a translation transformation before painting its child. The
/// translation is expressed as a [FractionalOffset] relative to the
/// RenderFractionalTranslation box's size. Hit tests will only be detected
/// inside the bounds of the RenderFractionalTranslation, even if the contents
/// are offset such that they overflow.
class RenderFractionalTranslation extends RenderProxyBox {
RenderFractionalTranslation({
FractionalOffset translation,
this.transformHitTests: true,
RenderBox child
}) : _translation = translation, super(child) {
assert(translation == null || (translation.dx != null && translation.dy != null));
}
/// The translation to apply to the child, as a multiple of the size.
FractionalOffset get translation => _translation;
FractionalOffset _translation;
void set translation (FractionalOffset newTranslation) {
assert(newTranslation == null || (newTranslation.dx != null && newTranslation.dy != null));
if (_translation == newTranslation)
return;
_translation = newTranslation;
markNeedsPaint();
}
/// When set to true, hit tests are performed based on the position of the
/// child as it is painted. When set to false, hit tests are performed
/// ignoring the transformation.
///
/// applyPaintTransform(), and therefore localToGlobal() and globalToLocal(),
/// always honor the transformation, regardless of the value of this property.
bool transformHitTests;
bool hitTest(HitTestResult result, { Point position }) {
assert(!needsLayout);
if (transformHitTests)
position = new Point(position.x - translation.dx * size.width, position.y - translation.dy * size.height);
return super.hitTest(result, position: position);
}
void paint(PaintingContext context, Offset offset) {
assert(!needsLayout);
if (child != null)
super.paint(context, offset + translation.alongSize(size));
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.translate(translation.dx * size.width, translation.dy * size.height);
super.applyPaintTransform(child, transform);
}
void debugDescribeSettings(List<String> settings) {
super.debugDescribeSettings(settings);
settings.add('translation: $translation');
settings.add('transformHitTests: $transformHitTests');
} }
} }
......
...@@ -179,7 +179,7 @@ class RenderPositionedBox extends RenderShiftedBox { ...@@ -179,7 +179,7 @@ class RenderPositionedBox extends RenderShiftedBox {
_widthFactor = widthFactor, _widthFactor = widthFactor,
_heightFactor = heightFactor, _heightFactor = heightFactor,
super(child) { super(child) {
assert(alignment != null && alignment.x != null && alignment.y != null); assert(alignment != null && alignment.dx != null && alignment.dy != null);
assert(widthFactor == null || widthFactor >= 0.0); assert(widthFactor == null || widthFactor >= 0.0);
assert(heightFactor == null || heightFactor >= 0.0); assert(heightFactor == null || heightFactor >= 0.0);
} }
...@@ -196,7 +196,7 @@ class RenderPositionedBox extends RenderShiftedBox { ...@@ -196,7 +196,7 @@ class RenderPositionedBox extends RenderShiftedBox {
FractionalOffset get alignment => _alignment; FractionalOffset get alignment => _alignment;
FractionalOffset _alignment; FractionalOffset _alignment;
void set alignment (FractionalOffset newAlignment) { void set alignment (FractionalOffset newAlignment) {
assert(newAlignment != null && newAlignment.x != null && newAlignment.y != null); assert(newAlignment != null && newAlignment.dx != null && newAlignment.dy != null);
if (_alignment == newAlignment) if (_alignment == newAlignment)
return; return;
_alignment = newAlignment; _alignment = newAlignment;
...@@ -237,9 +237,8 @@ class RenderPositionedBox extends RenderShiftedBox { ...@@ -237,9 +237,8 @@ class RenderPositionedBox extends RenderShiftedBox {
child.layout(constraints.loosen(), parentUsesSize: true); child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(new Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.INFINITY, size = constraints.constrain(new Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.INFINITY,
shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.INFINITY)); shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.INFINITY));
final Offset delta = size - child.size;
final BoxParentData childParentData = child.parentData; final BoxParentData childParentData = child.parentData;
childParentData.position = delta.scale(_alignment.x, _alignment.y).toPoint(); childParentData.position = _alignment.alongOffset(size - child.size).toPoint();
} else { } else {
size = constraints.constrain(new Size(shrinkWrapWidth ? 0.0 : double.INFINITY, size = constraints.constrain(new Size(shrinkWrapWidth ? 0.0 : double.INFINITY,
shrinkWrapHeight ? 0.0 : double.INFINITY)); shrinkWrapHeight ? 0.0 : double.INFINITY));
......
...@@ -328,9 +328,7 @@ abstract class RenderStackBase extends RenderBox ...@@ -328,9 +328,7 @@ abstract class RenderStackBase extends RenderBox
final StackParentData childParentData = child.parentData; final StackParentData childParentData = child.parentData;
if (!childParentData.isPositioned) { if (!childParentData.isPositioned) {
double x = (size.width - child.size.width) * alignment.x; childParentData.position = alignment.alongOffset(size - child.size).toPoint();
double y = (size.height - child.size.height) * alignment.y;
childParentData.position = new Point(x, y);
} else { } else {
BoxConstraints childConstraints = const BoxConstraints(); BoxConstraints childConstraints = const BoxConstraints();
......
...@@ -270,7 +270,7 @@ class ClipOval extends OneChildRenderObjectWidget { ...@@ -270,7 +270,7 @@ class ClipOval extends OneChildRenderObjectWidget {
/// Applies a transformation before painting its child. /// Applies a transformation before painting its child.
class Transform extends OneChildRenderObjectWidget { class Transform extends OneChildRenderObjectWidget {
Transform({ Key key, this.transform, this.origin, this.alignment, Widget child }) Transform({ Key key, this.transform, this.origin, this.alignment, this.transformHitTests: true, Widget child })
: super(key: key, child: child) { : super(key: key, child: child) {
assert(transform != null); assert(transform != null);
} }
...@@ -291,12 +291,43 @@ class Transform extends OneChildRenderObjectWidget { ...@@ -291,12 +291,43 @@ class Transform extends OneChildRenderObjectWidget {
/// If it is specificed at the same time as an offset, both are applied. /// If it is specificed at the same time as an offset, both are applied.
final FractionalOffset alignment; final FractionalOffset alignment;
RenderTransform createRenderObject() => new RenderTransform(transform: transform, origin: origin, alignment: alignment); /// Whether to apply the translation when performing hit tests.
final bool transformHitTests;
RenderTransform createRenderObject() => new RenderTransform(
transform: transform,
origin: origin,
alignment: alignment,
transformHitTests: transformHitTests
);
void updateRenderObject(RenderTransform renderObject, Transform oldWidget) { void updateRenderObject(RenderTransform renderObject, Transform oldWidget) {
renderObject.transform = transform; renderObject.transform = transform;
renderObject.origin = origin; renderObject.origin = origin;
renderObject.alignment = alignment; renderObject.alignment = alignment;
renderObject.transformHitTests = transformHitTests;
}
}
/// Applies a translation expressed as a fraction of the box's size before
/// painting its child.
class FractionalTranslation extends OneChildRenderObjectWidget {
FractionalTranslation({ Key key, this.translation, this.transformHitTests: true, Widget child })
: super(key: key, child: child) {
assert(translation != null);
}
/// The offset by which to translate the child, as a multiple of its size.
final FractionalOffset translation;
/// Whether to apply the translation when performing hit tests.
final bool transformHitTests;
RenderFractionalTranslation createRenderObject() => new RenderFractionalTranslation(translation: translation, transformHitTests: transformHitTests);
void updateRenderObject(RenderFractionalTranslation renderObject, FractionalTranslation oldWidget) {
renderObject.translation = translation;
renderObject.transformHitTests = transformHitTests;
} }
} }
...@@ -335,7 +366,7 @@ class Align extends OneChildRenderObjectWidget { ...@@ -335,7 +366,7 @@ class Align extends OneChildRenderObjectWidget {
this.heightFactor, this.heightFactor,
Widget child Widget child
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
assert(alignment != null && alignment.x != null && alignment.y != null); assert(alignment != null && alignment.dx != null && alignment.dy != null);
assert(widthFactor == null || widthFactor >= 0.0); assert(widthFactor == null || widthFactor >= 0.0);
assert(heightFactor == null || heightFactor >= 0.0); assert(heightFactor == null || heightFactor >= 0.0);
} }
......
...@@ -11,9 +11,9 @@ import 'transitions.dart'; ...@@ -11,9 +11,9 @@ import 'transitions.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
const Duration _kCardDismissFadeout = const Duration(milliseconds: 200); const Duration _kCardDismissDuration = const Duration(milliseconds: 200);
const Duration _kCardDismissResize = const Duration(milliseconds: 300); const Duration _kCardResizeDuration = const Duration(milliseconds: 300);
const Curve _kCardDismissResizeCurve = const Interval(0.4, 1.0, curve: Curves.ease); const Curve _kCardResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease);
const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0; const double _kMinFlingVelocityDelta = 400.0;
const double _kFlingVelocityScale = 1.0 / 300.0; const double _kFlingVelocityScale = 1.0 / 300.0;
...@@ -60,7 +60,7 @@ class Dismissable extends StatefulComponent { ...@@ -60,7 +60,7 @@ class Dismissable extends StatefulComponent {
/// Called when the widget changes size (i.e., when contracting after being dismissed). /// Called when the widget changes size (i.e., when contracting after being dismissed).
final VoidCallback onResized; final VoidCallback onResized;
/// Called when the widget has been dismissed. /// Called when the widget has been dismissed, after finishing resizing.
final VoidCallback onDismissed; final VoidCallback onDismissed;
/// The direction in which the widget can be dismissed. /// The direction in which the widget can be dismissed.
...@@ -72,14 +72,14 @@ class Dismissable extends StatefulComponent { ...@@ -72,14 +72,14 @@ class Dismissable extends StatefulComponent {
class _DismissableState extends State<Dismissable> { class _DismissableState extends State<Dismissable> {
void initState() { void initState() {
super.initState(); super.initState();
_fadePerformance = new Performance(duration: _kCardDismissFadeout); _dismissPerformance = new Performance(duration: _kCardDismissDuration);
_fadePerformance.addStatusListener((PerformanceStatus status) { _dismissPerformance.addStatusListener((PerformanceStatus status) {
if (status == PerformanceStatus.completed) if (status == PerformanceStatus.completed)
_handleFadeCompleted(); _handleDismissCompleted();
}); });
} }
Performance _fadePerformance; Performance _dismissPerformance;
Performance _resizePerformance; Performance _resizePerformance;
Size _size; Size _size;
...@@ -87,7 +87,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -87,7 +87,7 @@ class _DismissableState extends State<Dismissable> {
bool _dragUnderway = false; bool _dragUnderway = false;
void dispose() { void dispose() {
_fadePerformance?.stop(); _dismissPerformance?.stop();
_resizePerformance?.stop(); _resizePerformance?.stop();
super.dispose(); super.dispose();
} }
...@@ -99,13 +99,13 @@ class _DismissableState extends State<Dismissable> { ...@@ -99,13 +99,13 @@ class _DismissableState extends State<Dismissable> {
config.direction == DismissDirection.down; config.direction == DismissDirection.down;
} }
void _handleFadeCompleted() { void _handleDismissCompleted() {
if (!_dragUnderway) if (!_dragUnderway)
_startResizePerformance(); _startResizePerformance();
} }
bool get _isActive { bool get _isActive {
return _size != null && (_dragUnderway || _fadePerformance.isAnimating); return _size != null && (_dragUnderway || _dismissPerformance.isAnimating);
} }
void _maybeCallOnResized() { void _maybeCallOnResized() {
...@@ -120,13 +120,12 @@ class _DismissableState extends State<Dismissable> { ...@@ -120,13 +120,12 @@ class _DismissableState extends State<Dismissable> {
void _startResizePerformance() { void _startResizePerformance() {
assert(_size != null); assert(_size != null);
assert(_fadePerformance != null); assert(_dismissPerformance != null);
assert(_fadePerformance.isCompleted); assert(_dismissPerformance.isCompleted);
assert(_resizePerformance == null); assert(_resizePerformance == null);
setState(() { setState(() {
_resizePerformance = new Performance() _resizePerformance = new Performance()
..duration = _kCardDismissResize ..duration = _kCardResizeDuration
..addListener(_handleResizeProgressChanged); ..addListener(_handleResizeProgressChanged);
_resizePerformance.play(); _resizePerformance.play();
}); });
...@@ -140,21 +139,21 @@ class _DismissableState extends State<Dismissable> { ...@@ -140,21 +139,21 @@ class _DismissableState extends State<Dismissable> {
} }
void _handleDragStart(_) { void _handleDragStart(_) {
if (_fadePerformance.isAnimating) if (_dismissPerformance.isAnimating)
return; return;
setState(() { setState(() {
_dragUnderway = true; _dragUnderway = true;
_dragExtent = 0.0; _dragExtent = 0.0;
_fadePerformance.progress = 0.0; _dismissPerformance.progress = 0.0;
}); });
} }
void _handleDragUpdate(double delta) { void _handleDragUpdate(double delta) {
if (!_isActive || _fadePerformance.isAnimating) if (!_isActive || _dismissPerformance.isAnimating)
return; return;
double oldDragExtent = _dragExtent; double oldDragExtent = _dragExtent;
switch(config.direction) { switch (config.direction) {
case DismissDirection.horizontal: case DismissDirection.horizontal:
case DismissDirection.vertical: case DismissDirection.vertical:
_dragExtent += delta; _dragExtent += delta;
...@@ -181,8 +180,8 @@ class _DismissableState extends State<Dismissable> { ...@@ -181,8 +180,8 @@ class _DismissableState extends State<Dismissable> {
// the performances. // the performances.
}); });
} }
if (!_fadePerformance.isAnimating) if (!_dismissPerformance.isAnimating)
_fadePerformance.progress = _dragExtent.abs() / (_size.width * _kDismissCardThreshold); _dismissPerformance.progress = _dragExtent.abs() / _size.width;
} }
bool _isFlingGesture(ui.Offset velocity) { bool _isFlingGesture(ui.Offset velocity) {
...@@ -215,19 +214,20 @@ class _DismissableState extends State<Dismissable> { ...@@ -215,19 +214,20 @@ class _DismissableState extends State<Dismissable> {
} }
void _handleDragEnd(ui.Offset velocity) { void _handleDragEnd(ui.Offset velocity) {
if (!_isActive || _fadePerformance.isAnimating) if (!_isActive || _dismissPerformance.isAnimating)
return; return;
setState(() { setState(() {
_dragUnderway = false; _dragUnderway = false;
if (_fadePerformance.isCompleted) { if (_dismissPerformance.isCompleted) {
_startResizePerformance(); _startResizePerformance();
} else if (_isFlingGesture(velocity)) { } else if (_isFlingGesture(velocity)) {
double flingVelocity = _directionIsYAxis ? velocity.dy : velocity.dx; double flingVelocity = _directionIsYAxis ? velocity.dy : velocity.dx;
_dragExtent = flingVelocity.sign; _dragExtent = flingVelocity.sign;
_fadePerformance.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); _dismissPerformance.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
} else if (_dismissPerformance.progress > _kDismissCardThreshold) {
_dismissPerformance.forward();
} else { } else {
_fadePerformance.reverse(); _dismissPerformance.reverse();
} }
}); });
} }
...@@ -238,12 +238,12 @@ class _DismissableState extends State<Dismissable> { ...@@ -238,12 +238,12 @@ class _DismissableState extends State<Dismissable> {
}); });
} }
Point get _activeCardDragEndPoint { FractionalOffset get _activeCardDragEndPoint {
if (!_isActive) if (!_isActive)
return Point.origin; return FractionalOffset.zero;
assert(_size != null); if (_directionIsYAxis)
double extent = _directionIsYAxis ? _size.height : _size.width; return new FractionalOffset(0.0, _dragExtent.sign);
return new Point(_dragExtent.sign * extent * _kDismissCardThreshold, 0.0); return new FractionalOffset(_dragExtent.sign, 0.0);
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -254,7 +254,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -254,7 +254,7 @@ class _DismissableState extends State<Dismissable> {
AnimatedValue<double> squashAxisExtent = new AnimatedValue<double>( AnimatedValue<double> squashAxisExtent = new AnimatedValue<double>(
_directionIsYAxis ? _size.width : _size.height, _directionIsYAxis ? _size.width : _size.height,
end: 0.0, end: 0.0,
curve: _kCardDismissResizeCurve curve: _kCardResizeTimeCurve
); );
return new SquashTransition( return new SquashTransition(
...@@ -274,16 +274,15 @@ class _DismissableState extends State<Dismissable> { ...@@ -274,16 +274,15 @@ class _DismissableState extends State<Dismissable> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new SizeObserver( child: new SizeObserver(
onSizeChanged: _handleSizeChanged, onSizeChanged: _handleSizeChanged,
child: new FadeTransition(
performance: _fadePerformance.view,
opacity: new AnimatedValue<double>(1.0, end: 0.0),
child: new SlideTransition( child: new SlideTransition(
performance: _fadePerformance.view, performance: _dismissPerformance.view,
position: new AnimatedValue<Point>(Point.origin, end: _activeCardDragEndPoint), position: new AnimatedValue<FractionalOffset>(
FractionalOffset.zero,
end: _activeCardDragEndPoint
),
child: config.child child: config.child
) )
) )
)
); );
} }
} }
...@@ -82,18 +82,18 @@ class SlideTransition extends TransitionWithChild { ...@@ -82,18 +82,18 @@ class SlideTransition extends TransitionWithChild {
Key key, Key key,
this.position, this.position,
PerformanceView performance, PerformanceView performance,
this.transformHitTests: true,
Widget child Widget child
}) : super(key: key, }) : super(key: key,
performance: performance, performance: performance,
child: child); child: child);
final AnimatedValue<Point> position; final AnimatedValue<FractionalOffset> position;
bool transformHitTests;
Widget buildWithChild(BuildContext context, Widget child) { Widget buildWithChild(BuildContext context, Widget child) {
performance.updateVariable(position); performance.updateVariable(position);
Matrix4 transform = new Matrix4.identity() return new FractionalTranslation(translation: position.value, transformHitTests: transformHitTests, child: child);
..translate(position.value.x, position.value.y);
return new Transform(transform: transform, child: child);
} }
} }
......
...@@ -12,11 +12,11 @@ ScrollDirection scrollDirection = ScrollDirection.vertical; ...@@ -12,11 +12,11 @@ ScrollDirection scrollDirection = ScrollDirection.vertical;
DismissDirection dismissDirection = DismissDirection.horizontal; DismissDirection dismissDirection = DismissDirection.horizontal;
List<int> dismissedItems = <int>[]; List<int> dismissedItems = <int>[];
void handleOnResized(item) { void handleOnResized(int item) {
expect(dismissedItems.contains(item), isFalse); expect(dismissedItems.contains(item), isFalse);
} }
void handleOnDismissed(item) { void handleOnDismissed(int item) {
expect(dismissedItems.contains(item), isFalse); expect(dismissedItems.contains(item), isFalse);
dismissedItems.add(item); dismissedItems.add(item);
} }
...@@ -39,7 +39,7 @@ Widget widgetBuilder() { ...@@ -39,7 +39,7 @@ Widget widgetBuilder() {
return new Container( return new Container(
padding: const EdgeDims.all(10.0), padding: const EdgeDims.all(10.0),
child: new ScrollableList<int>( child: new ScrollableList<int>(
items: [0, 1, 2, 3, 4].where((int i) => !dismissedItems.contains(i)).toList(), items: <int>[0, 1, 2, 3, 4].where((int i) => !dismissedItems.contains(i)).toList(),
itemBuilder: buildDismissableItem, itemBuilder: buildDismissableItem,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
itemExtent: itemExtent itemExtent: itemExtent
...@@ -56,25 +56,25 @@ void dismissElement(WidgetTester tester, Element itemElement, { DismissDirection ...@@ -56,25 +56,25 @@ void dismissElement(WidgetTester tester, Element itemElement, { DismissDirection
Point upLocation; Point upLocation;
switch(gestureDirection) { switch(gestureDirection) {
case DismissDirection.left: case DismissDirection.left:
// Note: getTopRight() returns a point that's just beyond // getTopRight() returns a point that's just beyond itemWidget's right
// itemWidget's right edge and outside the Dismissable event // edge and outside the Dismissable event listener's bounds.
// listener's bounds.
downLocation = tester.getTopRight(itemElement) + const Offset(-0.1, 0.0); downLocation = tester.getTopRight(itemElement) + const Offset(-0.1, 0.0);
upLocation = tester.getTopLeft(itemElement); upLocation = tester.getTopLeft(itemElement);
break; break;
case DismissDirection.right: case DismissDirection.right:
downLocation = tester.getTopLeft(itemElement); // we do the same thing here to keep the test symmetric
downLocation = tester.getTopLeft(itemElement) + const Offset(0.1, 0.0);
upLocation = tester.getTopRight(itemElement); upLocation = tester.getTopRight(itemElement);
break; break;
case DismissDirection.up: case DismissDirection.up:
// Note: getBottomLeft() returns a point that's just below // getBottomLeft() returns a point that's just below itemWidget's bottom
// itemWidget's bottom edge and outside the Dismissable event // edge and outside the Dismissable event listener's bounds.
// listener's bounds.
downLocation = tester.getBottomLeft(itemElement) + const Offset(0.0, -0.1); downLocation = tester.getBottomLeft(itemElement) + const Offset(0.0, -0.1);
upLocation = tester.getTopLeft(itemElement); upLocation = tester.getTopLeft(itemElement);
break; break;
case DismissDirection.down: case DismissDirection.down:
downLocation = tester.getTopLeft(itemElement); // again with doing the same here for symmetry
downLocation = tester.getTopLeft(itemElement) + const Offset(0.1, 0.0);
upLocation = tester.getBottomLeft(itemElement); upLocation = tester.getBottomLeft(itemElement);
break; break;
default: default:
...@@ -96,9 +96,11 @@ void dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirect ...@@ -96,9 +96,11 @@ void dismissItem(WidgetTester tester, int item, { DismissDirection gestureDirect
dismissElement(tester, itemElement, gestureDirection: gestureDirection); dismissElement(tester, itemElement, gestureDirection: gestureDirection);
tester.pumpWidget(widgetBuilder()); // start the resize animation tester.pumpWidget(widgetBuilder()); // start the slide
tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the resize animation tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the slide and start shrinking...
tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // dismiss tester.pumpWidget(widgetBuilder()); // first frame of shrinking animation
tester.pumpWidget(widgetBuilder(), const Duration(seconds: 1)); // finish the shrinking and call the callback...
tester.pumpWidget(widgetBuilder()); // rebuild after the callback removes the entry
} }
class Test1215DismissableComponent extends StatelessComponent { class Test1215DismissableComponent extends StatelessComponent {
...@@ -229,8 +231,12 @@ void main() { ...@@ -229,8 +231,12 @@ void main() {
}); });
}); });
// This is a regression test for // This is a regression test for an fn2 bug where dragging a card caused an
// https://github.com/domokit/sky_engine/issues/1068 // assert "'!_disqualifiedFromEverAppearingAgain' is not true". The old URL
// was https://github.com/domokit/sky_engine/issues/1068 but that issue is 404
// now since we migrated to the new repo. The bug was fixed by
// https://github.com/flutter/engine/pull/1134 at the time, and later made
// irrelevant by fn3, but just in case...
test('Verify that drag-move events do not assert', () { test('Verify that drag-move events do not assert', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
scrollDirection = ScrollDirection.horizontal; scrollDirection = ScrollDirection.horizontal;
...@@ -255,8 +261,12 @@ void main() { ...@@ -255,8 +261,12 @@ void main() {
}); });
}); });
// This one is for // This one is for a case where dssmissing a component above a previously
// https://github.com/flutter/engine/issues/1215 // dismissed component threw an exception, which was documented at the
// now-obsolete URL https://github.com/flutter/engine/issues/1215 (the URL
// died in the migration to the new repo). Don't copy this test; it doesn't
// actually remove the dismissed widget, which is a violation of the
// Dismissable contract. This is not an example of good practice.
test('dismissing bottom then top (smoketest)', () { test('dismissing bottom then top (smoketest)', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
tester.pumpWidget(new Center( tester.pumpWidget(new Center(
...@@ -272,11 +282,13 @@ void main() { ...@@ -272,11 +282,13 @@ void main() {
expect(tester.findText('1'), isNotNull); expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull); expect(tester.findText('2'), isNotNull);
dismissElement(tester, tester.findText('2'), gestureDirection: DismissDirection.right); dismissElement(tester, tester.findText('2'), gestureDirection: DismissDirection.right);
tester.pump(new Duration(seconds: 1)); tester.pump(); // start the slide away
tester.pump(new Duration(seconds: 1)); // finish the slide away
expect(tester.findText('1'), isNotNull); expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNull); expect(tester.findText('2'), isNull);
dismissElement(tester, tester.findText('1'), gestureDirection: DismissDirection.right); dismissElement(tester, tester.findText('1'), gestureDirection: DismissDirection.right);
tester.pump(new Duration(seconds: 1)); tester.pump(); // start the slide away
tester.pump(new Duration(seconds: 1)); // finish the slide away (at which point the child is no longer included in the tree)
expect(tester.findText('1'), isNull); expect(tester.findText('1'), isNull);
expect(tester.findText('2'), isNull); expect(tester.findText('2'), isNull);
}); });
......
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