Commit 262dd7a6 authored by Adam Barth's avatar Adam Barth

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.
parent cdc40554
// 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 // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
...@@ -8,6 +8,45 @@ ...@@ -8,6 +8,45 @@
import 'package:cassowary/cassowary.dart' as al; import 'package:cassowary/cassowary.dart' as al;
import 'package:flutter/rendering.dart'; 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() { void main() {
RenderDecoratedBox c1 = new RenderDecoratedBox( RenderDecoratedBox c1 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFF0000)) decoration: new BoxDecoration(backgroundColor: const Color(0xFFFF0000))
...@@ -25,40 +64,22 @@ void main() { ...@@ -25,40 +64,22 @@ void main() {
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFFFFFF)) 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;
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,
// The boxes must be stacked left to right RenderAutoLayout root = new RenderAutoLayout(
p1.rightEdge <= p2.leftEdge, delegate: delegate,
p2.rightEdge <= p3.leftEdge, children: <RenderBox>[c1, c2, c3, c4]
);
// 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 AutoLayoutParentData parentData1 = c1.parentData;
(p1.height == p2.height) as al.Constraint, AutoLayoutParentData parentData2 = c2.parentData;
(p2.height == p3.height) as al.Constraint, AutoLayoutParentData parentData3 = c3.parentData;
(p3.height == root.height) as al.Constraint, AutoLayoutParentData parentData4 = c4.parentData;
// The fourth box should be half as wide as the second and must be attached parentData1.params = delegate.p1;
// to the right edge of the same (by its center) parentData2.params = delegate.p2;
(p4.width == p2.width / al.cm(2.0)) as al.Constraint, parentData3.params = delegate.p3;
(p4.height == al.cm(50.0)) as al.Constraint, parentData4.params = delegate.p4;
(p4.horizontalCenter == p2.rightEdge) as al.Constraint,
(p4.verticalCenter == p2.height / al.cm(2.0)) as al.Constraint,
]);
new RenderingFlutterBinding(root: root); new RenderingFlutterBinding(root: root);
} }
// 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());
}
...@@ -9,24 +9,23 @@ import 'object.dart'; ...@@ -9,24 +9,23 @@ import 'object.dart';
/// Hosts the edge parameters and vends useful methods to construct expressions /// Hosts the edge parameters and vends useful methods to construct expressions
/// for constraints. Also sets up and manages implicit constraints and edit /// for constraints. Also sets up and manages implicit constraints and edit
/// variables. Used as a mixin by layout containers and parent data instances /// variables.
/// of render boxes taking part in auto layout. class AutoLayoutParams {
abstract class _AutoLayoutParamMixin { AutoLayoutParams() {
_leftEdge = new al.Param.withContext(this);
void _setupLayoutParameters(dynamic context) { _rightEdge = new al.Param.withContext(this);
_leftEdge = new al.Param.withContext(context); _topEdge = new al.Param.withContext(this);
_rightEdge = new al.Param.withContext(context); _bottomEdge = new al.Param.withContext(this);
_topEdge = new al.Param.withContext(context);
_bottomEdge = new al.Param.withContext(context);
} }
/// The render box with which these parameters are associated.
RenderBox _renderBox;
al.Param _leftEdge; al.Param _leftEdge;
al.Param _rightEdge; al.Param _rightEdge;
al.Param _topEdge; al.Param _topEdge;
al.Param _bottomEdge; al.Param _bottomEdge;
List<al.Constraint> _implicitConstraints;
al.Param get leftEdge => _leftEdge; al.Param get leftEdge => _leftEdge;
al.Param get rightEdge => _rightEdge; al.Param get rightEdge => _rightEdge;
al.Param get topEdge => _topEdge; al.Param get topEdge => _topEdge;
...@@ -38,153 +37,154 @@ abstract class _AutoLayoutParamMixin { ...@@ -38,153 +37,154 @@ abstract class _AutoLayoutParamMixin {
al.Expression get horizontalCenter => (_leftEdge + _rightEdge) / al.cm(2.0); al.Expression get horizontalCenter => (_leftEdge + _rightEdge) / al.cm(2.0);
al.Expression get verticalCenter => (_topEdge + _bottomEdge) / al.cm(2.0); al.Expression get verticalCenter => (_topEdge + _bottomEdge) / al.cm(2.0);
void _setupEditVariablesInSolver(al.Solver solver, double priority) { List<al.Constraint> _implicitConstraints;
solver.addEditVariables(<al.Variable>[
_leftEdge.variable,
_rightEdge.variable,
_topEdge.variable,
_bottomEdge.variable
], priority);
}
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);
}
/// 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();
/// 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) { void _addImplicitConstraints() {
assert(_renderBox != null);
if (_renderBox.parent == null)
return; return;
} assert(_renderBox.parent is RenderAutoLayout);
final RenderAutoLayout parent = _renderBox.parent;
al.Result result = solver.addConstraints(implicit); 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); assert(result == al.Result.success);
parent.markNeedsLayout();
_implicitConstraints = implicit; _implicitConstraints = implicit;
} }
void _removeImplicitConstraints(al.Solver solver) { void _removeImplicitConstraints() {
if (_implicitConstraints == null || _implicitConstraints.length == 0) { assert(_renderBox != null);
if (_renderBox.parent == null)
return; return;
} if (_implicitConstraints == null || _implicitConstraints.isEmpty)
return;
al.Result result = solver.removeConstraints(_implicitConstraints); assert(_renderBox.parent is RenderAutoLayout);
final RenderAutoLayout parent = _renderBox.parent;
final al.Result result = parent._solver.removeConstraints(_implicitConstraints);
assert(result == al.Result.success); assert(result == al.Result.success);
parent.markNeedsLayout();
_implicitConstraints = null; _implicitConstraints = null;
} }
} }
class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> with _AutoLayoutParamMixin { class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
AutoLayoutParentData(this._renderBox);
AutoLayoutParentData(this._renderBox) {
_setupLayoutParameters(this);
}
final RenderBox _renderBox; final RenderBox _renderBox;
void _applyAutolayoutParameterUpdates() { AutoLayoutParams get params => _params;
// This is called by the parent's layout function AutoLayoutParams _params;
// to lay our box out. void set params(AutoLayoutParams value) {
assert(_renderBox.parentData == this); if (_params == value)
assert(() { return;
final RenderAutoLayout parent = _renderBox.parent; if (_params != null) {
assert(parent.debugDoingThisLayout); _params._removeImplicitConstraints();
}); _params._renderBox = null;
BoxConstraints size = new BoxConstraints.tightFor( }
width: _rightEdge.value - _leftEdge.value, _params = value;
height: _bottomEdge.value - _topEdge.value if (_params != null) {
assert(_params._renderBox == null);
_params._renderBox = _renderBox;
_params._addImplicitConstraints();
}
}
BoxConstraints get _constraints {
return new BoxConstraints.tightFor(
width: _params._rightEdge.value - _params._leftEdge.value,
height: _params._bottomEdge.value - _params._topEdge.value
); );
_renderBox.layout(size);
offset = new Offset(_leftEdge.value, _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() { List<al.Constraint> _constructImplicitConstraints() {
return <al.Constraint>[ return <al.Constraint>[
_leftEdge >= al.cm(0.0), // The left edge must be positive. _params._leftEdge >= al.cm(0.0), // The left edge must be positive.
_rightEdge >= _leftEdge, // Width must be positive. _params._rightEdge >= _params._leftEdge, // Width must be positive.
]; ];
} }
}
abstract class AutoLayoutDelegate {
const AutoLayoutDelegate();
List<al.Constraint> getConstraints(AutoLayoutParams parentParams);
bool shouldUpdateConstraints(AutoLayoutDelegate oldDelegate);
} }
class RenderAutoLayout extends RenderBox class RenderAutoLayout extends RenderBox
with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>, with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData>, RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData> {
_AutoLayoutParamMixin {
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);
RenderAutoLayout({ List<RenderBox> children }) {
_setupLayoutParameters(this);
_setupEditVariablesInSolver(_solver, al.Priority.required - 1);
addAll(children); addAll(children);
} }
final al.Solver _solver = new al.Solver(); AutoLayoutDelegate get delegate => _delegate;
List<al.Constraint> _explicitConstraints = new List<al.Constraint>(); AutoLayoutDelegate _delegate;
void set delegate(AutoLayoutDelegate newDelegate) {
/// Adds all the given constraints to the solver. Either all constraints are if (_delegate == newDelegate)
/// added or none. return;
al.Result addConstraints(List<al.Constraint> constraints) { AutoLayoutDelegate oldDelegate = _delegate;
al.Result result = _solver.addConstraints(constraints); _delegate = newDelegate;
if (result == al.Result.success) { if (newDelegate == null) {
assert(oldDelegate != null);
_needToUpdateConstraints = true;
markNeedsLayout();
} else if (oldDelegate == null ||
newDelegate.runtimeType != oldDelegate.runtimeType ||
newDelegate.shouldUpdateConstraints(oldDelegate)) {
_needToUpdateConstraints = true;
markNeedsLayout(); markNeedsLayout();
_explicitConstraints.addAll(constraints);
} }
return result;
} }
/// Adds the given constraint to the solver. bool _needToUpdateConstraints;
al.Result addConstraint(al.Constraint constraint) {
al.Result result = _solver.addConstraint(constraint);
if (result == al.Result.success) { final AutoLayoutParams _params = new AutoLayoutParams();
markNeedsLayout();
_explicitConstraints.add(constraint);
}
return result; final al.Solver _solver = new al.Solver();
} final List<al.Constraint> _explicitConstraints = new List<al.Constraint>();
/// Removes all explicitly added constraints.
al.Result clearAllConstraints() {
al.Result result = _solver.removeConstraints(_explicitConstraints);
if (result == al.Result.success) { void _addExplicitConstraints(List<al.Constraint> constraints) {
markNeedsLayout(); if (constraints == null || constraints.isEmpty)
_explicitConstraints = new List<al.Constraint>(); return;
} if (_solver.addConstraints(constraints) == al.Result.success)
_explicitConstraints.addAll(constraints);
}
return result; void _clearExplicitConstraints() {
if (_solver.removeConstraints(_explicitConstraints) == al.Result.success)
_explicitConstraints.clear();
} }
void adoptChild(RenderObject child) { void adoptChild(RenderObject child) {
// Make sure to call super first to setup the parent data // Make sure to call super first to setup the parent data
super.adoptChild(child); super.adoptChild(child);
final AutoLayoutParentData childParentData = child.parentData; final AutoLayoutParentData childParentData = child.parentData;
childParentData._setupImplicitConstraints(_solver); childParentData._params?._addImplicitConstraints();
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
} }
void dropChild(RenderObject child) { void dropChild(RenderObject child) {
final AutoLayoutParentData childParentData = child.parentData; final AutoLayoutParentData childParentData = child.parentData;
childParentData._removeImplicitConstraints(_solver); childParentData._params?._removeImplicitConstraints();
assert(child.parentData == childParentData); assert(child.parentData == childParentData);
super.dropChild(child); super.dropChild(child);
} }
...@@ -201,23 +201,41 @@ class RenderAutoLayout extends RenderBox ...@@ -201,23 +201,41 @@ class RenderAutoLayout extends RenderBox
} }
void performLayout() { void performLayout() {
// Step 1: Update dimensions of self // Step 1: Update constraints if needed.
_applyEditsAtSize(_solver, size); if (_needToUpdateConstraints) {
_clearExplicitConstraints();
if (_delegate != null)
_addExplicitConstraints(_delegate.getConstraints(_params));
_needToUpdateConstraints = false;
}
// 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 2: Resolve solver updates and flush parameters // Step 3: Resolve solver updates and flush parameters
// We don't iterate over the children, instead, we ask the solver to tell // 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) // us the updated parameters. Attached to the parameters (via the context)
// are the _AutoLayoutParamMixin instances. // are the AutoLayoutParams instances.
for (_AutoLayoutParamMixin update in _solver.flushUpdates()) { for (AutoLayoutParams update in _solver.flushUpdates()) {
update._applyAutolayoutParameterUpdates(); RenderBox child = update._renderBox;
if (child != null)
_layoutChild(child);
} }
} }
void _applyAutolayoutParameterUpdates() { void _layoutChild(RenderBox child) {
// Nothing to do since the size update has already been presented to the assert(debugDoingThisLayout);
// solver as an edit variable modification. The invokation of this method assert(child.parent == this);
// only indicates that the value has been flushed to the variable. 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 }) { bool hitTestChildren(HitTestResult result, { Point position }) {
...@@ -227,11 +245,4 @@ class RenderAutoLayout extends RenderBox ...@@ -227,11 +245,4 @@ class RenderAutoLayout extends RenderBox
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
defaultPaint(context, 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;
}
} }
// 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;
}
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
library widgets; library widgets;
export 'src/widgets/asset_vendor.dart'; export 'src/widgets/asset_vendor.dart';
export 'src/widgets/auto_layout.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
export 'src/widgets/checked_mode_banner.dart'; export 'src/widgets/checked_mode_banner.dart';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment