diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart index c1c3011714..b65b09b466 100644 --- a/packages/flutter/lib/rendering.dart +++ b/packages/flutter/lib/rendering.dart @@ -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'; diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index c97da57dbc..162cf04520 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.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); diff --git a/packages/flutter/lib/src/rendering/rotated_box.dart b/packages/flutter/lib/src/rendering/rotated_box.dart new file mode 100644 index 0000000000..18a151d9be --- /dev/null +++ b/packages/flutter/lib/src/rendering/rotated_box.dart @@ -0,0 +1,110 @@ +// 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 { + 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); + } +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index e4bc9fcbf6..2c800dd493 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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 diff --git a/packages/flutter/test/widget/rotated_box_test.dart b/packages/flutter/test/widget/rotated_box_test.dart new file mode 100644 index 0000000000..1a1276f0c5 --- /dev/null +++ b/packages/flutter/test/widget/rotated_box_test.dart @@ -0,0 +1,58 @@ +// 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 log = []; + Key rotatedBoxKey = new UniqueKey(); + + tester.pumpWidget( + new Center( + child: new RotatedBox( + key: rotatedBoxKey, + quarterTurns: 1, + child: new Row( + justifyContent: FlexJustifyContent.collapse, + children: [ + 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(); + }); + }); +}