Commit 9bc64540 authored by Adam Barth's avatar Adam Barth

Improve hit testing

Now a RenderBox is considered hit if one of its children are hit or it itself
decides that it's hit. In particular, empty space inside a flex won't be hit
because none of the children are located there and a RenderFlex doesn't
consider itself hittable.

Fixes #53
Fixes #1221
parent 7b36d944
......@@ -22,6 +22,8 @@ class IconButton extends StatelessComponent {
final VoidCallback onPressed;
Widget build(BuildContext context) {
// TODO(abarth): We should use a radial reaction here so you can hit the
// 8.0 pixel padding as well as the icon.
return new GestureDetector(
onTap: onPressed,
child: new Padding(
......
......@@ -248,6 +248,8 @@ class _RenderInkSplashes extends RenderProxyBox {
onLongPress();
}
bool hitTestSelf(Point position) => true;
void paint(PaintingContext context, Offset offset) {
if (!_splashes.isEmpty) {
final PaintingCanvas canvas = context.canvas;
......
......@@ -205,8 +205,8 @@ class _RenderTabBar extends RenderBox with
reportLayoutChangedIfNeeded();
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
void _paintIndicator(PaintingCanvas canvas, RenderBox selectedTab, Offset offset) {
......
......@@ -219,8 +219,8 @@ class RenderAutoLayout extends RenderBox
// only indicates that the value has been flushed to the variable.
}
void hitTestChildren(HitTestResult result, {Point position}) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, {Point position}) {
return defaultHitTestChildren(result, position: position);
}
void paint(PaintingContext context, Offset offset) {
......
......@@ -219,8 +219,8 @@ class RenderBlock extends RenderBlockBase {
defaultPaint(context, offset);
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
}
......@@ -423,11 +423,11 @@ class RenderBlockViewport extends RenderBlockBase {
transform.translate(startOffset, 0.0);
}
void hitTestChildren(HitTestResult result, { Point position }) {
bool hitTestChildren(HitTestResult result, { Point position }) {
if (isVertical)
defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
return defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
else
defaultHitTestChildren(result, position: position + new Offset(-startOffset, 0.0));
return defaultHitTestChildren(result, position: position + new Offset(-startOffset, 0.0));
}
void debugDescribeSettings(List<String> settings) {
......
......@@ -545,20 +545,25 @@ abstract class RenderBox extends RenderObject {
bool hitTest(HitTestResult result, { Point position }) {
if (position.x >= 0.0 && position.x < _size.width &&
position.y >= 0.0 && position.y < _size.height) {
hitTestChildren(result, position: position);
result.add(new BoxHitTestEntry(this, position));
return true;
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(new BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
/// Override this function if this render object can be hit even if its
/// children were not hit
bool hitTestSelf(Point position) => false;
/// Override this function to check whether any children are located at the
/// given position
///
/// Typically children should be hit tested in reverse paint order so that
/// hit tests at locations where children overlap hit the child that is
/// visually "on top" (i.e., paints later).
void hitTestChildren(HitTestResult result, { Point position }) { }
bool hitTestChildren(HitTestResult result, { Point position }) => false;
/// Multiply the transform from the parent's coordinate system to this box's
/// coordinate system into the given transform
......
......@@ -108,7 +108,7 @@ class RenderCustomMultiChildLayoutBox extends RenderBox
defaultPaint(context, offset);
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
}
......@@ -30,6 +30,8 @@ class RenderErrorBox extends RenderBox {
bool get sizedByParent => true;
bool hitTestSelf(Point position) => true;
void performResize() {
size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight));
}
......
......@@ -542,8 +542,8 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
void paint(PaintingContext context, Offset offset) {
......
......@@ -134,8 +134,8 @@ class RenderGrid extends RenderBox with ContainerRenderObjectMixin<RenderBox, Gr
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
void paint(PaintingContext context, Offset offset) {
......
......@@ -179,6 +179,8 @@ class RenderImage extends RenderBox {
return _sizeForConstraints(constraints).height;
}
bool hitTestSelf(Point position) => true;
void performLayout() {
size = _sizeForConstraints(constraints);
}
......
......@@ -125,11 +125,8 @@ class RenderOverflowBox extends RenderBox with RenderObjectWithChildMixin<Render
child.layout(_getInnerConstraints(constraints));
}
void hitTestChildren(HitTestResult result, { Point position }) {
if (child != null)
child.hitTest(result, position: position);
else
super.hitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return child?.hitTest(result, position: position) ?? false;
}
void paint(PaintingContext context, Offset offset) {
......@@ -195,11 +192,8 @@ class RenderSizedOverflowBox extends RenderBox with RenderObjectWithChildMixin<R
child.layout(constraints);
}
void hitTestChildren(HitTestResult result, { Point position }) {
if (child != null)
child.hitTest(result, position: position);
else
super.hitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return child?.hitTest(result, position: position) ?? false;
}
void paint(PaintingContext context, Offset offset) {
......
......@@ -96,6 +96,8 @@ class RenderParagraph extends RenderBox {
return textPainter.computeDistanceToActualBaseline(baseline);
}
bool hitTestSelf(Point position) => true;
void performLayout() {
layoutText(constraints);
size = constraints.constrain(textPainter.size);
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/painting.dart';
......@@ -70,11 +71,8 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
if (child != null)
child.hitTest(result, position: position);
else
super.hitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return child?.hitTest(result, position: position) ?? false;
}
void paint(PaintingContext context, Offset offset) {
......@@ -625,6 +623,15 @@ class RenderClipOval extends RenderProxyBox {
return _cachedPath;
}
bool hitTest(HitTestResult result, { Point position }) {
Point center = size.center(Point.origin);
Offset offset = new Offset((position.x - center.x) / size.width,
(position.y - center.y) / size.height);
if (offset.distance > 0.5)
return false;
return super.hitTest(result, position: position);
}
void paint(PaintingContext context, Offset offset) {
if (child != null) {
Rect rect = offset & size;
......@@ -706,6 +713,19 @@ class RenderDecoratedBox extends RenderProxyBox {
super.detach();
}
bool hitTestSelf(Point position) {
switch (_painter.decoration.shape) {
case Shape.rectangle:
// TODO(abarth): We should check the border radius.
return true;
case Shape.circle:
// Circles are inscribed into our smallest dimension.
Point center = size.center(Point.origin);
double distance = (position - center).distance;
return distance <= math.min(size.width, size.height) / 2.0;
}
}
void paint(PaintingContext context, Offset offset) {
assert(size.width != null);
assert(size.height != null);
......@@ -894,6 +914,7 @@ class RenderSizeObserver extends RenderProxyBox {
/// Called when its time to paint into the given canvas
typedef void CustomPaintCallback(PaintingCanvas canvas, Size size);
typedef bool CustomHitTestCallback(Point position);
/// Delegates its painting to [onPaint]
///
......@@ -911,6 +932,7 @@ class RenderCustomPaint extends RenderProxyBox {
RenderCustomPaint({
CustomPaintCallback onPaint,
this.onHitTest,
RenderBox child
}) : super(child) {
assert(onPaint != null);
......@@ -931,11 +953,17 @@ class RenderCustomPaint extends RenderProxyBox {
markNeedsPaint();
}
CustomHitTestCallback onHitTest;
void attach() {
assert(_onPaint != null);
super.attach();
}
bool hitTestSelf(Point position) {
return onHitTest == null || onHitTest(position);
}
void paint(PaintingContext context, Offset offset) {
assert(_onPaint != null);
context.canvas.translate(offset.dx, offset.dy);
......
......@@ -60,12 +60,14 @@ abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixi
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
child.hitTest(result, position: new Point(position.x - childParentData.position.x,
position.y - childParentData.position.y));
final Point childPosition = new Point(position.x - childParentData.position.x,
position.y - childParentData.position.y);
return child.hitTest(result, position: childPosition);
}
return false;
}
}
......
......@@ -357,8 +357,8 @@ abstract class RenderStackBase extends RenderBox
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
defaultHitTestChildren(result, position: position);
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position);
}
void paintStack(PaintingContext context, Offset offset);
......@@ -455,15 +455,15 @@ class RenderIndexedStack extends RenderStackBase {
return child;
}
void hitTestChildren(HitTestResult result, { Point position }) {
bool hitTestChildren(HitTestResult result, { Point position }) {
if (firstChild == null)
return;
return false;
assert(position != null);
RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
Point transformed = new Point(position.x - childParentData.position.x,
position.y - childParentData.position.y);
child.hitTest(result, position: transformed);
return child.hitTest(result, position: transformed);
}
void paintStack(PaintingContext context, Offset offset) {
......
......@@ -73,4 +73,6 @@ abstract class RenderToggleable extends RenderConstrainedBox {
if (onChanged != null)
onChanged(!_value);
}
bool hitTestSelf(Point position) => true;
}
......@@ -160,11 +160,12 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
transform.translate(-scrollOffset.dx, -scrollOffset.dy);
}
void hitTestChildren(HitTestResult result, { Point position }) {
bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
assert(child.parentData is BoxParentData);
Point transformed = position + _scrollOffsetRoundedToIntegerDevicePixels;
child.hitTest(result, position: transformed);
return child.hitTest(result, position: transformed);
}
return false;
}
}
......@@ -138,7 +138,7 @@ class DecoratedBox extends OneChildRenderObjectWidget {
}
class CustomPaint extends OneChildRenderObjectWidget {
CustomPaint({ Key key, this.onPaint, this.token, Widget child })
CustomPaint({ Key key, this.onPaint, this.onHitTest, this.token, Widget child })
: super(key: key, child: child) {
assert(onPaint != null);
}
......@@ -151,19 +151,23 @@ class CustomPaint extends OneChildRenderObjectWidget {
/// has a more stable identity.
final CustomPaintCallback onPaint;
final CustomHitTestCallback onHitTest;
/// This widget repaints whenever you supply a new token.
final Object token;
RenderCustomPaint createRenderObject() => new RenderCustomPaint(onPaint: onPaint);
RenderCustomPaint createRenderObject() => new RenderCustomPaint(onPaint: onPaint, onHitTest: onHitTest);
void updateRenderObject(RenderCustomPaint renderObject, CustomPaint oldWidget) {
if (oldWidget.token != token)
renderObject.markNeedsPaint();
renderObject.onPaint = onPaint;
renderObject.onHitTest = onHitTest;
}
void didUnmountRenderObject(RenderCustomPaint renderObject) {
renderObject.onPaint = null;
renderObject.onHitTest = null;
}
}
......
......@@ -17,29 +17,23 @@ const Color _kTransparent = const Color(0x00000000);
class ModalBarrier extends StatelessComponent {
ModalBarrier({
Key key,
this.color
this.color: _kTransparent
}) : super(key: key);
final Color color;
Widget build(BuildContext context) {
Widget child;
if (color != null) {
child = new DecoratedBox(
decoration: new BoxDecoration(
backgroundColor: color
)
);
}
return new Listener(
onPointerDown: (_) {
Navigator.of(context).pop();
},
child: new ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: child
child: new DecoratedBox(
decoration: new BoxDecoration(
backgroundColor: color
)
)
)
);
}
......
......@@ -5,8 +5,14 @@ import 'rendering_tester.dart';
void main() {
test('Should be able to hit with negative scroll offset', () {
RenderBox green = new RenderDecoratedBox(
decoration: new BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
));
RenderBox size = new RenderConstrainedBox(
additionalConstraints: new BoxConstraints.tight(const Size(100.0, 100.0)));
additionalConstraints: new BoxConstraints.tight(const Size(100.0, 100.0)),
child: green);
RenderBox red = new RenderDecoratedBox(
decoration: new BoxDecoration(
......@@ -21,10 +27,10 @@ void main() {
result = new HitTestResult();
renderView.hitTest(result, position: new Point(15.0, 0.0));
expect(result.path.first.target, equals(viewport));
expect(result.path.first.target.runtimeType, equals(TestRenderView));
result = new HitTestResult();
renderView.hitTest(result, position: new Point(15.0, 15.0));
expect(result.path.first.target, equals(size));
expect(result.path.first.target, equals(green));
});
}
......@@ -23,7 +23,11 @@ void main() {
onVerticalDragEnd: (Offset velocity) {
didEndDrag = true;
},
child: new Container()
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
)
)
);
tester.pumpWidget(widget);
......@@ -70,7 +74,11 @@ void main() {
onVerticalDragEnd: (Offset velocity) { gestureCount += 1; },
onHorizontalDragUpdate: (_) { fail("gesture should not match"); },
onHorizontalDragEnd: (Offset velocity) { fail("gesture should not match"); },
child: new Container()
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
)
)
);
tester.pumpWidget(widget);
......@@ -106,7 +114,11 @@ void main() {
onPanEnd: (_) {
didEndPan = true;
},
child: new Container()
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
)
)
)
);
......
......@@ -21,9 +21,14 @@ void main() {
content: new Text(helloSnackBar)
);
},
child: new Center(
key: tapTarget,
child: new Placeholder(key: placeholderKey)
child: new Container(
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00)
),
child: new Center(
key: tapTarget,
child: new Placeholder(key: placeholderKey)
)
)
);
}
......
......@@ -33,7 +33,11 @@ void main() {
onTap: () {
didReceiveTap = true;
},
child: new Container()
child: new Container(
decoration: new BoxDecoration(
backgroundColor: new Color(0xFF00FFFF)
)
)
)
)
)
......@@ -78,7 +82,11 @@ void main() {
onTap: () {
didReceiveTap = true;
},
child: new Container()
child: new Container(
decoration: new BoxDecoration(
backgroundColor: new Color(0xFF00FFFF)
)
)
)
)
)
......@@ -124,7 +132,11 @@ void main() {
onTap: () {
didReceiveTap = true;
},
child: new Container()
child: new Container(
decoration: new BoxDecoration(
backgroundColor: new Color(0xFF00FFFF)
)
)
)
)
)
......
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