Commit 4f672dc0 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #1020 from Hixie/transform

RenderFractionalTranslation and Dismissable
parents a9ddbb4e 9bad312a
...@@ -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 }) {
Matrix4 inverse = new Matrix4.zero(); if (transformHitTests) {
// TODO(abarth): Check the determinant for degeneracy. Matrix4 inverse = new Matrix4.zero();
inverse.copyInverse(_effectiveTransform); // TODO(abarth): Check the determinant for degeneracy.
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,14 +274,13 @@ class _DismissableState extends State<Dismissable> { ...@@ -274,14 +274,13 @@ class _DismissableState extends State<Dismissable> {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: new SizeObserver( child: new SizeObserver(
onSizeChanged: _handleSizeChanged, onSizeChanged: _handleSizeChanged,
child: new FadeTransition( child: new SlideTransition(
performance: _fadePerformance.view, performance: _dismissPerformance.view,
opacity: new AnimatedValue<double>(1.0, end: 0.0), position: new AnimatedValue<FractionalOffset>(
child: new SlideTransition( FractionalOffset.zero,
performance: _fadePerformance.view, end: _activeCardDragEndPoint
position: new AnimatedValue<Point>(Point.origin, 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