Commit e59b25b2 authored by Adam Barth's avatar Adam Barth

Add RotatedBox which applies a rotation before layout

Transform applies its transform before painting, but sometimes you want
the widget to layout after its transform has been applied. We can't
handle general tranforms in this way because we can't couple width and
height constriants, but we can handle certain rotations.

Fixes #1214
parent 9319ac5e
......@@ -26,6 +26,7 @@ export 'src/rendering/overflow.dart';
export 'src/rendering/paragraph.dart';
export 'src/rendering/performance_overlay.dart';
export 'src/rendering/proxy_box.dart';
export 'src/rendering/rotated_box.dart';
export 'src/rendering/semantics.dart';
export 'src/rendering/shifted_box.dart';
export 'src/rendering/stack.dart';
......
......@@ -154,6 +154,16 @@ class BoxConstraints extends Constraints {
maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight));
}
/// A box constraints with the width and height constraints flipped.
BoxConstraints get flipped {
return new BoxConstraints(
minWidth: minHeight,
maxWidth: maxHeight,
minHeight: minWidth,
maxHeight: maxWidth
);
}
/// Returns box constraints with the same width constraints but with
/// unconstrainted height.
BoxConstraints widthConstraints() => new BoxConstraints(minWidth: minWidth, maxWidth: maxWidth);
......
// Copyright 2016 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 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:vector_math/vector_math_64.dart';
import 'box.dart';
import 'object.dart';
const double _kQuarterTurnsInRadians = math.PI / 2.0;
/// Rotates its child by a integral number of quarter turns.
///
/// Unlike [RenderTransform], which applies a transform just prior to painting,
/// this object applies its rotation prior to layout, which means the entire
/// rotated box consumes only as much space as required by the rotated child.
class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
RenderRotatedBox({
int quarterTurns,
RenderBox child
}) : _quarterTurns = quarterTurns {
assert(quarterTurns != null);
this.child = child;
}
/// The number of clockwise quarter turns the child should be rotated.
int get quarterTurns => _quarterTurns;
int _quarterTurns;
void set quarterTurns(int value) {
assert(value != null);
if (_quarterTurns == value)
return;
_quarterTurns = value;
markNeedsLayout();
}
bool get _isVertical => quarterTurns % 2 == 1;
double getMinIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return _isVertical ? child.getMinIntrinsicHeight(constraints.flipped) : child.getMinIntrinsicWidth(constraints);
return super.getMinIntrinsicWidth(constraints);
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return _isVertical ? child.getMaxIntrinsicHeight(constraints.flipped) : child.getMaxIntrinsicWidth(constraints);
return super.getMaxIntrinsicWidth(constraints);
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return _isVertical ? child.getMinIntrinsicWidth(constraints.flipped) : child.getMinIntrinsicHeight(constraints);
return super.getMinIntrinsicHeight(constraints);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.debugAssertIsNormalized);
if (child != null)
return _isVertical ? child.getMaxIntrinsicWidth(constraints.flipped) : child.getMaxIntrinsicHeight(constraints);
return super.getMaxIntrinsicHeight(constraints);
}
Matrix4 _paintTransform;
void performLayout() {
_paintTransform = null;
if (child != null) {
child.layout(_isVertical ? constraints.flipped : constraints, parentUsesSize: true);
size = _isVertical ? new Size(child.size.height, child.size.width) : child.size;
_paintTransform = new Matrix4.identity()
..translate(size.width / 2.0, size.height / 2.0)
..rotateZ(_kQuarterTurnsInRadians * (quarterTurns % 4))
..translate(-child.size.width / 2.0, -child.size.height / 2.0);
} else {
performResize();
}
}
bool hitTestChildren(HitTestResult result, { Point position }) {
assert(_paintTransform != null || needsLayout || child == null);
if (child == null || _paintTransform == null)
return false;
Matrix4 inverse = new Matrix4.inverted(_paintTransform);
Vector3 position3 = new Vector3(position.x, position.y, 0.0);
Vector3 transformed3 = inverse.transform3(position3);
return child.hitTest(result, position: new Point(transformed3.x, transformed3.y));
}
void _paintChild(PaintingContext context, Offset offset) {
context.paintChild(child, offset);
}
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.pushTransform(needsCompositing, offset, _paintTransform, _paintChild);
}
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (_paintTransform != null)
transform.multiply(_paintTransform);
super.applyPaintTransform(child, transform);
}
}
......@@ -320,6 +320,27 @@ class FractionalTranslation extends OneChildRenderObjectWidget {
}
}
/// Rotates its child by a integral number of quarter turns.
///
/// Unlike [Transform], which applies a transform just prior to painting,
/// this object applies its rotation prior to layout, which means the entire
/// rotated box consumes only as much space as required by the rotated child.
class RotatedBox extends OneChildRenderObjectWidget {
RotatedBox({ Key key, this.quarterTurns, Widget child })
: super(key: key, child: child) {
assert(quarterTurns != null);
}
/// The number of clockwise quarter turns the child should be rotated.
final int quarterTurns;
RenderRotatedBox createRenderObject(BuildContext context) => new RenderRotatedBox(quarterTurns: quarterTurns);
void updateRenderObject(BuildContext context, RenderRotatedBox renderObject) {
renderObject.quarterTurns = quarterTurns;
}
}
/// Insets its child by the given padding.
///
/// When passing layout constraints to its child, padding shrinks the
......
// 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/material.dart';
import 'package:test/test.dart';
void main() {
test('Rotated box control test', () {
testWidgets((WidgetTester tester) {
List<String> log = <String>[];
Key rotatedBoxKey = new UniqueKey();
tester.pumpWidget(
new Center(
child: new RotatedBox(
key: rotatedBoxKey,
quarterTurns: 1,
child: new Row(
justifyContent: FlexJustifyContent.collapse,
children: <Widget>[
new GestureDetector(
onTap: () { log.add('left'); },
child: new Container(
width: 100.0,
height: 40.0,
decoration: new BoxDecoration(backgroundColor: Colors.blue[500])
)
),
new GestureDetector(
onTap: () { log.add('right'); },
child: new Container(
width: 75.0,
height: 65.0,
decoration: new BoxDecoration(backgroundColor: Colors.blue[500])
)
),
]
)
)
)
);
RenderBox box = tester.findElementByKey(rotatedBoxKey).renderObject;
expect(box.size.width, equals(65.0));
expect(box.size.height, equals(175.0));
tester.tapAt(new Point(420.0, 280.0));
expect(log, equals(['left']));
log.clear();
tester.tapAt(new Point(380.0, 320.0));
expect(log, equals(['right']));
log.clear();
});
});
}
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