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 {
}
}
/// 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> {
/// 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
/// clipped is of the given size.
T getClip(Size size);
......@@ -871,9 +892,22 @@ abstract class CustomClipper<T> {
/// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Point.origin & size;
/// Returns `true` if the new instance will result in a different clip
/// than the oldClipper instance.
bool shouldRepaint(@checked CustomClipper<T> oldClipper);
/// Called whenever a new instance of the custom clipper delegate class is
/// provided to the clip object, or any time that a new clip object is created
/// 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 {
......@@ -893,13 +927,33 @@ abstract class _RenderCustomClip<T> extends RenderProxyBox {
assert(newClipper != null || oldClipper != null);
if (newClipper == null || oldClipper == null ||
oldClipper.runtimeType != oldClipper.runtimeType ||
newClipper.shouldRepaint(oldClipper)) {
_clip = null;
markNeedsPaint();
markNeedsSemanticsUpdate(onlyChanges: true);
newClipper.shouldReclip(oldClipper)) {
_markNeedsClip();
}
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 _clip;
......@@ -1660,14 +1714,15 @@ abstract class CustomPainter {
/// Called whenever a new instance of the custom painter delegate class is
/// provided to the [RenderCustomPaint] object, or any time that a new
/// [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).
///
/// 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 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
/// [shouldRepaint] returns `false` (e.g. if an ancestor or descendant needed to
......
......@@ -114,6 +114,46 @@ class Dismissable extends StatefulWidget {
_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 {
@override
void initState() {
......@@ -156,17 +196,18 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
return _dragUnderway || _moveController.isAnimating;
}
Size _findSize() {
RenderBox box = context.findRenderObject();
double get _overallDragAxisExtent {
final RenderBox box = context.findRenderObject();
assert(box != null);
assert(box.hasSize);
return box.size;
final Size size = box.size;
return _directionIsXAxis ? size.width : size.height;
}
void _handleDragStart(DragStartDetails details) {
_dragUnderway = true;
if (_moveController.isAnimating) {
_dragExtent = _moveController.value * _findSize().width * _dragExtent.sign;
_dragExtent = _moveController.value * _overallDragAxisExtent * _dragExtent.sign;
_moveController.stop();
} else {
_dragExtent = 0.0;
......@@ -207,7 +248,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
});
}
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
}
bool _isFlingGesture(Velocity velocity) {
double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.pixelsPerSecond.dy;
final double vx = velocity.pixelsPerSecond.dx;
final double vy = velocity.pixelsPerSecond.dy;
if (_directionIsXAxis) {
if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
return false;
......@@ -255,7 +296,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
if (_moveController.isCompleted) {
_startResizeAnimation();
} 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;
_moveController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
} else if (_moveController.value > _kDismissThreshold) {
......@@ -340,17 +381,28 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
);
}
Widget backgroundAndChild = new SlideTransition(
Widget content = new SlideTransition(
position: _moveAnimation,
child: config.child
);
if (background != null) {
backgroundAndChild = new Stack(
children: <Widget>[
new Positioned(left: 0.0, top: 0.0, bottom: 0.0, right: 0.0, child: background),
new Viewport(child: backgroundAndChild)
]
);
List<Widget> children = <Widget>[];
if (!_moveAnimation.isDismissed) {
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.
......@@ -362,7 +414,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
onVerticalDragUpdate: _directionIsXAxis ? null : _handleDragUpdate,
onVerticalDragEnd: _directionIsXAxis ? null : _handleDragEnd,
behavior: HitTestBehavior.opaque,
child: backgroundAndChild
child: content
);
}
}
......@@ -15,7 +15,7 @@ class PathClipper extends CustomClipper<Path> {
..addRect(new Rect.fromLTWH(50.0, 50.0, 100.0, 100.0));
}
@override
bool shouldRepaint(PathClipper oldClipper) => false;
bool shouldReclip(PathClipper oldClipper) => false;
}
class ValueClipper<T> extends CustomClipper<T> {
......@@ -31,7 +31,7 @@ class ValueClipper<T> extends CustomClipper<T> {
}
@override
bool shouldRepaint(ValueClipper<T> oldClipper) {
bool shouldReclip(ValueClipper<T> oldClipper) {
return oldClipper.message != message || oldClipper.value != value;
}
}
......
......@@ -302,7 +302,7 @@ void main() {
await dismissElement(tester, itemFinder, gestureDirection: DismissDirection.startToEnd);
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'));
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