Commit f53a5a52 authored by Adam Barth's avatar Adam Barth

Add Flow layout

A flow layout is optimized for reposition children with transformation
matrices. A flow layout can animate the position of its children very
efficiently.
parent 3ca80d9e
......@@ -30,6 +30,7 @@ export 'src/rendering/debug.dart';
export 'src/rendering/editable_line.dart';
export 'src/rendering/error.dart';
export 'src/rendering/flex.dart';
export 'src/rendering/flow.dart';
export 'src/rendering/grid.dart';
export 'src/rendering/image.dart';
export 'src/rendering/layer.dart';
......
......@@ -51,7 +51,7 @@ class SnackBarAction extends StatefulWidget {
/// The button label.
final String label;
/// The callback to be invoked when the button is pressed. Must be non-null.
/// The callback to be invoked when the button is pressed. Must not be null.
///
/// This callback will be invoked at most once each time this action is
/// displayed in a [SnackBar].
......
......@@ -7,7 +7,7 @@ import 'dart:math' as math;
import 'box.dart';
import 'object.dart';
/// Parent data for use with [RenderFlex]
/// Parent data for use with [RenderFlex].
class FlexParentData extends ContainerBoxParentDataMixin<RenderBox> {
/// The flex factor to use for this child
///
......
This diff is collapsed.
......@@ -806,6 +806,7 @@ abstract class CustomClipper<T> {
/// Returns a description of the clip given that the render object being
/// clipped is of the given size.
T getClip(Size size);
/// Returns an approximation of the clip returned by [getClip], as
/// an axis-aligned Rect. This is used by the semantics layer to
/// determine whether widgets should be excluded.
......@@ -815,6 +816,7 @@ abstract class CustomClipper<T> {
/// same size as the RenderObject (e.g. it's a rounded rectangle
/// with very small arcs in the corners), then this may be adequate.
Rect getApproximateClipRect(Size size) => Point.origin & size;
/// Returns `true` if the new instance will result in a different clip
/// than the oldClipper instance.
bool shouldRepaint(CustomClipper<T> oldClipper);
......@@ -995,7 +997,10 @@ enum DecorationPosition {
/// Paints a [Decoration] either before or after its child paints.
class RenderDecoratedBox extends RenderProxyBox {
/// Creates a decorated box.
///
/// Both the [decoration] and the [position] arguments are required. By
/// default the decoration paints behind the child.
RenderDecoratedBox({
Decoration decoration,
DecorationPosition position: DecorationPosition.background,
......@@ -1010,6 +1015,8 @@ class RenderDecoratedBox extends RenderProxyBox {
BoxPainter _painter;
/// What decoration to paint.
///
/// Commonly a [BoxDecoration].
Decoration get decoration => _decoration;
Decoration _decoration;
void set decoration (Decoration newDecoration) {
......@@ -1023,7 +1030,7 @@ class RenderDecoratedBox extends RenderProxyBox {
markNeedsPaint();
}
/// Where to paint the box decoration.
/// Whether to paint the box decoration behind or in front of the child.
DecorationPosition get position => _position;
DecorationPosition _position;
void set position (DecorationPosition newPosition) {
......
......@@ -221,7 +221,7 @@ class RenderPadding extends RenderShiftedBox {
abstract class RenderAligningShiftedBox extends RenderShiftedBox {
RenderAligningShiftedBox({
RenderBox child,
FractionalOffset alignment: const FractionalOffset(0.5, 0.5)
FractionalOffset alignment: FractionalOffset.center
}) : _alignment = alignment,
super(child) {
assert(alignment != null && alignment.dx != null && alignment.dy != null);
......@@ -277,7 +277,7 @@ abstract class RenderAligningShiftedBox extends RenderShiftedBox {
///
/// For example, to align a box at the bottom right, you would pass this box a
/// tight constraint that is bigger than the child's natural size,
/// with an alignment of [const FractionalOffset(1.0, 1.0)].
/// with an alignment of [FractionalOffset.bottomRight].
///
/// By default, sizes to be as big as possible in both axes. If either axis is
/// unconstrained, then in that direction it will be sized to fit the child's
......@@ -288,7 +288,7 @@ class RenderPositionedBox extends RenderAligningShiftedBox {
RenderBox child,
double widthFactor,
double heightFactor,
FractionalOffset alignment: const FractionalOffset(0.5, 0.5)
FractionalOffset alignment: FractionalOffset.center
}) : _widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment) {
......@@ -432,7 +432,7 @@ class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
double maxWidth,
double minHeight,
double maxHeight,
FractionalOffset alignment: const FractionalOffset(0.5, 0.5)
FractionalOffset alignment: FractionalOffset.center
}) : _minWidth = minWidth,
_maxWidth = maxWidth,
_minHeight = minHeight,
......@@ -548,7 +548,7 @@ class RenderSizedOverflowBox extends RenderAligningShiftedBox {
RenderSizedOverflowBox({
RenderBox child,
Size requestedSize,
FractionalOffset alignment: const FractionalOffset(0.5, 0.5)
FractionalOffset alignment: FractionalOffset.center
}) : _requestedSize = requestedSize,
super(child: child, alignment: alignment) {
assert(requestedSize != null);
......@@ -620,7 +620,7 @@ class RenderFractionallySizedOverflowBox extends RenderAligningShiftedBox {
RenderBox child,
double widthFactor,
double heightFactor,
FractionalOffset alignment: const FractionalOffset(0.5, 0.5)
FractionalOffset alignment: FractionalOffset.center
}) : _widthFactor = widthFactor,
_heightFactor = heightFactor,
super(child: child, alignment: alignment) {
......
......@@ -133,7 +133,7 @@ class RelativeRect {
String toString() => "RelativeRect.fromLTRB(${left?.toStringAsFixed(1)}, ${top?.toStringAsFixed(1)}, ${right?.toStringAsFixed(1)}, ${bottom?.toStringAsFixed(1)})";
}
/// Parent data for use with [RenderStack]
/// Parent data for use with [RenderStack].
class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
/// The distance by which the child's top edge is inset from the top of the stack.
double top;
......@@ -437,6 +437,10 @@ abstract class RenderStackBase extends RenderBox
/// edge of the stack. If the child extends beyond the bounds of the
/// stack, the stack will clip the child's painting to the bounds of
/// the stack.
///
/// See also:
///
/// * [RenderFlow]
class RenderStack extends RenderStackBase {
RenderStack({
List<RenderBox> children,
......
......@@ -14,7 +14,7 @@ import 'package:flutter/foundation.dart';
class ImageInfo {
/// Creates an [ImageInfo] object for the given image and scale.
///
/// Both the image and the scale must be non-null.
/// Both the image and the scale must not be null.
ImageInfo({ this.image, this.scale: 1.0 }) {
assert(image != null);
assert(scale != null);
......
......@@ -21,6 +21,8 @@ export 'package:flutter/rendering.dart' show
CustomPainter,
FixedColumnCountGridDelegate,
FlexDirection,
FlowDelegate,
FlowPaintingContext,
FractionalOffsetTween,
GridDelegate,
GridDelegateWithInOrderChildPlacement,
......@@ -165,7 +167,7 @@ class DecoratedBox extends SingleChildRenderObjectWidget {
/// Commonly a [BoxDecoration].
final Decoration decoration;
/// Where to paint the box decoration.
/// Whether to paint the box decoration behind or in front of the child.
final DecorationPosition position;
@override
......@@ -431,16 +433,25 @@ class Padding extends SingleChildRenderObjectWidget {
///
/// For example, to align a box at the bottom right, you would pass this box a
/// tight constraint that is bigger than the child's natural size,
/// with an alignment of [const FractionalOffset(1.0, 1.0)].
/// with an alignment of [FractionalOffset.bottomRight].
///
/// By default, sizes to be as big as possible in both axes. If either axis is
/// unconstrained, then in that direction it will be sized to fit the child's
/// dimensions. Using widthFactor and heightFactor you can force this latter
/// behavior in all cases.
///
/// See also:
///
/// * [CustomSingleChildLayout]
/// * [Center] (which is the same as [Align] but with the [alignment] always
/// set to [FractionalOffset.center])
class Align extends SingleChildRenderObjectWidget {
/// Creates an alignment widget.
///
/// The alignment defaults to [FractionalOffset.center].
Align({
Key key,
this.alignment: const FractionalOffset(0.5, 0.5),
this.alignment: FractionalOffset.center,
this.widthFactor,
this.heightFactor,
Widget child
......@@ -494,6 +505,10 @@ class Align extends SingleChildRenderObjectWidget {
}
/// Centers its child within itself.
///
/// See also:
///
/// * [Align]
class Center extends Align {
Center({ Key key, double widthFactor, double heightFactor, Widget child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
......@@ -505,7 +520,17 @@ class Center extends Align {
/// decide where to position the child. The delegate can also determine the size
/// of the parent, but the size of the parent cannot depend on the size of the
/// child.
///
/// See also:
///
/// * [SingleChildLayoutDelegate]
/// * [Align] (which positions a single child according to a [FractionalOffset])
/// * [CustomMultiChildLayout] (which uses a delegate to position multiple
/// children)
class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// Creates a custom single child layout.
///
/// The delegate argument must not be null.
CustomSingleChildLayout({
Key key,
this.delegate,
......@@ -514,6 +539,7 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
assert(delegate != null);
}
/// The delegate that controls the layout of the child.
final SingleChildLayoutDelegate delegate;
@override
......@@ -527,6 +553,9 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// Metadata for identifying children in a [CustomMultiChildLayout].
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
/// Marks a child with a layout identifier.
///
/// Both the child and the id arguments must not be null.
LayoutId({
Key key,
Widget child,
......@@ -566,7 +595,17 @@ const List<Widget> _emptyWidgetList = const <Widget>[];
/// decide where to position each child. The delegate can also determine the
/// size of the parent, but the size of the parent cannot depend on the sizes of
/// the children.
///
/// See also:
///
/// * [MultiChildLayoutDelegate]
/// * [CustomSingleChildLayout]
/// * [Stack]
/// * [Flow]
class CustomMultiChildLayout extends MultiChildRenderObjectWidget {
/// Creates a custom multi-child layout.
///
/// The delegate argument must not be null.
CustomMultiChildLayout({
Key key,
List<Widget> children: _emptyWidgetList,
......@@ -667,7 +706,7 @@ class ConstrainedBox extends SingleChildRenderObjectWidget {
class FractionallySizedBox extends SingleChildRenderObjectWidget {
FractionallySizedBox({
Key key,
this.alignment: const FractionalOffset(0.5, 0.5),
this.alignment: FractionalOffset.center,
this.width,
this.height,
Widget child
......@@ -772,7 +811,7 @@ class LimitedBox extends SingleChildRenderObjectWidget {
class OverflowBox extends SingleChildRenderObjectWidget {
OverflowBox({
Key key,
this.alignment: const FractionalOffset(0.5, 0.5),
this.alignment: FractionalOffset.center,
this.minWidth,
this.maxWidth,
this.minHeight,
......@@ -844,7 +883,7 @@ class OverflowBox extends SingleChildRenderObjectWidget {
class SizedOverflowBox extends SingleChildRenderObjectWidget {
SizedOverflowBox({
Key key,
this.alignment: const FractionalOffset(0.5, 0.5),
this.alignment: FractionalOffset.center,
this.size,
Widget child
}) : super(key: key, child: child) {
......@@ -1265,11 +1304,18 @@ abstract class StackRenderObjectWidgetBase extends MultiChildRenderObjectWidget
/// For more details about the stack layout algorithm, see
/// [RenderStack]. To control the position of child widgets, see the
/// [Positioned] widget.
///
/// See also:
///
/// * [Flow]
/// * [Align] (which positions a single child according to a [FractionalOffset])
/// * [CustomSingleChildLayout]
/// * [CustomMultiChildLayout]
class Stack extends StackRenderObjectWidgetBase {
Stack({
Key key,
List<Widget> children: _emptyWidgetList,
this.alignment: const FractionalOffset(0.0, 0.0)
this.alignment: FractionalOffset.topLeft
}) : super(key: key, children: children);
/// How to align the non-positioned children in the stack.
......@@ -1289,7 +1335,7 @@ class IndexedStack extends StackRenderObjectWidgetBase {
IndexedStack({
Key key,
List<Widget> children: _emptyWidgetList,
this.alignment: const FractionalOffset(0.0, 0.0),
this.alignment: FractionalOffset.topLeft,
this.index: 0
}) : super(key: key, children: children) {
assert(index != null);
......@@ -1718,6 +1764,71 @@ class Flexible extends ParentDataWidget<Flex> {
}
}
/// Implements the flow layout algorithm.
///
/// Flow layouts are optimized for repositioning children using transformation
/// matrices.
///
/// The flow container is sized independently from the children by the
/// [FlowDelegate.getSize] function of the delegate. The children are then sized
/// independently given the constraints from the
/// [FlowDelegate.getConstraintsForChild] function.
///
/// Rather than positioning the children during layout, the children are
/// positioned using transformation matrices during the paint phase using the
/// matrices from the [FlowDelegate.paintChildren] function. The children can be
/// repositioned efficiently by simply repainting the flow.
///
/// The most efficient way to trigger a repaint of the flow is to supply a
/// repaint argument to the constructor of the [FlowDelegate]. The flow will
/// listen to this animation and repaint whenever the animation ticks, avoiding
/// both the build and layout phases of the pipeline.
///
/// See also:
///
/// * [FlowDelegate]
/// * [Stack]
/// * [CustomSingleChildLayout]
/// * [CustomMultiChildLayout]
class Flow extends MultiChildRenderObjectWidget {
/// Creates a flow layout.
///
/// Wraps each of the given children in a [RepaintBoundary] to avoid
/// repainting the children when the flow repaints.
Flow({
Key key,
List<Widget> children: _emptyWidgetList,
this.delegate
}) : super(key: key, children: RepaintBoundary.wrapAll(children)) {
assert(delegate != null);
}
/// Creates a flow layout.
///
/// Does not wrap the given children in repaint boundaries, unlike the default
/// constructor. Useful when the child is trivial to paint or already contains
/// a repaint boundary.
Flow.unwrapped({
Key key,
List<Widget> children: _emptyWidgetList,
this.delegate
}) : super(key: key, children: children) {
assert(delegate != null);
}
/// The delegate that controls the transformation matrices of the children.
final FlowDelegate delegate;
@override
RenderFlow createRenderObject(BuildContext context) => new RenderFlow(delegate: delegate);
@override
void updateRenderObject(BuildContext context, RenderFlow renderObject) {
renderObject
..delegate = delegate;
}
}
/// A paragraph of rich text.
///
/// The [RichText] widget displays text using a variety of different styles. The
......
// 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:test/test.dart';
class TestFlowDelegate extends FlowDelegate {
TestFlowDelegate({
Animation<double> startOffset
}) : startOffset = startOffset, super(repaint: startOffset);
final Animation<double> startOffset;
@override
BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) {
return constraints.loosen();
}
@override
void paintChildren(FlowPaintingContext context) {
double dy = startOffset.value;
for (int i = 0; i < context.childCount; ++i) {
context.paintChild(i, transform: new Matrix4.translationValues(0.0, dy, 0.0));
dy += 0.75 * context.getChildSize(i).height;
}
}
@override
bool shouldRepaint(TestFlowDelegate oldDelegate) => startOffset == oldDelegate.startOffset;
}
void main() {
test('Flow control test', () {
testWidgets((WidgetTester tester) {
AnimationController startOffset = new AnimationController.unbounded();
List<int> log = <int>[];
Widget buildBox(int i) {
return new GestureDetector(
onTap: () {
log.add(i);
},
child: new Container(
width: 100.0,
height: 100.0,
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF0000FF)
),
child: new Text('$i')
)
);
}
tester.pumpWidget(
new Flow(
delegate: new TestFlowDelegate(startOffset: startOffset),
children: <Widget>[
buildBox(0),
buildBox(1),
buildBox(2),
buildBox(3),
buildBox(4),
buildBox(5),
buildBox(6),
]
)
);
tester.tap(find.text('0'));
expect(log, equals([0]));
tester.tap(find.text('1'));
expect(log, equals([0, 1]));
tester.tap(find.text('2'));
expect(log, equals([0, 1, 2]));
log.clear();
tester.tapAt(new Point(20.0, 90.0));
expect(log, equals([1]));
startOffset.value = 50.0;
tester.pump();
log.clear();
tester.tapAt(new Point(20.0, 90.0));
expect(log, equals([0]));
});
});
}
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