Merge pull request #1896 from abarth/autolayout2
Add support for autolayout to widgets
diff --git a/examples/layers/rendering/autolayout.dart b/examples/layers/rendering/autolayout.dart
index c2c30c5..f89f4bf 100644
--- a/examples/layers/rendering/autolayout.dart
+++ b/examples/layers/rendering/autolayout.dart
@@ -1,4 +1,4 @@
-// Copyright 2015 The Chromium Authors. All rights reserved.
+// 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.
@@ -8,6 +8,45 @@
import 'package:cassowary/cassowary.dart' as al;
import 'package:flutter/rendering.dart';
+class _MyAutoLayoutDelegate extends AutoLayoutDelegate {
+ AutoLayoutParams p1 = new AutoLayoutParams();
+ AutoLayoutParams p2 = new AutoLayoutParams();
+ AutoLayoutParams p3 = new AutoLayoutParams();
+ AutoLayoutParams p4 = new AutoLayoutParams();
+
+ List<al.Constraint> getConstraints(AutoLayoutParams parentParams) {
+ return <al.Constraint>[
+ // Sum of widths of each box must be equal to that of the container
+ (p1.width + p2.width + p3.width == parentParams.width) as al.Constraint,
+
+ // The boxes must be stacked left to right
+ p1.rightEdge <= p2.leftEdge,
+ p2.rightEdge <= p3.leftEdge,
+
+ // The widths of the first and the third boxes should be equal
+ (p1.width == p3.width) as al.Constraint,
+
+ // The width of the second box should be twice as much as that of the first
+ // and third
+ (p2.width * al.cm(2.0) == p1.width) as al.Constraint,
+
+ // The height of the three boxes should be equal to that of the container
+ (p1.height == p2.height) as al.Constraint,
+ (p2.height == p3.height) as al.Constraint,
+ (p3.height == parentParams.height) as al.Constraint,
+
+ // The fourth box should be half as wide as the second and must be attached
+ // to the right edge of the same (by its center)
+ (p4.width == p2.width / al.cm(2.0)) as al.Constraint,
+ (p4.height == al.cm(50.0)) as al.Constraint,
+ (p4.horizontalCenter == p2.rightEdge) as al.Constraint,
+ (p4.verticalCenter == p2.height / al.cm(2.0)) as al.Constraint,
+ ];
+ }
+
+ bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate) => true;
+}
+
void main() {
RenderDecoratedBox c1 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFF0000))
@@ -25,40 +64,22 @@
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFFFFFF))
);
- RenderAutoLayout root = new RenderAutoLayout(children: <RenderBox>[c1, c2, c3, c4]);
+ _MyAutoLayoutDelegate delegate = new _MyAutoLayoutDelegate();
- AutoLayoutParentData p1 = c1.parentData;
- AutoLayoutParentData p2 = c2.parentData;
- AutoLayoutParentData p3 = c3.parentData;
- AutoLayoutParentData p4 = c4.parentData;
+ RenderAutoLayout root = new RenderAutoLayout(
+ delegate: delegate,
+ children: <RenderBox>[c1, c2, c3, c4]
+ );
- root.addConstraints(<al.Constraint>[
- // Sum of widths of each box must be equal to that of the container
- (p1.width + p2.width + p3.width == root.width) as al.Constraint,
+ AutoLayoutParentData parentData1 = c1.parentData;
+ AutoLayoutParentData parentData2 = c2.parentData;
+ AutoLayoutParentData parentData3 = c3.parentData;
+ AutoLayoutParentData parentData4 = c4.parentData;
- // The boxes must be stacked left to right
- p1.rightEdge <= p2.leftEdge,
- p2.rightEdge <= p3.leftEdge,
-
- // The widths of the first and the third boxes should be equal
- (p1.width == p3.width) as al.Constraint,
-
- // The width of the second box should be twice as much as that of the first
- // and third
- (p2.width * al.cm(2.0) == p1.width) as al.Constraint,
-
- // The height of the three boxes should be equal to that of the container
- (p1.height == p2.height) as al.Constraint,
- (p2.height == p3.height) as al.Constraint,
- (p3.height == root.height) as al.Constraint,
-
- // The fourth box should be half as wide as the second and must be attached
- // to the right edge of the same (by its center)
- (p4.width == p2.width / al.cm(2.0)) as al.Constraint,
- (p4.height == al.cm(50.0)) as al.Constraint,
- (p4.horizontalCenter == p2.rightEdge) as al.Constraint,
- (p4.verticalCenter == p2.height / al.cm(2.0)) as al.Constraint,
- ]);
+ parentData1.params = delegate.p1;
+ parentData2.params = delegate.p2;
+ parentData3.params = delegate.p3;
+ parentData4.params = delegate.p4;
new RenderingFlutterBinding(root: root);
}
diff --git a/examples/layers/widgets/autolayout.dart b/examples/layers/widgets/autolayout.dart
new file mode 100644
index 0000000..d7e81f1
--- /dev/null
+++ b/examples/layers/widgets/autolayout.dart
@@ -0,0 +1,87 @@
+// 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.
+
+// This example shows how to use the Cassowary autolayout system with widgets.
+
+import 'package:cassowary/cassowary.dart' as al;
+import 'package:flutter/widgets.dart';
+
+class _MyAutoLayoutDelegate extends AutoLayoutDelegate {
+ AutoLayoutParams p1 = new AutoLayoutParams();
+ AutoLayoutParams p2 = new AutoLayoutParams();
+ AutoLayoutParams p3 = new AutoLayoutParams();
+ AutoLayoutParams p4 = new AutoLayoutParams();
+
+ List<al.Constraint> getConstraints(AutoLayoutParams parentParams) {
+ return <al.Constraint>[
+ // Sum of widths of each box must be equal to that of the container
+ (p1.width + p2.width + p3.width == parentParams.width) as al.Constraint,
+
+ // The boxes must be stacked left to right
+ p1.rightEdge <= p2.leftEdge,
+ p2.rightEdge <= p3.leftEdge,
+
+ // The widths of the first and the third boxes should be equal
+ (p1.width == p3.width) as al.Constraint,
+
+ // The width of the second box should be twice as much as that of the first
+ // and third
+ (p2.width * al.cm(2.0) == p1.width) as al.Constraint,
+
+ // The height of the three boxes should be equal to that of the container
+ (p1.height == p2.height) as al.Constraint,
+ (p2.height == p3.height) as al.Constraint,
+ (p3.height == parentParams.height) as al.Constraint,
+
+ // The fourth box should be half as wide as the second and must be attached
+ // to the right edge of the same (by its center)
+ (p4.width == p2.width / al.cm(2.0)) as al.Constraint,
+ (p4.height == al.cm(50.0)) as al.Constraint,
+ (p4.horizontalCenter == p2.rightEdge) as al.Constraint,
+ (p4.verticalCenter == p2.height / al.cm(2.0)) as al.Constraint,
+ ];
+ }
+
+ bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate) => true;
+}
+
+class ColoredBox extends StatelessComponent {
+ ColoredBox({ Key key, this.params, this.color }) : super(key: key);
+
+ final AutoLayoutParams params;
+ final Color color;
+
+ Widget build(BuildContext context) {
+ return new AutoLayoutChild(
+ params: params,
+ child: new DecoratedBox(
+ decoration: new BoxDecoration(backgroundColor: color)
+ )
+ );
+ }
+}
+
+class ColoredBoxes extends StatefulComponent {
+ _ColoredBoxesState createState() => new _ColoredBoxesState();
+}
+
+class _ColoredBoxesState extends State<ColoredBoxes> {
+ final _MyAutoLayoutDelegate delegate = new _MyAutoLayoutDelegate();
+
+ Widget build(BuildContext context) {
+ return new AutoLayout(
+ delegate: delegate,
+ children: <Widget>[
+ new ColoredBox(params: delegate.p1, color: const Color(0xFFFF0000)),
+ new ColoredBox(params: delegate.p2, color: const Color(0xFF00FF00)),
+ new ColoredBox(params: delegate.p3, color: const Color(0xFF0000FF)),
+ new ColoredBox(params: delegate.p4, color: const Color(0xFFFFFFFF)),
+ ]
+ );
+ }
+}
+
+void main() {
+ runApp(new ColoredBoxes());
+}
diff --git a/packages/flutter/lib/src/rendering/auto_layout.dart b/packages/flutter/lib/src/rendering/auto_layout.dart
index 20ec85f..156621d 100644
--- a/packages/flutter/lib/src/rendering/auto_layout.dart
+++ b/packages/flutter/lib/src/rendering/auto_layout.dart
@@ -9,24 +9,23 @@
/// Hosts the edge parameters and vends useful methods to construct expressions
/// for constraints. Also sets up and manages implicit constraints and edit
-/// variables. Used as a mixin by layout containers and parent data instances
-/// of render boxes taking part in auto layout.
-abstract class _AutoLayoutParamMixin {
-
- void _setupLayoutParameters(dynamic context) {
- _leftEdge = new al.Param.withContext(context);
- _rightEdge = new al.Param.withContext(context);
- _topEdge = new al.Param.withContext(context);
- _bottomEdge = new al.Param.withContext(context);
+/// variables.
+class AutoLayoutParams {
+ AutoLayoutParams() {
+ _leftEdge = new al.Param.withContext(this);
+ _rightEdge = new al.Param.withContext(this);
+ _topEdge = new al.Param.withContext(this);
+ _bottomEdge = new al.Param.withContext(this);
}
+ /// The render box with which these parameters are associated.
+ RenderBox _renderBox;
+
al.Param _leftEdge;
al.Param _rightEdge;
al.Param _topEdge;
al.Param _bottomEdge;
- List<al.Constraint> _implicitConstraints;
-
al.Param get leftEdge => _leftEdge;
al.Param get rightEdge => _rightEdge;
al.Param get topEdge => _topEdge;
@@ -38,153 +37,154 @@
al.Expression get horizontalCenter => (_leftEdge + _rightEdge) / al.cm(2.0);
al.Expression get verticalCenter => (_topEdge + _bottomEdge) / al.cm(2.0);
- void _setupEditVariablesInSolver(al.Solver solver, double priority) {
- solver.addEditVariables(<al.Variable>[
- _leftEdge.variable,
- _rightEdge.variable,
- _topEdge.variable,
- _bottomEdge.variable
- ], priority);
+ List<al.Constraint> _implicitConstraints;
+
+ void _addImplicitConstraints() {
+ assert(_renderBox != null);
+ if (_renderBox.parent == null)
+ return;
+ assert(_renderBox.parent is RenderAutoLayout);
+ final RenderAutoLayout parent = _renderBox.parent;
+ final AutoLayoutParentData parentData = _renderBox.parentData;
+ final List<al.Constraint> implicit = parentData._constructImplicitConstraints();
+ if (implicit == null || implicit.isEmpty)
+ return;
+ final al.Result result = parent._solver.addConstraints(implicit);
+ assert(result == al.Result.success);
+ parent.markNeedsLayout();
+ _implicitConstraints = implicit;
}
- void _applyEditsAtSize(al.Solver solver, Size size) {
- solver.suggestValueForVariable(_leftEdge.variable, 0.0);
- solver.suggestValueForVariable(_topEdge.variable, 0.0);
- solver.suggestValueForVariable(_bottomEdge.variable, size.height);
- solver.suggestValueForVariable(_rightEdge.variable, size.width);
+ void _removeImplicitConstraints() {
+ assert(_renderBox != null);
+ if (_renderBox.parent == null)
+ return;
+ if (_implicitConstraints == null || _implicitConstraints.isEmpty)
+ return;
+ assert(_renderBox.parent is RenderAutoLayout);
+ final RenderAutoLayout parent = _renderBox.parent;
+ final al.Result result = parent._solver.removeConstraints(_implicitConstraints);
+ assert(result == al.Result.success);
+ parent.markNeedsLayout();
+ _implicitConstraints = null;
+ }
+}
+
+class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
+ AutoLayoutParentData(this._renderBox);
+
+ final RenderBox _renderBox;
+
+ AutoLayoutParams get params => _params;
+ AutoLayoutParams _params;
+ void set params(AutoLayoutParams value) {
+ if (_params == value)
+ return;
+ if (_params != null) {
+ _params._removeImplicitConstraints();
+ _params._renderBox = null;
+ }
+ _params = value;
+ if (_params != null) {
+ assert(_params._renderBox == null);
+ _params._renderBox = _renderBox;
+ _params._addImplicitConstraints();
+ }
}
- /// Applies the parameter updates.
- ///
- /// This method is called when the solver has updated at least one of the
- /// layout parameters of this object. The object is now responsible for
- /// applying this update to its other properties (if necessary).
- void _applyAutolayoutParameterUpdates();
+ BoxConstraints get _constraints {
+ return new BoxConstraints.tightFor(
+ width: _params._rightEdge.value - _params._leftEdge.value,
+ height: _params._bottomEdge.value - _params._topEdge.value
+ );
+ }
/// Returns the set of implicit constraints that need to be applied to all
/// instances of this class when they are moved into a render object with an
/// active solver. If no implicit constraints needs to be applied, the object
/// may return null.
- List<al.Constraint> _constructImplicitConstraints();
-
- void _setupImplicitConstraints(al.Solver solver) {
- List<al.Constraint> implicit = _constructImplicitConstraints();
-
- if (implicit == null || implicit.length == 0) {
- return;
- }
-
- al.Result result = solver.addConstraints(implicit);
- assert(result == al.Result.success);
-
- _implicitConstraints = implicit;
- }
-
- void _removeImplicitConstraints(al.Solver solver) {
- if (_implicitConstraints == null || _implicitConstraints.length == 0) {
- return;
- }
-
- al.Result result = solver.removeConstraints(_implicitConstraints);
- assert(result == al.Result.success);
-
- _implicitConstraints = null;
+ List<al.Constraint> _constructImplicitConstraints() {
+ return <al.Constraint>[
+ _params._leftEdge >= al.cm(0.0), // The left edge must be positive.
+ _params._rightEdge >= _params._leftEdge, // Width must be positive.
+ ];
}
}
-class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> with _AutoLayoutParamMixin {
+abstract class AutoLayoutDelegate {
+ const AutoLayoutDelegate();
- AutoLayoutParentData(this._renderBox) {
- _setupLayoutParameters(this);
- }
-
- final RenderBox _renderBox;
-
- void _applyAutolayoutParameterUpdates() {
- // This is called by the parent's layout function
- // to lay our box out.
- assert(_renderBox.parentData == this);
- assert(() {
- final RenderAutoLayout parent = _renderBox.parent;
- assert(parent.debugDoingThisLayout);
- });
- BoxConstraints size = new BoxConstraints.tightFor(
- width: _rightEdge.value - _leftEdge.value,
- height: _bottomEdge.value - _topEdge.value
- );
- _renderBox.layout(size);
- offset = new Offset(_leftEdge.value, _topEdge.value);
- }
-
- List<al.Constraint> _constructImplicitConstraints() {
- return <al.Constraint>[
- _leftEdge >= al.cm(0.0), // The left edge must be positive.
- _rightEdge >= _leftEdge, // Width must be positive.
- ];
- }
-
+ List<al.Constraint> getConstraints(AutoLayoutParams parentParams);
+ bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate);
}
class RenderAutoLayout extends RenderBox
with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>,
- RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData>,
- _AutoLayoutParamMixin {
+ RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData> {
- RenderAutoLayout({ List<RenderBox> children }) {
- _setupLayoutParameters(this);
- _setupEditVariablesInSolver(_solver, al.Priority.required - 1);
+ RenderAutoLayout({
+ AutoLayoutDelegate delegate,
+ List<RenderBox> children
+ }) : _delegate = delegate, _needToUpdateConstraints = (delegate != null) {
+ _solver.addEditVariables(<al.Variable>[
+ _params._leftEdge.variable,
+ _params._rightEdge.variable,
+ _params._topEdge.variable,
+ _params._bottomEdge.variable
+ ], al.Priority.required - 1);
+
addAll(children);
}
+ AutoLayoutDelegate get delegate => _delegate;
+ AutoLayoutDelegate _delegate;
+ void set delegate(AutoLayoutDelegate newDelegate) {
+ if (_delegate == newDelegate)
+ return;
+ AutoLayoutDelegate oldDelegate = _delegate;
+ _delegate = newDelegate;
+ if (newDelegate == null) {
+ assert(oldDelegate != null);
+ _needToUpdateConstraints = true;
+ markNeedsLayout();
+ } else if (oldDelegate == null ||
+ newDelegate.runtimeType != oldDelegate.runtimeType ||
+ newDelegate.shouldUpdateConstraints(oldDelegate)) {
+ _needToUpdateConstraints = true;
+ markNeedsLayout();
+ }
+ }
+
+ bool _needToUpdateConstraints;
+
+ final AutoLayoutParams _params = new AutoLayoutParams();
+
final al.Solver _solver = new al.Solver();
- List<al.Constraint> _explicitConstraints = new List<al.Constraint>();
+ final List<al.Constraint> _explicitConstraints = new List<al.Constraint>();
- /// Adds all the given constraints to the solver. Either all constraints are
- /// added or none.
- al.Result addConstraints(List<al.Constraint> constraints) {
- al.Result result = _solver.addConstraints(constraints);
- if (result == al.Result.success) {
- markNeedsLayout();
+ void _addExplicitConstraints(List<al.Constraint> constraints) {
+ if (constraints == null || constraints.isEmpty)
+ return;
+ if (_solver.addConstraints(constraints) == al.Result.success)
_explicitConstraints.addAll(constraints);
- }
- return result;
}
- /// Adds the given constraint to the solver.
- al.Result addConstraint(al.Constraint constraint) {
- al.Result result = _solver.addConstraint(constraint);
-
- if (result == al.Result.success) {
- markNeedsLayout();
- _explicitConstraints.add(constraint);
- }
-
- return result;
- }
-
- /// Removes all explicitly added constraints.
- al.Result clearAllConstraints() {
- al.Result result = _solver.removeConstraints(_explicitConstraints);
-
- if (result == al.Result.success) {
- markNeedsLayout();
- _explicitConstraints = new List<al.Constraint>();
- }
-
- return result;
+ void _clearExplicitConstraints() {
+ if (_solver.removeConstraints(_explicitConstraints) == al.Result.success)
+ _explicitConstraints.clear();
}
void adoptChild(RenderObject child) {
// Make sure to call super first to setup the parent data
super.adoptChild(child);
final AutoLayoutParentData childParentData = child.parentData;
- childParentData._setupImplicitConstraints(_solver);
+ childParentData._params?._addImplicitConstraints();
assert(child.parentData == childParentData);
}
void dropChild(RenderObject child) {
final AutoLayoutParentData childParentData = child.parentData;
- childParentData._removeImplicitConstraints(_solver);
+ childParentData._params?._removeImplicitConstraints();
assert(child.parentData == childParentData);
super.dropChild(child);
}
@@ -201,23 +201,41 @@
}
void performLayout() {
- // Step 1: Update dimensions of self
- _applyEditsAtSize(_solver, size);
+ // Step 1: Update constraints if needed.
+ if (_needToUpdateConstraints) {
+ _clearExplicitConstraints();
+ if (_delegate != null)
+ _addExplicitConstraints(_delegate.getConstraints(_params));
+ _needToUpdateConstraints = false;
+ }
- // Step 2: Resolve solver updates and flush parameters
+ // Step 2: Update dimensions of this render object.
+ _solver
+ ..suggestValueForVariable(_params._leftEdge.variable, 0.0)
+ ..suggestValueForVariable(_params._topEdge.variable, 0.0)
+ ..suggestValueForVariable(_params._bottomEdge.variable, size.height)
+ ..suggestValueForVariable(_params._rightEdge.variable, size.width);
+
+ // Step 3: Resolve solver updates and flush parameters
// We don't iterate over the children, instead, we ask the solver to tell
// us the updated parameters. Attached to the parameters (via the context)
- // are the _AutoLayoutParamMixin instances.
- for (_AutoLayoutParamMixin update in _solver.flushUpdates()) {
- update._applyAutolayoutParameterUpdates();
+ // are the AutoLayoutParams instances.
+ for (AutoLayoutParams update in _solver.flushUpdates()) {
+ RenderBox child = update._renderBox;
+ if (child != null)
+ _layoutChild(child);
}
}
- void _applyAutolayoutParameterUpdates() {
- // Nothing to do since the size update has already been presented to the
- // solver as an edit variable modification. The invokation of this method
- // only indicates that the value has been flushed to the variable.
+ void _layoutChild(RenderBox child) {
+ assert(debugDoingThisLayout);
+ assert(child.parent == this);
+ final AutoLayoutParentData childParentData = child.parentData;
+ child.layout(childParentData._constraints);
+ childParentData.offset = new Offset(childParentData._params._leftEdge.value,
+ childParentData._params._topEdge.value);
+ assert(child.parentData == childParentData);
}
bool hitTestChildren(HitTestResult result, { Point position }) {
@@ -227,11 +245,4 @@
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
-
- List<al.Constraint> _constructImplicitConstraints() {
- // Only edits variables are present on layout containers. If, in the future,
- // implicit constraints (for say margins, padding, etc.) need to be added,
- // they must be returned from here.
- return null;
- }
}
diff --git a/packages/flutter/lib/src/widgets/auto_layout.dart b/packages/flutter/lib/src/widgets/auto_layout.dart
new file mode 100644
index 0000000..b915d10
--- /dev/null
+++ b/packages/flutter/lib/src/widgets/auto_layout.dart
@@ -0,0 +1,42 @@
+// 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/rendering.dart';
+
+import 'framework.dart';
+
+export 'package:flutter/rendering.dart' show
+ AutoLayoutParams,
+ AutoLayoutDelegate;
+
+class AutoLayout extends MultiChildRenderObjectWidget {
+ AutoLayout({
+ Key key,
+ this.delegate,
+ List<Widget> children: const <Widget>[]
+ }) : super(key: key, children: children);
+
+ final AutoLayoutDelegate delegate;
+
+ RenderAutoLayout createRenderObject() => new RenderAutoLayout(delegate: delegate);
+
+ void updateRenderObject(RenderAutoLayout renderObject, AutoLayout oldWidget) {
+ renderObject.delegate = delegate;
+ }
+}
+
+class AutoLayoutChild extends ParentDataWidget<AutoLayout> {
+ AutoLayoutChild({ Key key, this.params, Widget child })
+ : super(key: key, child: child);
+
+ final AutoLayoutParams params;
+
+ void applyParentData(RenderObject renderObject) {
+ assert(renderObject.parentData is AutoLayoutParentData);
+ final AutoLayoutParentData parentData = renderObject.parentData;
+ // AutoLayoutParentData filters out redundant writes and marks needs layout
+ // as appropriate.
+ parentData.params = params;
+ }
+}
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index 0f565b5..e764483 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -6,6 +6,7 @@
library widgets;
export 'src/widgets/asset_vendor.dart';
+export 'src/widgets/auto_layout.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/checked_mode_banner.dart';