Commit c27b03b8 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add FittedBox (#6029)

This widget lets you apply an ImageFit to a child widget.

Fixes #5830
parent 12054e33
......@@ -209,6 +209,46 @@ class BoxConstraints extends Constraints {
return result;
}
/// Returns a size that attempts to meet the following conditions, in order:
///
/// - The size must satisfy these constraints.
/// - The aspect ratio of the returned size matches the aspect ratio of the
/// given size.
/// - The returned size as big as possible while still being equal to or
/// smaller than the given size.
Size constrainSizeAndAttemptToPreserveAspectRatio(Size size) {
if (isTight)
return smallest;
double width = size.width;
double height = size.height;
assert(width > 0.0);
assert(height > 0.0);
double aspectRatio = width / height;
if (width > maxWidth) {
width = maxWidth;
height = width / aspectRatio;
}
if (height > maxHeight) {
height = maxHeight;
width = height * aspectRatio;
}
if (width < minWidth) {
width = minWidth;
height = width / aspectRatio;
}
if (height < minHeight) {
height = minHeight;
width = height * aspectRatio;
}
return new Size(constrainWidth(width), constrainHeight(height));
}
/// The biggest size that satisifes the constraints.
Size get biggest => new Size(constrainWidth(), constrainHeight());
......
......@@ -114,7 +114,7 @@ class RenderImage extends RenderBox {
markNeedsPaint();
}
/// How to inscribe the image into the place allocated during layout.
/// How to inscribe the image into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
......@@ -178,36 +178,13 @@ class RenderImage extends RenderBox {
height: _height
).enforce(constraints);
if (constraints.isTight || _image == null)
if (_image == null)
return constraints.smallest;
double width = _image.width.toDouble() / _scale;
double height = _image.height.toDouble() / _scale;
assert(width > 0.0);
assert(height > 0.0);
double aspectRatio = width / height;
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));
return constraints.constrainSizeAndAttemptToPreserveAspectRatio(new Size(
_image.width.toDouble() / _scale,
_image.height.toDouble() / _scale
));
}
@override
......
......@@ -1400,6 +1400,139 @@ class RenderTransform extends RenderProxyBox {
}
}
/// Scales and positions its child within itself according to [fit].
class RenderFittedBox extends RenderProxyBox {
/// Scales and positions its child within itself.
///
/// The [fit] and [alignment] arguments must not be null.
RenderFittedBox({
RenderBox child,
ImageFit fit: ImageFit.contain,
FractionalOffset alignment: FractionalOffset.center
}) : _fit = fit, _alignment = alignment, super(child) {
assert(fit != null);
assert(alignment != null && alignment.dx != null && alignment.dy != null);
}
/// How to inscribe the child into the space allocated during layout.
ImageFit get fit => _fit;
ImageFit _fit;
set fit (ImageFit newFit) {
assert(newFit != null);
if (_fit == newFit)
return;
_fit = newFit;
_clearPaintData();
markNeedsPaint();
}
/// How to align the child within its parent's bounds.
///
/// An alignment of (0.0, 0.0) aligns the child to the top-left corner of its
/// parent's bounds. An alignment of (1.0, 0.5) aligns the child to the middle
/// of the right edge of its parent's bounds.
FractionalOffset get alignment => _alignment;
FractionalOffset _alignment;
set alignment (FractionalOffset newAlignment) {
assert(newAlignment != null && newAlignment.dx != null && newAlignment.dy != null);
if (_alignment == newAlignment)
return;
_alignment = newAlignment;
_clearPaintData();
markNeedsPaint();
}
@override
void performLayout() {
if (child != null) {
child.layout(const BoxConstraints(), parentUsesSize: true);
size = constraints.constrainSizeAndAttemptToPreserveAspectRatio(child.size);
_clearPaintData();
} else {
size = constraints.smallest;
}
}
bool _hasVisualOverflow;
Matrix4 _transform;
void _clearPaintData() {
_hasVisualOverflow = null;
_transform = null;
}
void _updatePaintData() {
if (_transform != null)
return;
if (child == null) {
_hasVisualOverflow = false;
_transform = new Matrix4.identity();
} else {
final Size childSize = child.size;
final FittedSizes sizes = applyImageFit(_fit, childSize, size);
final double scaleX = sizes.destination.width / sizes.source.width;
final double scaleY = sizes.destination.height / sizes.source.height;
final Rect sourceRect = _alignment.inscribe(sizes.source, Point.origin & childSize);
final Rect destinationRect = _alignment.inscribe(sizes.destination, Point.origin & size);
_hasVisualOverflow = sourceRect.width < childSize.width || sourceRect.height < childSize.width;
_transform = new Matrix4.translationValues(destinationRect.left, destinationRect.top, 0.0)
..scale(scaleX, scaleY)
..translate(-sourceRect.left, -sourceRect.top);
}
}
void _paintChildWithTransform(PaintingContext context, Offset offset) {
Offset childOffset = MatrixUtils.getAsTranslation(_transform);
if (childOffset == null)
context.pushTransform(needsCompositing, offset, _transform, super.paint);
else
super.paint(context, offset + childOffset);
}
@override
void paint(PaintingContext context, Offset offset) {
_updatePaintData();
if (child != null) {
if (_hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintChildWithTransform);
else
_paintChildWithTransform(context, offset);
}
}
@override
bool hitTest(HitTestResult result, { Point position }) {
_updatePaintData();
Matrix4 inverse;
try {
inverse = new Matrix4.inverted(_transform);
} catch (e) {
// We cannot invert the effective transform. That means the child
// doesn't appear on screen and cannot be hit.
return false;
}
Vector3 position3 = new Vector3(position.x, position.y, 0.0);
Vector3 transformed3 = inverse.transform3(position3);
position = new Point(transformed3.x, transformed3.y);
return super.hitTest(result, position: position);
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
_updatePaintData();
transform.multiply(_transform);
super.applyPaintTransform(child, transform);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('fit: $fit');
description.add('alignment: $alignment');
}
}
/// Applies a translation transformation before painting its child.
///
/// The translation is expressed as a [FractionalOffset] relative to the
......
......@@ -432,6 +432,42 @@ class Transform extends SingleChildRenderObjectWidget {
}
}
/// Scales and positions its child within itself according to [fit].
class FittedBox extends SingleChildRenderObjectWidget {
/// Creates a widget that scales and positions its child within itself according to [fit].
///
/// The [fit] and [alignment] arguments must not be null.
FittedBox({
Key key,
this.fit: ImageFit.contain,
this.alignment: FractionalOffset.center,
Widget child
}) : super(key: key, child: child) {
assert(fit != null);
assert(alignment != null && alignment.dx != null && alignment.dy != null);
}
/// How to inscribe the child into the space allocated during layout.
final ImageFit fit;
/// How to align the child within its parent's bounds.
///
/// An alignment of (0.0, 0.0) aligns the child to the top-left corner of its
/// parent's bounds. An alignment of (1.0, 0.5) aligns the child to the middle
/// of the right edge of its parent's bounds.
final FractionalOffset alignment;
@override
RenderFittedBox createRenderObject(BuildContext context) => new RenderFittedBox(fit: fit, alignment: alignment);
@override
void updateRenderObject(BuildContext context, RenderFittedBox renderObject) {
renderObject
..fit = fit
..alignment = alignment;
}
}
/// A widget that applies a translation expressed as a fraction of the box's
/// size before painting its child.
class FractionalTranslation extends SingleChildRenderObjectWidget {
......@@ -2227,7 +2263,7 @@ class RawImage extends LeafRenderObjectWidget {
/// If non-null, apply this color filter to the image before painting.
final Color color;
/// How to inscribe the image into the place allocated during layout.
/// How to inscribe the image into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
......
......@@ -129,7 +129,7 @@ class Image extends StatefulWidget {
/// If non-null, apply this color filter to the image before painting.
final Color color;
/// How to inscribe the image into the place allocated during layout.
/// How to inscribe the image into the space allocated during layout.
///
/// The default varies based on the other fields. See the discussion at
/// [paintImage].
......
// 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';
void main() {
testWidgets('Can size according to aspect ratio', (WidgetTester tester) async {
Key outside = new UniqueKey();
Key inside = new UniqueKey();
await tester.pumpWidget(
new Center(
child: new Container(
width: 200.0,
child: new FittedBox(
key: outside,
child: new Container(
key: inside,
width: 100.0,
height: 50.0,
)
)
)
)
);
RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside));
expect(outsideBox.size.width, 200.0);
expect(outsideBox.size.height, 100.0);
RenderBox insideBox = tester.firstRenderObject(find.byKey(inside));
expect(insideBox.size.width, 100.0);
expect(insideBox.size.height, 50.0);
Point insidePoint = insideBox.localToGlobal(new Point(100.0, 50.0));
Point outsidePoint = outsideBox.localToGlobal(new Point(200.0, 100.0));
expect(insidePoint, equals(outsidePoint));
});
testWidgets('Can contain child', (WidgetTester tester) async {
Key outside = new UniqueKey();
Key inside = new UniqueKey();
await tester.pumpWidget(
new Center(
child: new Container(
width: 200.0,
height: 200.0,
child: new FittedBox(
key: outside,
child: new Container(
key: inside,
width: 100.0,
height: 50.0,
)
)
)
)
);
RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside));
expect(outsideBox.size.width, 200.0);
expect(outsideBox.size.height, 200.0);
RenderBox insideBox = tester.firstRenderObject(find.byKey(inside));
expect(insideBox.size.width, 100.0);
expect(insideBox.size.height, 50.0);
Point insidePoint = insideBox.localToGlobal(new Point(100.0, 0.0));
Point outsidePoint = outsideBox.localToGlobal(new Point(200.0, 50.0));
expect(insidePoint, equals(outsidePoint));
});
testWidgets('Child can conver', (WidgetTester tester) async {
Key outside = new UniqueKey();
Key inside = new UniqueKey();
await tester.pumpWidget(
new Center(
child: new Container(
width: 200.0,
height: 200.0,
child: new FittedBox(
key: outside,
fit: ImageFit.cover,
child: new Container(
key: inside,
width: 100.0,
height: 50.0,
)
)
)
)
);
RenderBox outsideBox = tester.firstRenderObject(find.byKey(outside));
expect(outsideBox.size.width, 200.0);
expect(outsideBox.size.height, 200.0);
RenderBox insideBox = tester.firstRenderObject(find.byKey(inside));
expect(insideBox.size.width, 100.0);
expect(insideBox.size.height, 50.0);
Point insidePoint = insideBox.localToGlobal(new Point(50.0, 25.0));
Point outsidePoint = outsideBox.localToGlobal(new Point(100.0, 100.0));
expect(insidePoint, equals(outsidePoint));
});
}
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