Commit 09d26302 authored by Hans Muller's avatar Hans Muller

IndexedStack

Added horizontal and vertical alignment properties to Stack so that the origin of non-positioned children can be specified. Currently all of the non-positioned children just end up with their top-left at 0,0. Now, for example, you can center the children by specifying verticalAlignment: 0.5, horizontalAlignment: 0.5.

Added IndexedStack which only paints the stack child specified by the index property. Since it's a Stack, it's as big as the biggest non-positioned child. This component will be essential for building mobile drop down menus.

Added a (likely temporary) example that demonstrates IndexedStack.
parent 2da0eb97
// 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:sky/material.dart';
import 'package:sky/rendering.dart';
import 'package:sky/widgets.dart';
class IndexedStackDemo extends StatefulComponent {
IndexedStackDemo({ this.navigator });
final NavigatorState navigator;
IndexedStackDemoState createState() => new IndexedStackDemoState();
}
class IndexedStackDemoState extends State<IndexedStackDemo> {
int _itemCount = 7;
int _itemIndex = 0;
void _handleTap() {
setState(() {
_itemIndex = (_itemIndex + 1) % _itemCount;
});
}
List <PopupMenuItem> _buildMenu(NavigatorState navigator) {
TextStyle style = const TextStyle(fontSize: 18.0, fontWeight: bold);
String pad = '';
return new List.generate(_itemCount, (int i) {
pad += '-';
return new PopupMenuItem(value: i, child: new Text('$pad Hello World $i $pad', style: style));
});
}
Widget build(BuildContext context) {
List <PopupMenuItem> items = _buildMenu(config.navigator);
IndexedStack indexedStack = new IndexedStack(items, index: _itemIndex, horizontalAlignment: 0.5);
return new Scaffold(
toolBar: new ToolBar(center: new Text('IndexedStackDemo Demo')),
body: new GestureDetector(
onTap: _handleTap,
child: new Container(
decoration: new BoxDecoration(backgroundColor: Theme.of(context).primarySwatch[50]),
child: new Center(
child: new Container(
child: indexedStack,
padding: const EdgeDims.all(8.0),
decoration: new BoxDecoration(border: new Border.all(color: Theme.of(context).accentColor))
)
)
)
)
);
}
}
void main() {
runApp(new App(
title: 'IndexedStackDemo',
theme: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Colors.blue,
accentColor: Colors.redAccent[200]
),
routes: {
'/': (RouteArguments args) => new IndexedStackDemo(navigator: args.navigator),
}
));
}
......@@ -61,7 +61,7 @@ class PaintingContext {
_startRecording(paintBounds);
}
/// Construct a painting context for paiting into the given layer with the given bounds
/// Construct a painting context for painting into the given layer with the given bounds
PaintingContext.withLayer(ContainerLayer containerLayer, Rect paintBounds) {
_containerLayer = containerLayer;
_startRecording(paintBounds);
......
......@@ -44,32 +44,14 @@ class StackParentData extends BoxParentData with ContainerParentDataMixin<Render
String toString() => '${super.toString()}; top=$top; right=$right; bottom=$bottom, left=$left';
}
/// Implements the stack layout algorithm
///
/// In a stack layout, the children are positioned on top of each other in the
/// order in which they appear in the child list. First, the non-positioned
/// children (those with null values for top, right, bottom, and left) are
/// layed out and placed in the upper-left corner of the stack. The stack is
/// then sized to enclose all of the non-positioned children. If there are no
/// non-positioned children, the stack becomes as large as possible.
///
/// Next, the positioned children are laid out. If a child has top and bottom
/// values that are both non-null, the child is given a fixed height determined
/// by deflating the width of the stack by the sum of the top and bottom values.
/// Similarly, if the child has rigth and left values that are both non-null,
/// the child is given a fixed width. Otherwise, the child is given unbounded
/// space in the non-fixed dimensions.
///
/// Once the child is laid out, the stack positions the child according to the
/// top, right, bottom, and left offsets. For example, if the top value is 10.0,
/// the top edge of the child will be placed 10.0 pixels from the top 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.
class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData>,
abstract class RenderStackBase extends RenderBox
with ContainerRenderObjectMixin<RenderBox, StackParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
RenderStack({
List<RenderBox> children
}) {
RenderStackBase({
List<RenderBox> children,
double horizontalAlignment: 0.0,
double verticalAlignment: 0.0
}) : _horizontalAlignment = horizontalAlignment, _verticalAlignment = verticalAlignment {
addAll(children);
}
......@@ -80,6 +62,24 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
child.parentData = new StackParentData();
}
double get horizontalAlignment => _horizontalAlignment;
double _horizontalAlignment;
void set horizontalAlignment (double value) {
if (_horizontalAlignment != value) {
_horizontalAlignment = value;
markNeedsLayout();
}
}
double get verticalAlignment => _verticalAlignment;
double _verticalAlignment;
void set verticalAlignment (double value) {
if (_verticalAlignment != value) {
_verticalAlignment = value;
markNeedsLayout();
}
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
double width = constraints.minWidth;
RenderBox child = firstChild;
......@@ -186,7 +186,11 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
assert(child.parentData is StackParentData);
final StackParentData childData = child.parentData;
if (childData.isPositioned) {
if (!childData.isPositioned) {
double x = (size.width - child.size.width) * horizontalAlignment;
double y = (size.height - child.size.height) * verticalAlignment;
childData.position = new Point(x, y);
} else {
BoxConstraints childConstraints = const BoxConstraints();
if (childData.left != null && childData.right != null)
......@@ -226,14 +230,118 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
defaultHitTestChildren(result, position: position);
}
void paintStack(PaintingContext context, Offset offset);
void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow) {
context.canvas.save();
context.canvas.clipRect(offset & size);
defaultPaint(context, offset);
paintStack(context, offset);
context.canvas.restore();
} else {
paintStack(context, offset);
}
}
}
/// Implements the stack layout algorithm
///
/// In a stack layout, the children are positioned on top of each other in the
/// order in which they appear in the child list. First, the non-positioned
/// children (those with null values for top, right, bottom, and left) are
/// initially layed out and placed in the upper-left corner of the stack. The
/// stack is then sized to enclose all of the non-positioned children. If there
/// are no non-positioned children, the stack becomes as large as possible.
///
/// The final location of non-positioned children is determined by the alignment
/// parameters. The left of each non-positioned child becomes the
/// difference between the child's width and the stack's width scaled by
/// horizontalAlignment. The top of each non-positioned child is computed
/// similarly and scaled by verticalAlignement. So if the alignment parameters
/// are 0.0 (the default) then the non-positioned children remain in the
/// upper-left corner. If the alignment parameters are 0.5 then the
/// non-positioned children are centered within the stack.
///
/// Next, the positioned children are laid out. If a child has top and bottom
/// values that are both non-null, the child is given a fixed height determined
/// by deflating the width of the stack by the sum of the top and bottom values.
/// Similarly, if the child has rigth and left values that are both non-null,
/// the child is given a fixed width. Otherwise, the child is given unbounded
/// space in the non-fixed dimensions.
///
/// Once the child is laid out, the stack positions the child according to the
/// top, right, bottom, and left offsets. For example, if the top value is 10.0,
/// the top edge of the child will be placed 10.0 pixels from the top 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.
class RenderStack extends RenderStackBase {
RenderStack({
List<RenderBox> children,
double horizontalAlignment: 0.0,
double verticalAlignment: 0.0
}) : super(
children: children,
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment
);
void paintStack(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
}
/// Implements the same layout algorithm as RenderStack but only paints the child
/// specified by index.
/// Note: although only one child is displayed, the cost of the layout algorithm is
/// still O(N), like an ordinary stack.
class RenderIndexedStack extends RenderStackBase {
RenderIndexedStack({
List<RenderBox> children,
double horizontalAlignment: 0.0,
double verticalAlignment: 0.0,
int index: 0
}) : _index = index, super(
children: children,
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment
);
int get index => _index;
int _index;
void set index (int value) {
if (_index != value) {
_index = value;
markNeedsLayout();
}
}
RenderBox _childAtIndex() {
RenderBox child = firstChild;
int i = 0;
while (child != null && i < index) {
assert(child.parentData is StackParentData);
child = child.parentData.nextSibling;
i += 1;
}
assert(i == index);
assert(child != null);
return child;
}
void hitTestChildren(HitTestResult result, { Point position }) {
if (firstChild == null)
return;
assert(position != null);
RenderBox child = _childAtIndex();
Point transformed = new Point(position.x - child.parentData.position.x,
position.y - child.parentData.position.y);
child.hitTest(result, position: transformed);
}
void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null)
return;
RenderBox child = _childAtIndex();
context.paintChild(child, child.parentData.position + offset);
}
}
......@@ -513,8 +513,57 @@ class BlockBody extends MultiChildRenderObjectWidget {
}
class Stack extends MultiChildRenderObjectWidget {
Stack(List<Widget> children, { Key key }) : super(key: key, children: children);
RenderStack createRenderObject() => new RenderStack();
Stack(List<Widget> children, {
Key key,
this.horizontalAlignment: 0.0,
this.verticalAlignment: 0.0
}) : super(key: key, children: children) {
assert(horizontalAlignment != null);
assert(verticalAlignment != null);
}
final double horizontalAlignment;
final double verticalAlignment;
RenderStack createRenderObject() {
return new RenderStack(
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment
);
}
void updateRenderObject(RenderStack renderObject, Stack oldWidget) {
renderObject.horizontalAlignment = horizontalAlignment;
renderObject.verticalAlignment = verticalAlignment;
}
}
class IndexedStack extends MultiChildRenderObjectWidget {
IndexedStack(List<Widget> children, {
Key key,
this.horizontalAlignment: 0.0,
this.verticalAlignment: 0.0,
this.index: 0
}) : super(key: key, children: children);
final int index;
final double horizontalAlignment;
final double verticalAlignment;
RenderIndexedStack createRenderObject() {
return new RenderIndexedStack(
index: index,
verticalAlignment: verticalAlignment,
horizontalAlignment: horizontalAlignment
);
}
void updateRenderObject(RenderIndexedStack renderObject, IndexedStack oldWidget) {
super.updateRenderObject(renderObject, oldWidget);
renderObject.index = index;
renderObject.horizontalAlignment = horizontalAlignment;
renderObject.verticalAlignment = verticalAlignment;
}
}
class Positioned extends ParentDataWidget {
......
......@@ -4,6 +4,12 @@ import 'package:test/test.dart';
import 'widget_tester.dart';
void main() {
test('Can construct an empty Stack', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new Stack([]));
});
});
test('Can change position data', () {
testWidgets((WidgetTester tester) {
Key key = new Key('container');
......@@ -70,4 +76,89 @@ void main() {
expect(containerElement.renderObject.parentData.left, isNull);
});
});
test('Can align non-positioned children', () {
testWidgets((WidgetTester tester) {
Key child0Key = new Key('child0');
Key child1Key = new Key('child1');
tester.pumpWidget(
new Center(
child: new Stack([
new Container(key: child0Key, width: 20.0, height: 20.0),
new Container(key: child1Key, width: 10.0, height: 10.0)
],
horizontalAlignment: 0.5,
verticalAlignment: 0.5
)
)
);
Element child0 = tester.findElementByKey(child0Key);
expect(child0.renderObject.parentData.position, equals(const Point(0.0, 0.0)));
Element child1 = tester.findElementByKey(child1Key);
expect(child1.renderObject.parentData.position, equals(const Point(5.0, 5.0)));
});
});
test('Can construct an empty IndexedStack', () {
testWidgets((WidgetTester tester) {
tester.pumpWidget(new IndexedStack([]));
});
});
test('Can construct an IndexedStack', () {
testWidgets((WidgetTester tester) {
int itemCount = 3;
List<int> itemsPainted;
Widget buildFrame(int index) {
itemsPainted = [];
List<Widget> items = new List.generate(itemCount, (i) {
return new CustomPaint(child: new Text('$i'), callback: (_0, _1) { itemsPainted.add(i); });
});
return new Center(child: new IndexedStack(items, index: index));
}
tester.pumpWidget(buildFrame(0));
expect(tester.findText('0'), isNotNull);
expect(tester.findText('1'), isNotNull);
expect(tester.findText('2'), isNotNull);
expect(itemsPainted, equals([0]));
tester.pumpWidget(buildFrame(1));
expect(itemsPainted, equals([1]));
tester.pumpWidget(buildFrame(2));
expect(itemsPainted, equals([2]));
});
});
test('Can hit test an IndexedStack', () {
testWidgets((WidgetTester tester) {
Key key = new Key('indexedStack');
int itemCount = 3;
List<int> itemsTapped;
Widget buildFrame(int index) {
itemsTapped = [];
List<Widget> items = new List.generate(itemCount, (i) {
return new GestureDetector(child: new Text('$i'), onTap: () { itemsTapped.add(i); });
});
return new Center(child: new IndexedStack(items, key: key, index: index));
}
tester.pumpWidget(buildFrame(0));
expect(itemsTapped, isEmpty);
tester.tap(tester.findElementByKey(key));
expect(itemsTapped, [0]);
tester.pumpWidget(buildFrame(2));
expect(itemsTapped, isEmpty);
tester.tap(tester.findElementByKey(key));
expect(itemsTapped, [2]);
});
});
}
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