Commit 6a54e122 authored by Adam Barth's avatar Adam Barth

AspectRatio should attempt to fit its height

If there's not enough height for the given aspect ratio, we now try to see if
using the height to infer the width will fit the given constraints. The
algorithm we use is similar to the one we use for RenderImage.

Fixes #2620
parent b9f28e6f
...@@ -345,22 +345,32 @@ class RenderFractionallySizedBox extends RenderProxyBox { ...@@ -345,22 +345,32 @@ class RenderFractionallySizedBox extends RenderProxyBox {
} }
} }
/// Forces child to layout at a specific aspect ratio. /// Attempts to size the child to a specific aspect ratio.
/// ///
/// The width of this render object is the largest width permited by the layout /// The render object first tries the largest width permited by the layout
/// constraints. The height of the render object is determined by applying the /// constraints. The height of the render object is determined by applying the
/// given aspect ratio to the width, expressed as a ratio of width to height. /// given aspect ratio to the width, expressed as a ratio of width to height.
/// For example, a 16:9 width:height aspect ratio would have a value of 16.0/9.0.
/// ///
/// For example, given an aspect ratio of 2.0 and layout constraints that /// For example, a 16:9 width:height aspect ratio would have a value of
/// require the width to be between 0.0 and 100.0 and the height to be between /// 16.0/9.0. If the maximum width is infinite, the initial width is determined
/// 0.0 and 100.0, we'll select a width of 100.0 (the biggest allowed) and a /// by applying the aspect ratio to the maximum height.
/// height of 50.0 (to match the aspect ratio). ///
/// Now consider a second example, this time with an aspect ratio of 2.0 and
/// layout constraints that require the width to be between 0.0 and 100.0 and
/// the height to be between 0.0 and 100.0. We'll select a width of 100.0 (the
/// biggest allowed) and a height of 50.0 (to match the aspect ratio).
/// ///
/// In that same situation, if the aspect ratio is 0.5, we'll also select a /// In that same situation, if the aspect ratio is 0.5, we'll also select a
/// width of 100.0 (still the biggest allowed) and we'll attempt to use a height /// width of 100.0 (still the biggest allowed) and we'll attempt to use a height
/// of 200.0. Unfortunately, that violates the constraints and we'll end up with /// of 200.0. Unfortunately, that violates the constraints because the child can
/// a height of 100.0 instead. /// be at most 100.0 pixels tall. The render object will then take that value
/// and apply the aspect ratio again to obtain a width of 50.0. That width is
/// permitted by the constraints and the child receives a width of 50.0 and a
/// height of 100.0. If the width were not permitted, the render object would
/// continue iterating through the constraints. If the render object does not
/// find a feasible size after consulting each constraint, the render object
/// will eventually select a size for the child that meets the layout
/// constraints but fails to meet the aspect ratio constraints.
class RenderAspectRatio extends RenderProxyBox { class RenderAspectRatio extends RenderProxyBox {
RenderAspectRatio({ RenderAspectRatio({
RenderBox child, RenderBox child,
...@@ -369,7 +379,7 @@ class RenderAspectRatio extends RenderProxyBox { ...@@ -369,7 +379,7 @@ class RenderAspectRatio extends RenderProxyBox {
assert(_aspectRatio != null); assert(_aspectRatio != null);
} }
/// The aspect ratio to use when computing the height from the width. /// The aspect ratio to attempt to use.
/// ///
/// The aspect ratio is expressed as a ratio of width to height. For example, /// The aspect ratio is expressed as a ratio of width to height. For example,
/// a 16:9 width:height aspect ratio would have a value of 16.0/9.0. /// a 16:9 width:height aspect ratio would have a value of 16.0/9.0.
...@@ -383,28 +393,83 @@ class RenderAspectRatio extends RenderProxyBox { ...@@ -383,28 +393,83 @@ class RenderAspectRatio extends RenderProxyBox {
markNeedsLayout(); markNeedsLayout();
} }
double getMinIntrinsicWidth(BoxConstraints constraints) {
return constraints.minWidth;
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
return constraints.maxWidth;
}
double getMinIntrinsicHeight(BoxConstraints constraints) { double getMinIntrinsicHeight(BoxConstraints constraints) {
return _applyAspectRatio(constraints).height; return constraints.minHeight;
} }
double getMaxIntrinsicHeight(BoxConstraints constraints) { double getMaxIntrinsicHeight(BoxConstraints constraints) {
return _applyAspectRatio(constraints).height; return constraints.maxHeight;
} }
Size _applyAspectRatio(BoxConstraints constraints) { Size _applyAspectRatio(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized); assert(constraints.debugAssertIsNormalized);
double width = constraints.constrainWidth(); assert(() {
double height = constraints.constrainHeight(width / _aspectRatio); if (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight) {
return new Size(width, height); throw new RenderingError(
} '$runtimeType has unbounded constraints.\n'
'This $runtimeType was given an aspect ratio of $aspectRatio but was given '
'both unbounded width and unbounded height constraints. Because both '
'constraints were unbounded, this render object doesn\'t know how much '
'size to consume.'
);
}
return true;
});
bool get sizedByParent => true; if (constraints.isTight)
return constraints.smallest;
void performResize() { double width = constraints.maxWidth;
size = _applyAspectRatio(constraints); double height;
// We default to picking the height based on the width, but if the width
// would be infinite, that's not sensible so we try to infer the height
// from the width.
if (width.isFinite) {
height = width / _aspectRatio;
} else {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
// Similar to RenderImage, we iteratively attempt to fit within the given
// constraings while maintaining the given aspect ratio. The order of
// applying the constraints is also biased towards inferring the height
// from the width.
if (width > constraints.maxWidth) {
width = constraints.maxWidth;
height = width / _aspectRatio;
}
if (height > constraints.maxHeight) {
height = constraints.maxHeight;
width = height * _aspectRatio;
}
if (width < constraints.minWidth) {
width = constraints.minWidth;
height = width / _aspectRatio;
}
if (height < constraints.minHeight) {
height = constraints.minHeight;
width = height * _aspectRatio;
}
return constraints.constrain(new Size(width, height));
} }
void performLayout() { void performLayout() {
size = _applyAspectRatio(constraints);
if (child != null) if (child != null)
child.layout(new BoxConstraints.tight(size)); child.layout(new BoxConstraints.tight(size));
} }
......
...@@ -702,16 +702,39 @@ class OffStage extends OneChildRenderObjectWidget { ...@@ -702,16 +702,39 @@ class OffStage extends OneChildRenderObjectWidget {
RenderOffStage createRenderObject(BuildContext context) => new RenderOffStage(); RenderOffStage createRenderObject(BuildContext context) => new RenderOffStage();
} }
/// Forces child to layout at a specific aspect ratio. /// Attempts to size the child to a specific aspect ratio.
/// ///
/// See [RenderAspectRatio] for details. /// The widget first tries the largest width permited by the layout
/// constraints. The height of the widget is determined by applying the
/// given aspect ratio to the width, expressed as a ratio of width to height.
///
/// For example, a 16:9 width:height aspect ratio would have a value of
/// 16.0/9.0. If the maximum width is infinite, the initial width is determined
/// by applying the aspect ratio to the maximum height.
///
/// Now consider a second example, this time with an aspect ratio of 2.0 and
/// layout constraints that require the width to be between 0.0 and 100.0 and
/// the height to be between 0.0 and 100.0. We'll select a width of 100.0 (the
/// biggest allowed) and a height of 50.0 (to match the aspect ratio).
///
/// In that same situation, if the aspect ratio is 0.5, we'll also select a
/// width of 100.0 (still the biggest allowed) and we'll attempt to use a height
/// of 200.0. Unfortunately, that violates the constraints because the child can
/// be at most 100.0 pixels tall. The widget will then take that value
/// and apply the aspect ratio again to obtain a width of 50.0. That width is
/// permitted by the constraints and the child receives a width of 50.0 and a
/// height of 100.0. If the width were not permitted, the widget would
/// continue iterating through the constraints. If the widget does not
/// find a feasible size after consulting each constraint, the widget
/// will eventually select a size for the child that meets the layout
/// constraints but fails to meet the aspect ratio constraints.
class AspectRatio extends OneChildRenderObjectWidget { class AspectRatio extends OneChildRenderObjectWidget {
AspectRatio({ Key key, this.aspectRatio, Widget child }) AspectRatio({ Key key, this.aspectRatio, Widget child })
: super(key: key, child: child) { : super(key: key, child: child) {
assert(aspectRatio != null); assert(aspectRatio != null);
} }
/// The aspect ratio to use when computing the height from the width. /// The aspect ratio to attempt to use.
/// ///
/// The aspect ratio is expressed as a ratio of width to height. For example, /// The aspect ratio is expressed as a ratio of width to height. For example,
/// a 16:9 width:height aspect ratio would have a value of 16.0/9.0. /// a 16:9 width:height aspect ratio would have a value of 16.0/9.0.
......
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
Size _getSize(WidgetTester tester, BoxConstraints constraints, double aspectRatio) {
Key childKey = new UniqueKey();
tester.pumpWidget(
new Center(
child: new ConstrainedBox(
constraints: constraints,
child: new AspectRatio(
aspectRatio: aspectRatio,
child: new Container(
key: childKey
)
)
)
)
);
RenderBox box = tester.findElementByKey(childKey).renderObject;
return box.size;
}
void main() {
test('Aspect ratio control test', () {
testWidgets((WidgetTester tester) {
expect(_getSize(tester, new BoxConstraints.loose(new Size(500.0, 500.0)), 2.0), equals(new Size(500.0, 250.0)));
expect(_getSize(tester, new BoxConstraints.loose(new Size(500.0, 500.0)), 0.5), equals(new Size(250.0, 500.0)));
});
});
test('Aspect ratio infinite width', () {
testWidgets((WidgetTester tester) {
Key childKey = new UniqueKey();
tester.pumpWidget(
new Center(
child: new Viewport(
scrollDirection: Axis.horizontal,
child: new AspectRatio(
aspectRatio: 2.0,
child: new Container(
key: childKey
)
)
)
)
);
RenderBox box = tester.findElementByKey(childKey).renderObject;
expect(box.size, equals(new Size(1200.0, 600.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