mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
2da0eb9769
commit
09d26302cb
71
examples/widgets/indexed_stack.dart
Normal file
71
examples/widgets/indexed_stack.dart
Normal file
@ -0,0 +1,71 @@
|
||||
// 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>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
|
||||
RenderStack({
|
||||
List<RenderBox> children
|
||||
}) {
|
||||
abstract class RenderStackBase extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, StackParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
|
||||
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 {
|
||||
defaultPaint(context, offset);
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user