From 04ddf9290942674cc1554250e68e29b802ddb5fc Mon Sep 17 00:00:00 2001 From: Hixie Date: Wed, 14 Oct 2015 15:30:21 -0700 Subject: [PATCH] Transform alignment For those times where you want to spin something around a point relative to the size of your box, but you don't know the size of your box. --- .../sky/lib/src/rendering/proxy_box.dart | 58 ++++++++++-- sky/packages/sky/lib/src/widgets/basic.dart | 7 +- sky/unit/test/widget/transform_test.dart | 91 +++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) diff --git a/sky/packages/sky/lib/src/rendering/proxy_box.dart b/sky/packages/sky/lib/src/rendering/proxy_box.dart index 02619505cc8..d68ec6c8a9c 100644 --- a/sky/packages/sky/lib/src/rendering/proxy_box.dart +++ b/sky/packages/sky/lib/src/rendering/proxy_box.dart @@ -862,15 +862,41 @@ class RenderDecoratedBox extends RenderProxyBox { String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}decoration:\n${_painter.decoration.toString(prefix + " ")}\n'; } +/// An offset that's expressed as a fraction of a Size. +/// +/// FractionalOffset(0.0, 0.0) represents the top left of the Size, +/// FractionalOffset(1.0, 1.0) represents the bottom right of the Size. +class FractionalOffset { + const FractionalOffset(this.x, this.y); + final double x; + final double y; + bool operator ==(dynamic other) { + if (other is! FractionalOffset) + return false; + final FractionalOffset typedOther = other; + return x == typedOther.x && + y == typedOther.y; + } + int get hashCode { + int value = 373; + value = 37 * value + x.hashCode; + value = 37 * value + y.hashCode; + return value; + } +} + /// Applies a transformation before painting its child class RenderTransform extends RenderProxyBox { RenderTransform({ Matrix4 transform, Offset origin, + FractionalOffset alignment, RenderBox child }) : super(child) { assert(transform != null); + assert(alignment == null || (alignment.x != null && alignment.y != null)); this.transform = transform; + this.alignment = alignment; this.origin = origin; } @@ -888,6 +914,20 @@ class RenderTransform extends RenderProxyBox { markNeedsPaint(); } + /// The alignment of the origin, relative to the size of the box. + /// + /// This is equivalent to setting an origin based on the size of the box. + /// If it is specificed at the same time as an offset, both are applied. + FractionalOffset get alignment => _alignment; + FractionalOffset _alignment; + void set alignment (FractionalOffset newAlignment) { + assert(newAlignment == null || (newAlignment.x != null && newAlignment.y != null)); + if (_alignment == newAlignment) + return; + _alignment = newAlignment; + markNeedsPaint(); + } + // Note the lack of a getter for transform because Matrix4 is not immutable Matrix4 _transform; @@ -937,13 +977,19 @@ class RenderTransform extends RenderProxyBox { } Matrix4 get _effectiveTransform { - if (_origin == null) + if (_origin == null && _alignment == null) return _transform; - return new Matrix4 - .identity() - .translate(_origin.dx, _origin.dy) - .multiply(_transform) - .translate(-_origin.dx, -_origin.dy); + Matrix4 result = new Matrix4.identity(); + if (_origin != null) + result.translate(_origin.dx, _origin.dy); + if (_alignment != null) + result.translate(_alignment.x * size.width, _alignment.y * size.height); + result.multiply(_transform); + if (_alignment != null) + result.translate(-_alignment.x * size.width, -_alignment.y * size.height); + if (_origin != null) + result.translate(-_origin.dx, -_origin.dy); + return result; } bool hitTest(HitTestResult result, { Point position }) { diff --git a/sky/packages/sky/lib/src/widgets/basic.dart b/sky/packages/sky/lib/src/widgets/basic.dart index 147faf57325..1ef3bd88f44 100644 --- a/sky/packages/sky/lib/src/widgets/basic.dart +++ b/sky/packages/sky/lib/src/widgets/basic.dart @@ -23,6 +23,7 @@ export 'package:flutter/rendering.dart' show FlexAlignItems, FlexDirection, FlexJustifyContent, + FractionalOffset, Matrix4, Offset, Paint, @@ -171,19 +172,21 @@ class ClipOval extends OneChildRenderObjectWidget { // POSITIONING AND SIZING NODES class Transform extends OneChildRenderObjectWidget { - Transform({ Key key, this.transform, this.origin, Widget child }) + Transform({ Key key, this.transform, this.origin, this.alignment, Widget child }) : super(key: key, child: child) { assert(transform != null); } final Matrix4 transform; final Offset origin; + final FractionalOffset alignment; - RenderTransform createRenderObject() => new RenderTransform(transform: transform, origin: origin); + RenderTransform createRenderObject() => new RenderTransform(transform: transform, origin: origin, alignment: alignment); void updateRenderObject(RenderTransform renderObject, Transform oldWidget) { renderObject.transform = transform; renderObject.origin = origin; + renderObject.alignment = alignment; } } diff --git a/sky/unit/test/widget/transform_test.dart b/sky/unit/test/widget/transform_test.dart index d9c2a47bb5e..efa44cdf45d 100644 --- a/sky/unit/test/widget/transform_test.dart +++ b/sky/unit/test/widget/transform_test.dart @@ -48,4 +48,95 @@ void main() { expect(didReceiveTap, isTrue); }); }); + + test('Transform alignment', () { + testWidgets((WidgetTester tester) { + bool didReceiveTap = false; + tester.pumpWidget( + new Stack([ + new Positioned( + top: 100.0, + left: 100.0, + child: new Container( + width: 100.0, + height: 100.0, + decoration: new BoxDecoration( + backgroundColor: new Color(0xFF0000FF) + ) + ) + ), + new Positioned( + top: 100.0, + left: 100.0, + child: new Container( + width: 100.0, + height: 100.0, + child: new Transform( + transform: new Matrix4.identity().scale(0.5, 0.5), + alignment: new FractionalOffset(1.0, 0.5), + child: new GestureDetector( + onTap: () { + didReceiveTap = true; + }, + child: new Container() + ) + ) + ) + ) + ]) + ); + + expect(didReceiveTap, isFalse); + tester.tapAt(new Point(110.0, 110.0)); + expect(didReceiveTap, isFalse); + tester.tapAt(new Point(190.0, 150.0)); + expect(didReceiveTap, isTrue); + }); + }); + + test('Transform offset + alignment', () { + testWidgets((WidgetTester tester) { + bool didReceiveTap = false; + tester.pumpWidget( + new Stack([ + new Positioned( + top: 100.0, + left: 100.0, + child: new Container( + width: 100.0, + height: 100.0, + decoration: new BoxDecoration( + backgroundColor: new Color(0xFF0000FF) + ) + ) + ), + new Positioned( + top: 100.0, + left: 100.0, + child: new Container( + width: 100.0, + height: 100.0, + child: new Transform( + transform: new Matrix4.identity().scale(0.5, 0.5), + origin: new Offset(100.0, 0.0), + alignment: new FractionalOffset(0.0, 0.5), + child: new GestureDetector( + onTap: () { + didReceiveTap = true; + }, + child: new Container() + ) + ) + ) + ) + ]) + ); + + expect(didReceiveTap, isFalse); + tester.tapAt(new Point(110.0, 110.0)); + expect(didReceiveTap, isFalse); + tester.tapAt(new Point(190.0, 150.0)); + expect(didReceiveTap, isTrue); + }); + }); }