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
// found in the LICENSE file.
......@@ -8,33 +8,16 @@
import 'package:cassowary/cassowary.dart' as al;
import 'package:flutter/rendering.dart';
void main() {
RenderDecoratedBox c1 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFF0000))
);
class _MyAutoLayoutDelegate extends AutoLayoutDelegate {
AutoLayoutParams p1 = new AutoLayoutParams();
AutoLayoutParams p2 = new AutoLayoutParams();
AutoLayoutParams p3 = new AutoLayoutParams();
AutoLayoutParams p4 = new AutoLayoutParams();
RenderDecoratedBox c2 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFF00FF00))
);
RenderDecoratedBox c3 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFF0000FF))
);
RenderDecoratedBox c4 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFFFFFF))
);
RenderAutoLayout root = new RenderAutoLayout(children: <RenderBox>[c1, c2, c3, c4]);
AutoLayoutParentData p1 = c1.parentData;
AutoLayoutParentData p2 = c2.parentData;
AutoLayoutParentData p3 = c3.parentData;
AutoLayoutParentData p4 = c4.parentData;
root.addConstraints(<al.Constraint>[
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 == root.width) as al.Constraint,
(p1.width + p2.width + p3.width == parentParams.width) as al.Constraint,
// The boxes must be stacked left to right
p1.rightEdge <= p2.leftEdge,
......@@ -50,7 +33,7 @@ void main() {
// 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,
(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)
......@@ -58,7 +41,45 @@ void main() {
(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))
);
RenderDecoratedBox c2 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFF00FF00))
);
RenderDecoratedBox c3 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFF0000FF))
);
RenderDecoratedBox c4 = new RenderDecoratedBox(
decoration: new BoxDecoration(backgroundColor: const Color(0xFFFFFFFF))
);
_MyAutoLayoutDelegate delegate = new _MyAutoLayoutDelegate();
RenderAutoLayout root = new RenderAutoLayout(
delegate: delegate,
children: <RenderBox>[c1, c2, c3, c4]
);
AutoLayoutParentData parentData1 = c1.parentData;
AutoLayoutParentData parentData2 = c2.parentData;
AutoLayoutParentData parentData3 = c3.parentData;
AutoLayoutParentData parentData4 = c4.parentData;
parentData1.params = delegate.p1;
parentData2.params = delegate.p2;
parentData3.params = delegate.p3;
parentData4.params = delegate.p4;
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';
/// 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 @@ 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(<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();
List<al.Constraint> _implicitConstraints;
if (implicit == null || implicit.length == 0) {
void _addImplicitConstraints() {
assert(_renderBox != null);
if (_renderBox.parent == null)
return;
}
al.Result result = solver.addConstraints(implicit);
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 _removeImplicitConstraints(al.Solver solver) {
if (_implicitConstraints == null || _implicitConstraints.length == 0) {
void _removeImplicitConstraints() {
assert(_renderBox != null);
if (_renderBox.parent == null)
return;
}
al.Result result = solver.removeConstraints(_implicitConstraints);
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> with _AutoLayoutParamMixin {
AutoLayoutParentData(this._renderBox) {
_setupLayoutParameters(this);
}
class AutoLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
AutoLayoutParentData(this._renderBox);
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
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();
}
}
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() {
return <al.Constraint>[
_leftEdge >= al.cm(0.0), // The left edge must be positive.
_rightEdge >= _leftEdge, // Width must be positive.
_params._leftEdge >= al.cm(0.0), // The left edge 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
with ContainerRenderObjectMixin<RenderBox, AutoLayoutParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData>,
_AutoLayoutParamMixin {
RenderBoxContainerDefaultsMixin<RenderBox, AutoLayoutParentData> {
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);
}
final al.Solver _solver = new al.Solver();
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) {
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();
_explicitConstraints.addAll(constraints);
}
return result;
}
/// Adds the given constraint to the solver.
al.Result addConstraint(al.Constraint constraint) {
al.Result result = _solver.addConstraint(constraint);
bool _needToUpdateConstraints;
if (result == al.Result.success) {
markNeedsLayout();
_explicitConstraints.add(constraint);
}
return result;
}
final AutoLayoutParams _params = new AutoLayoutParams();
/// Removes all explicitly added constraints.
al.Result clearAllConstraints() {
al.Result result = _solver.removeConstraints(_explicitConstraints);
final al.Solver _solver = new al.Solver();
final List<al.Constraint> _explicitConstraints = new List<al.Constraint>();
if (result == al.Result.success) {
markNeedsLayout();
_explicitConstraints = new List<al.Constraint>();
void _addExplicitConstraints(List<al.Constraint> constraints) {
if (constraints == null || constraints.isEmpty)
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) {
// 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: 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
// 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<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 @@
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';
......
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