From e59b25b21f399295b5f747bc6910f68eaf9962dc Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 11 Mar 2016 00:31:08 -0800 Subject: [PATCH] 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 --- packages/flutter/lib/rendering.dart | 1 + packages/flutter/lib/src/rendering/box.dart | 10 ++ .../lib/src/rendering/rotated_box.dart | 110 ++++++++++++++++++ packages/flutter/lib/src/widgets/basic.dart | 21 ++++ .../flutter/test/widget/rotated_box_test.dart | 58 +++++++++ 5 files changed, 200 insertions(+) create mode 100644 packages/flutter/lib/src/rendering/rotated_box.dart create mode 100644 packages/flutter/test/widget/rotated_box_test.dart 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(); + }); + }); +}