From 262dd7a63b69dec2f754910e7f25d353a6d63e63 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Mon, 15 Feb 2016 02:18:16 -0800 Subject: [PATCH] Add support for autolayout to widgets This patch teaches the widget framework how to use Cassowary-based autolayout. To integrate autolayout with widgets, I had to refactor how RenderAutoLayout worked a bit. Now RenderAutoLayout follows the same delegate pattern we use for custom paint and custom layout. --- examples/layers/rendering/autolayout.dart | 85 ++++-- examples/layers/widgets/autolayout.dart | 87 ++++++ .../lib/src/rendering/auto_layout.dart | 287 +++++++++--------- .../flutter/lib/src/widgets/auto_layout.dart | 42 +++ packages/flutter/lib/widgets.dart | 1 + 5 files changed, 332 insertions(+), 170 deletions(-) create mode 100644 examples/layers/widgets/autolayout.dart create mode 100644 packages/flutter/lib/src/widgets/auto_layout.dart diff --git a/examples/layers/rendering/autolayout.dart b/examples/layers/rendering/autolayout.dart index c2c30c540be..f89f4bf948e 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 getConstraints(AutoLayoutParams parentParams) { + return [ + // 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 @@ void main() { decoration: new BoxDecoration(backgroundColor: const Color(0xFFFFFFFF)) ); - RenderAutoLayout root = new RenderAutoLayout(children: [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: [c1, c2, c3, c4] + ); - root.addConstraints([ - // 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 00000000000..d7e81f158ef --- /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 getConstraints(AutoLayoutParams parentParams) { + return [ + // 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 { + final _MyAutoLayoutDelegate delegate = new _MyAutoLayoutDelegate(); + + Widget build(BuildContext context) { + return new AutoLayout( + delegate: delegate, + children: [ + 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 20ec85f606a..156621d6fb3 100644 --- a/packages/flutter/lib/src/rendering/auto_layout.dart +++ b/packages/flutter/lib/src/rendering/auto_layout.dart @@ -9,24 +9,23 @@ import 'object.dart'; /// 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 _implicitConstraints; - al.Param get leftEdge => _leftEdge; al.Param get rightEdge => _rightEdge; al.Param get topEdge => _topEdge; @@ -38,153 +37,154 @@ abstract class _AutoLayoutParamMixin { 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([ - _leftEdge.variable, - _rightEdge.variable, - _topEdge.variable, - _bottomEdge.variable - ], priority); + List _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 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 { + 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 _constructImplicitConstraints(); - - void _setupImplicitConstraints(al.Solver solver) { - List 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 _constructImplicitConstraints() { + return [ + _params._leftEdge >= al.cm(0.0), // The left edge must be positive. + _params._rightEdge >= _params._leftEdge, // Width must be positive. + ]; } } -class AutoLayoutParentData extends ContainerBoxParentDataMixin with _AutoLayoutParamMixin { - - 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 _constructImplicitConstraints() { - return [ - _leftEdge >= al.cm(0.0), // The left edge must be positive. - _rightEdge >= _leftEdge, // Width must be positive. - ]; - } +abstract class AutoLayoutDelegate { + const AutoLayoutDelegate(); + List getConstraints(AutoLayoutParams parentParams); + bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate); } class RenderAutoLayout extends RenderBox with ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin, - _AutoLayoutParamMixin { + RenderBoxContainerDefaultsMixin { + + RenderAutoLayout({ + AutoLayoutDelegate delegate, + List children + }) : _delegate = delegate, _needToUpdateConstraints = (delegate != null) { + _solver.addEditVariables([ + _params._leftEdge.variable, + _params._rightEdge.variable, + _params._topEdge.variable, + _params._bottomEdge.variable + ], al.Priority.required - 1); - RenderAutoLayout({ List children }) { - _setupLayoutParameters(this); - _setupEditVariablesInSolver(_solver, 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 _explicitConstraints = new List(); + final List _explicitConstraints = new List(); - /// Adds all the given constraints to the solver. Either all constraints are - /// added or none. - al.Result addConstraints(List constraints) { - al.Result result = _solver.addConstraints(constraints); - if (result == al.Result.success) { - markNeedsLayout(); + void _addExplicitConstraints(List 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(); - } - - 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 @@ class RenderAutoLayout extends RenderBox } 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 @@ class RenderAutoLayout extends RenderBox void paint(PaintingContext context, Offset offset) { defaultPaint(context, offset); } - - List _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 00000000000..b915d104939 --- /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 children: const [] + }) : 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 { + 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 0f565b5d290..e7644837a66 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';