Commit 63e7a0e8 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add BuildContext.size as a convenience getter (#6355)

Developers need to get the size of the BuildContext sufficiently often
that we should provide a convenient getter for the value. Having this
getter is also an opportunity to catch common mistakes and provide
useful error messages that guide developers towards better patterns.

Fixes #2321
parent 4052a366
...@@ -62,11 +62,11 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv ...@@ -62,11 +62,11 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv
// renderer's box is w,h then the maximum value of the focal point is // renderer's box is w,h then the maximum value of the focal point is
// (w * _scale - w)/_scale, (h * _scale - h)/_scale. // (w * _scale - w)/_scale, (h * _scale - h)/_scale.
Point _clampFocalPoint(Point point) { Point _clampFocalPoint(Point point) {
final RenderBox box = context.findRenderObject(); final Size size = context.size;
final double inverseScale = (_scale - 1.0) / _scale; final double inverseScale = (_scale - 1.0) / _scale;
final Point bottomRight = new Point( final Point bottomRight = new Point(
box.size.width * inverseScale, size.width * inverseScale,
box.size.height * inverseScale size.height * inverseScale,
); );
return new Point(point.x.clamp(0.0, bottomRight.x), point.y.clamp(0.0, bottomRight.y)); return new Point(point.x.clamp(0.0, bottomRight.x), point.y.clamp(0.0, bottomRight.y));
} }
...@@ -101,8 +101,7 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv ...@@ -101,8 +101,7 @@ class _GridPhotoViewerState extends State<GridPhotoViewer> with SingleTickerProv
if (magnitude < _kMinFlingVelocity) if (magnitude < _kMinFlingVelocity)
return; return;
final Offset direction = details.velocity.pixelsPerSecond / magnitude; final Offset direction = details.velocity.pixelsPerSecond / magnitude;
final RenderBox box = context.findRenderObject(); final double distance = (Point.origin & context.size).shortestSide;
final double distance = (Point.origin & box.size).shortestSide;
_flingAnimation = new Tween<Point>( _flingAnimation = new Tween<Point>(
begin: _focalPoint, begin: _focalPoint,
end: _clampFocalPoint(_focalPoint + direction * -distance) end: _clampFocalPoint(_focalPoint + direction * -distance)
......
...@@ -745,13 +745,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin { ...@@ -745,13 +745,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
final RenderBox box = context.findRenderObject(); _backGestureController?.dragUpdate(details.primaryDelta / context.size.width);
_backGestureController?.dragUpdate(details.primaryDelta / box.size.width);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
final RenderBox box = context.findRenderObject(); _backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / context.size.width);
_backGestureController?.dragEnd(details.velocity.pixelsPerSecond.dx / box.size.width);
_backGestureController = null; _backGestureController = null;
} }
......
...@@ -197,10 +197,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -197,10 +197,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
} }
double get _overallDragAxisExtent { double get _overallDragAxisExtent {
final RenderBox box = context.findRenderObject(); final Size size = context.size;
assert(box != null);
assert(box.hasSize);
final Size size = box.size;
return _directionIsXAxis ? size.width : size.height; return _directionIsXAxis ? size.width : size.height;
} }
...@@ -324,8 +321,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin ...@@ -324,8 +321,7 @@ class _DismissableState extends State<Dismissable> with TickerProviderStateMixin
..addListener(_handleResizeProgressChanged); ..addListener(_handleResizeProgressChanged);
_resizeController.forward(); _resizeController.forward();
setState(() { setState(() {
RenderBox box = context.findRenderObject(); _sizePriorToCollapse = context.size;
_sizePriorToCollapse = box.size;
_resizeAnimation = new Tween<double>( _resizeAnimation = new Tween<double>(
begin: 1.0, begin: 1.0,
end: 0.0 end: 0.0
......
...@@ -1340,10 +1340,10 @@ abstract class BuildContext { ...@@ -1340,10 +1340,10 @@ abstract class BuildContext {
/// gesture callbacks) or layout or paint callbacks. /// gesture callbacks) or layout or paint callbacks.
/// ///
/// If the render object is a [RenderBox], which is the common case, then the /// If the render object is a [RenderBox], which is the common case, then the
/// size of the render object can be obtained from the [RenderBox.size] /// size of the render object can be obtained from the [size] getter. This is
/// getter. This is only valid after the layout phase, and should therefore /// only valid after the layout phase, and should therefore only be examined
/// only be examined from paint callbacks or interaction event handlers (e.g. /// from paint callbacks or interaction event handlers (e.g. gesture
/// gesture callbacks). /// callbacks).
/// ///
/// For details on the different phases of a frame, see /// For details on the different phases of a frame, see
/// [WidgetsBinding.beginFrame]. /// [WidgetsBinding.beginFrame].
...@@ -1354,6 +1354,27 @@ abstract class BuildContext { ...@@ -1354,6 +1354,27 @@ abstract class BuildContext {
/// render object is usually short. /// render object is usually short.
RenderObject findRenderObject(); RenderObject findRenderObject();
/// The size of the [RenderBox] returned by [findRenderObject].
///
/// This getter will only return a valid result after the layout phase is
/// complete. It is therefore not valid to call this from the [build] function
/// itself. It should only be called from paint callbacks or interaction event
/// handlers (e.g. gesture callbacks).
///
/// For details on the different phases of a frame, see
/// [WidgetsBinding.beginFrame].
///
/// This getter will only return a valid result if [findRenderObject] actually
/// returns a [RenderBox]. If [findRenderObject] returns a render object that
/// is not a subtype of [RenderBox] (e.g., [RenderView], this getter will
/// throw an exception in checked mode and will return null in release mode.
///
/// Calling this getter is theoretically relatively expensive (O(N) in the
/// depth of the tree), but in practice is usually cheap because the tree
/// usually has many render objects and therefore the distance to the nearest
/// render object is usually short.
Size get size;
/// Obtains the nearest widget of the given type, which must be the type of a /// Obtains the nearest widget of the given type, which must be the type of a
/// concrete [InheritedWidget] subclass, and registers this build context with /// concrete [InheritedWidget] subclass, and registers this build context with
/// that widget such that when that widget changes (or a new widget of that /// that widget such that when that widget changes (or a new widget of that
...@@ -2284,6 +2305,85 @@ abstract class Element implements BuildContext { ...@@ -2284,6 +2305,85 @@ abstract class Element implements BuildContext {
@override @override
RenderObject findRenderObject() => renderObject; RenderObject findRenderObject() => renderObject;
@override
Size get size {
assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) {
throw new FlutterError(
'Cannot get size of inactive element.\n'
'In order for an element to have a valid size, the element must be '
'active, which means it is part of the tree. Instead, this element '
'is in the $_debugLifecycleState state.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
}
if (owner._debugBuilding) {
throw new FlutterError(
'Cannot get size during build.\n'
'The size of this render object has not yet been determined because '
'the framework is still in the process of building widgets, which '
'means the render tree for this frame has not youet been determined. '
'The size getter should only be called from paint callbacks or '
'interaction event handlers (e.g. gesture callbacks).\n'
'\n'
'If you need some sizing information during build to decide which '
'widgets to build, consider using a LayoutBuilder widget, which can '
'tell you the layout constraints at a given location in the tree. See '
'<https://docs.flutter.io/flutter/widgets/LayoutBuilder-class.html> '
'for more details.\n'
'\n'
'The size getter was called for the following element:\n'
' $this\n'
);
}
return true;
});
final RenderObject renderObject = findRenderObject();
assert(() {
if (renderObject == null) {
throw new FlutterError(
'Cannot get size without a render object.\n'
'In order for an element to have a valid size, the element must have '
'an assoicated render object. This element does not have an assoicated '
'render object, which typically means that the size getter was called '
'too early in the pipeline (e.g., during the build phase) before the '
'framework has created the render tree.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
}
if (renderObject is! RenderBox) {
throw new FlutterError(
'Cannot get size from a render object that is not a RenderBox.\n'
'Instead of being a subtype of RenderBox, the render object associated '
'with this element is a ${renderObject.runtimeType}. If this type of '
'render object does have a size, consider calling findRenderObject '
'and extracting its size manually.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
}
final RenderBox box = renderObject;
if (!box.hasSize || box.needsLayout) {
throw new FlutterError(
'Cannot get size from a render object that has not been through layout.\n'
'The size of this render object has not yet been determined because '
'this render object has not yet been through layout, which typically '
'means that the size getter was called too early in the pipeline '
'(e.g., during the build pahse) before the framework has determined '
'the size and position of the render objects during layout.\n'
'The size getter was called for the following element:\n'
' $this\n'
);
}
return true;
});
if (renderObject is RenderBox)
return renderObject.size;
return null;
}
Map<Type, InheritedElement> _inheritedWidgets; Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies; Set<InheritedElement> _dependencies;
bool _hadUnsatisfiedDependencies = false; bool _hadUnsatisfiedDependencies = false;
......
...@@ -197,12 +197,8 @@ class MimicableState extends State<Mimicable> { ...@@ -197,12 +197,8 @@ class MimicableState extends State<Mimicable> {
} }
return true; return true;
}); });
RenderBox box = context.findRenderObject();
assert(box != null);
assert(box.hasSize);
assert(!box.needsLayout);
setState(() { setState(() {
_placeholderSize = box.size; _placeholderSize = context.size;
}); });
return new MimicableHandle._(this); return new MimicableHandle._(this);
} }
......
...@@ -595,8 +595,7 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr ...@@ -595,8 +595,7 @@ class ScrollableState<T extends Scrollable> extends State<T> with SingleTickerPr
/// Returns the snapped offset closest to the given scroll offset. /// Returns the snapped offset closest to the given scroll offset.
double snapScrollOffset(double scrollOffset) { double snapScrollOffset(double scrollOffset) {
RenderBox box = context.findRenderObject(); return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, context.size);
return config.snapOffsetCallback == null ? scrollOffset : config.snapOffsetCallback(scrollOffset, box.size);
} }
Simulation _createSnapSimulation(double scrollVelocity) { Simulation _createSnapSimulation(double scrollVelocity) {
......
...@@ -38,8 +38,8 @@ void main() { ...@@ -38,8 +38,8 @@ void main() {
) )
); );
RenderBox box = alignKey.currentContext.findRenderObject(); final Size size = alignKey.currentContext.size;
expect(box.size.width, equals(800.0)); expect(size.width, equals(800.0));
expect(box.size.height, equals(10.0)); expect(size.height, equals(10.0));
}); });
} }
...@@ -217,9 +217,9 @@ void main() { ...@@ -217,9 +217,9 @@ void main() {
await pageRight(tester); await pageRight(tester);
expect(currentPage, equals(5)); expect(currentPage, equals(5));
RenderBox box = globalKeys[5].currentContext.findRenderObject(); Size boxSize = globalKeys[5].currentContext.size;
expect(box.size.width, equals(pageSize.width)); expect(boxSize.width, equals(pageSize.width));
expect(box.size.height, equals(pageSize.height)); expect(boxSize.height, equals(pageSize.height));
pageSize = new Size(pageSize.height, pageSize.width); pageSize = new Size(pageSize.height, pageSize.width);
await tester.pumpWidget(buildFrame(itemsWrap: true)); await tester.pumpWidget(buildFrame(itemsWrap: true));
...@@ -231,8 +231,8 @@ void main() { ...@@ -231,8 +231,8 @@ void main() {
expect(find.text('4'), findsNothing); expect(find.text('4'), findsNothing);
expect(find.text('5'), findsOneWidget); expect(find.text('5'), findsOneWidget);
box = globalKeys[5].currentContext.findRenderObject(); boxSize = globalKeys[5].currentContext.size;
expect(box.size.width, equals(pageSize.width)); expect(boxSize.width, equals(pageSize.width));
expect(box.size.height, equals(pageSize.height)); expect(boxSize.height, equals(pageSize.height));
}); });
} }
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