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);
|
_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) {
|
PaintingContext.withLayer(ContainerLayer containerLayer, Rect paintBounds) {
|
||||||
_containerLayer = containerLayer;
|
_containerLayer = containerLayer;
|
||||||
_startRecording(paintBounds);
|
_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';
|
String toString() => '${super.toString()}; top=$top; right=$right; bottom=$bottom, left=$left';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implements the stack layout algorithm
|
abstract class RenderStackBase extends RenderBox
|
||||||
///
|
with ContainerRenderObjectMixin<RenderBox, StackParentData>,
|
||||||
/// 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> {
|
RenderBoxContainerDefaultsMixin<RenderBox, StackParentData> {
|
||||||
RenderStack({
|
RenderStackBase({
|
||||||
List<RenderBox> children
|
List<RenderBox> children,
|
||||||
}) {
|
double horizontalAlignment: 0.0,
|
||||||
|
double verticalAlignment: 0.0
|
||||||
|
}) : _horizontalAlignment = horizontalAlignment, _verticalAlignment = verticalAlignment {
|
||||||
addAll(children);
|
addAll(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +62,24 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
|
|||||||
child.parentData = new StackParentData();
|
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 getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||||
double width = constraints.minWidth;
|
double width = constraints.minWidth;
|
||||||
RenderBox child = firstChild;
|
RenderBox child = firstChild;
|
||||||
@ -186,7 +186,11 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
|
|||||||
assert(child.parentData is StackParentData);
|
assert(child.parentData is StackParentData);
|
||||||
final StackParentData childData = child.parentData;
|
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();
|
BoxConstraints childConstraints = const BoxConstraints();
|
||||||
|
|
||||||
if (childData.left != null && childData.right != null)
|
if (childData.left != null && childData.right != null)
|
||||||
@ -226,14 +230,118 @@ class RenderStack extends RenderBox with ContainerRenderObjectMixin<RenderBox, S
|
|||||||
defaultHitTestChildren(result, position: position);
|
defaultHitTestChildren(result, position: position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void paintStack(PaintingContext context, Offset offset);
|
||||||
|
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
if (_hasVisualOverflow) {
|
if (_hasVisualOverflow) {
|
||||||
context.canvas.save();
|
context.canvas.save();
|
||||||
context.canvas.clipRect(offset & size);
|
context.canvas.clipRect(offset & size);
|
||||||
defaultPaint(context, offset);
|
paintStack(context, offset);
|
||||||
context.canvas.restore();
|
context.canvas.restore();
|
||||||
} else {
|
} 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 {
|
class Stack extends MultiChildRenderObjectWidget {
|
||||||
Stack(List<Widget> children, { Key key }) : super(key: key, children: children);
|
Stack(List<Widget> children, {
|
||||||
RenderStack createRenderObject() => new RenderStack();
|
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 {
|
class Positioned extends ParentDataWidget {
|
||||||
|
@ -4,6 +4,12 @@ import 'package:test/test.dart';
|
|||||||
import 'widget_tester.dart';
|
import 'widget_tester.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
test('Can construct an empty Stack', () {
|
||||||
|
testWidgets((WidgetTester tester) {
|
||||||
|
tester.pumpWidget(new Stack([]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Can change position data', () {
|
test('Can change position data', () {
|
||||||
testWidgets((WidgetTester tester) {
|
testWidgets((WidgetTester tester) {
|
||||||
Key key = new Key('container');
|
Key key = new Key('container');
|
||||||
@ -70,4 +76,89 @@ void main() {
|
|||||||
expect(containerElement.renderObject.parentData.left, isNull);
|
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