Commit 93d757c3 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Dismissable should cull and clip background (#6325)

When not dismissing, the Dismissable widget should cull its background.
When a dismiss is in progress, it should clip the background to just the
part that is revealed.

Fixes #6127
parent 4a3cd610
...@@ -855,8 +855,29 @@ class RenderBackdropFilter extends RenderProxyBox { ...@@ -855,8 +855,29 @@ class RenderBackdropFilter extends RenderProxyBox {
} }
} }
/// A class that provides custom clips. /// An interface for providing custom clips.
///
/// This class is used by a number of clip widgets (e.g., [ClipRect] and
/// [ClipPath]).
///
/// The [getClip] method is called whenever the custom clip needs to be updated.
///
/// The [shouldReclip] method is called when a new instance of the class
/// is provided, to check if the new instance actually represents different
/// information.
///
/// The most efficient way to update the clip provided by this class is to
/// supply a reclip argument to the constructor of the [CustomClipper]. The
/// custom object will listen to this animation and update the clip whenever the
/// animation ticks, avoiding both the build and layout phases of the pipeline.
abstract class CustomClipper<T> { abstract class CustomClipper<T> {
/// Creates a custom clipper.
///
/// The clipper will update its clip whenever [reclip] notifies its listeners.
const CustomClipper({ Listenable reclip }) : _reclip = reclip;
final Listenable _reclip;
/// Returns a description of the clip given that the render object being /// Returns a description of the clip given that the render object being
/// clipped is of the given size. /// clipped is of the given size.
T getClip(Size size); T getClip(Size size);
...@@ -871,9 +892,22 @@ abstract class CustomClipper<T> { ...@@ -871,9 +892,22 @@ abstract class CustomClipper<T> {
/// with very small arcs in the corners), then this may be adequate. /// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Point.origin & size; Rect getApproximateClipRect(Size size) => Point.origin & size;
/// Returns `true` if the new instance will result in a different clip /// Called whenever a new instance of the custom clipper delegate class is
/// than the oldClipper instance. /// provided to the clip object, or any time that a new clip object is created
bool shouldRepaint(@checked CustomClipper<T> oldClipper); /// with a new instance of the custom painter delegate class (which amounts to
/// the same thing, because the latter is implemented in terms of the former).
///
/// If the new instance represents different information than the old
/// instance, then the method should return `true`, otherwise it should return
/// `false`.
///
/// If the method returns `false`, then the [getClip] call might be optimized
/// away.
///
/// It's possible that the [getClip] method will get called even if
/// [shouldReclip] returns `false` or if the [getClip] method is never called
/// at all (e.g. if the box changes size).
bool shouldReclip(@checked CustomClipper<T> oldClipper);
} }
abstract class _RenderCustomClip<T> extends RenderProxyBox { abstract class _RenderCustomClip<T> extends RenderProxyBox {
...@@ -893,13 +927,33 @@ abstract class _RenderCustomClip<T> extends RenderProxyBox { ...@@ -893,13 +927,33 @@ abstract class _RenderCustomClip<T> extends RenderProxyBox {
assert(newClipper != null || oldClipper != null); assert(newClipper != null || oldClipper != null);
if (newClipper == null || oldClipper == null || if (newClipper == null || oldClipper == null ||
oldClipper.runtimeType != oldClipper.runtimeType || oldClipper.runtimeType != oldClipper.runtimeType ||
newClipper.shouldRepaint(oldClipper)) { newClipper.shouldReclip(oldClipper)) {
_clip = null; _markNeedsClip();
markNeedsPaint(); }
markNeedsSemanticsUpdate(onlyChanges: true); if (attached) {
oldClipper?._reclip?.removeListener(_markNeedsClip);
newClipper?._reclip?.addListener(_markNeedsClip);
} }
} }
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_clipper?._reclip?.addListener(_markNeedsClip);
}
@override
void detach() {
_clipper?._reclip?.removeListener(_markNeedsClip);
super.detach();
}
void _markNeedsClip() {
_clip = null;
markNeedsPaint();
markNeedsSemanticsUpdate(onlyChanges: true);
}
T get _defaultClip; T get _defaultClip;
T _clip; T _clip;
...@@ -1660,14 +1714,15 @@ abstract class CustomPainter { ...@@ -1660,14 +1714,15 @@ abstract class CustomPainter {
/// Called whenever a new instance of the custom painter delegate class is /// Called whenever a new instance of the custom painter delegate class is
/// provided to the [RenderCustomPaint] object, or any time that a new /// provided to the [RenderCustomPaint] object, or any time that a new
/// [CustomPaint] object is created with a new instance of the custom painter /// [CustomPaint] object is created with a new instance of the custom painter
/// delegate class (which amounts to the same thing, since the latter is /// delegate class (which amounts to the same thing, because the latter is
/// implemented in terms of the former). /// implemented in terms of the former).
/// ///
/// If the new instance represents different information than the old /// If the new instance represents different information than the old
/// instance, then the method should return `true`, otherwise it should return /// instance, then the method should return `true`, otherwise it should return
/// `false`. /// `false`.
/// ///
/// If the method returns `false`, then the paint call might be optimized away. /// If the method returns `false`, then the [paint] call might be optimized
/// away.
/// ///
/// It's possible that the [paint] method will get called even if /// It's possible that the [paint] method will get called even if
/// [shouldRepaint] returns `false` (e.g. if an ancestor or descendant needed to /// [shouldRepaint] returns `false` (e.g. if an ancestor or descendant needed to
......
...@@ -114,6 +114,46 @@ class Dismissable extends StatefulWidget { ...@@ -114,6 +114,46 @@ class Dismissable extends StatefulWidget {
_DismissableState createState() => new _DismissableState(); _DismissableState createState() => new _DismissableState();
} }
class _DismissableClipper extends CustomClipper<Rect> {
_DismissableClipper({
this.axis,
Animation<FractionalOffset> moveAnimation
}) : moveAnimation = moveAnimation, super(reclip: moveAnimation) {
assert(axis != null);
assert(moveAnimation != null);
}
final Axis axis;
final Animation<FractionalOffset> moveAnimation;
@override
Rect getClip(Size size) {
assert(axis != null);
switch (axis) {
case Axis.horizontal:
final double offset = moveAnimation.value.dx * size.width;
if (offset < 0)
return new Rect.fromLTRB(size.width + offset, 0.0, size.width, size.height);
return new Rect.fromLTRB(0.0, 0.0, offset, size.height);
case Axis.vertical:
final double offset = moveAnimation.value.dy * size.height;
if (offset < 0)
return new Rect.fromLTRB(0.0, size.height + offset, size.width, size.height);
return new Rect.fromLTRB(0.0, 0.0, size.width, offset);
}
return null;
}
@override
Rect getApproximateClipRect(Size size) => getClip(size);
@override
bool shouldReclip(_DismissableClipper oldClipper) {
return oldClipper.axis != axis
|| oldClipper.moveAnimation.value != moveAnimation.value;
}
}
class _DismissableState extends State<Dismissable> with TickerProviderStateMixin { class _DismissableState extends State<Dismissable> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
...@@ -156,17 +196,18 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -156,17 +196,18 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
return _dragUnderway || _moveController.isAnimating; return _dragUnderway || _moveController.isAnimating;
} }
Size _findSize() { double get _overallDragAxisExtent {
RenderBox box = context.findRenderObject(); final RenderBox box = context.findRenderObject();
assert(box != null); assert(box != null);
assert(box.hasSize); assert(box.hasSize);
return box.size; final Size size = box.size;
return _directionIsXAxis ? size.width : size.height;
} }
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
_dragUnderway = true; _dragUnderway = true;
if (_moveController.isAnimating) { if (_moveController.isAnimating) {
_dragExtent = _moveController.value * _findSize().width * _dragExtent.sign; _dragExtent = _moveController.value * _overallDragAxisExtent * _dragExtent.sign;
_moveController.stop(); _moveController.stop();
} else { } else {
_dragExtent = 0.0; _dragExtent = 0.0;
...@@ -207,7 +248,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -207,7 +248,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
}); });
} }
if (!_moveController.isAnimating) { if (!_moveController.isAnimating) {
_moveController.value = _dragExtent.abs() / (_directionIsXAxis ? _findSize().width : _findSize().height); _moveController.value = _dragExtent.abs() / _overallDragAxisExtent;
} }
} }
...@@ -221,8 +262,8 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -221,8 +262,8 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
} }
bool _isFlingGesture(Velocity velocity) { bool _isFlingGesture(Velocity velocity) {
double vx = velocity.pixelsPerSecond.dx; final double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.pixelsPerSecond.dy; final double vy = velocity.pixelsPerSecond.dy;
if (_directionIsXAxis) { if (_directionIsXAxis) {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta) if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
return false; return false;
...@@ -255,7 +296,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -255,7 +296,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
if (_moveController.isCompleted) { if (_moveController.isCompleted) {
_startResizeAnimation(); _startResizeAnimation();
} else if (_isFlingGesture(details.velocity)) { } else if (_isFlingGesture(details.velocity)) {
double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy; final double flingVelocity = _directionIsXAxis ? details.velocity.pixelsPerSecond.dx : details.velocity.pixelsPerSecond.dy;
_dragExtent = flingVelocity.sign; _dragExtent = flingVelocity.sign;
_moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); _moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
} else if (_moveController.value > _kDismissThreshold) { } else if (_moveController.value > _kDismissThreshold) {
...@@ -340,17 +381,28 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -340,17 +381,28 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
); );
} }
Widget backgroundAndChild = new SlideTransition( Widget content = new SlideTransition(
position: _moveAnimation, position: _moveAnimation,
child: config.child child: config.child
); );
if (background != null) { if (background != null) {
backgroundAndChild = new Stack( List<Widget> children = <Widget>[];
children: <Widget>[
new Positioned(left: 0.0, top: 0.0, bottom: 0.0, right: 0.0, child: background), if (!_moveAnimation.isDismissed) {
new Viewport(child: backgroundAndChild) children.add(new Positioned.fill(
] child: new ClipRect(
); clipper: new _DismissableClipper(
axis: _directionIsXAxis ? Axis.horizontal : Axis.vertical,
moveAnimation: _moveAnimation,
),
child: background
)
));
}
children.add(content);
content = new Stack(children: children);
} }
// We are not resizing but we may be being dragging in config.direction. // We are not resizing but we may be being dragging in config.direction.
...@@ -362,7 +414,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -362,7 +414,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate, onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd, onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: backgroundAndChild child: content
); );
} }
} }
...@@ -15,7 +15,7 @@ class PathClipper extends CustomClipper<Path> { ...@@ -15,7 +15,7 @@ class PathClipper extends CustomClipper<Path> {
..addRect(new Rect.fromLTWH(50.0, 50.0, 100.0, 100.0)); ..addRect(new Rect.fromLTWH(50.0, 50.0, 100.0, 100.0));
} }
@override @override
bool shouldRepaint(PathClipper oldClipper) => false; bool shouldReclip(PathClipper oldClipper) => false;
} }
class ValueClipper<T> extends CustomClipper<T> { class ValueClipper<T> extends CustomClipper<T> {
...@@ -31,7 +31,7 @@ class ValueClipper<T> extends CustomClipper<T> { ...@@ -31,7 +31,7 @@ class ValueClipper<T> extends CustomClipper<T> {
} }
@override @override
bool shouldRepaint(ValueClipper<T> oldClipper) { bool shouldReclip(ValueClipper<T> oldClipper) {
return oldClipper.message != message || oldClipper.value != value; return oldClipper.message != message || oldClipper.value != value;
} }
} }
......
...@@ -302,7 +302,7 @@ void main() { ...@@ -302,7 +302,7 @@ void main() {
await dismissElement(tester, itemFinder, gestureDirection: DismissDirection.startToEnd); await dismissElement(tester, itemFinder, gestureDirection: DismissDirection.startToEnd);
await tester.pump(); await tester.pump();
expect(find.text('background'), findsNWidgets(5)); expect(find.text('background'), findsOneWidget); // The other four have been culled.
RenderBox backgroundBox = tester.firstRenderObject(find.text('background')); RenderBox backgroundBox = tester.firstRenderObject(find.text('background'));
expect(backgroundBox.size.height, equals(100.0)); expect(backgroundBox.size.height, equals(100.0));
}); });
......
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