From 09d26302cb41c94630bff20de34b50ac7d571b7b Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 6 Oct 2015 16:10:36 -0700 Subject: [PATCH] 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. --- examples/widgets/indexed_stack.dart | 71 ++++++++ .../flutter/lib/src/rendering/object.dart | 2 +- packages/flutter/lib/src/rendering/stack.dart | 166 +++++++++++++++--- packages/flutter/lib/src/widgets/basic.dart | 53 +++++- packages/unit/test/widget/stack_test.dart | 91 ++++++++++ 5 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 examples/widgets/indexed_stack.dart diff --git a/examples/widgets/indexed_stack.dart b/examples/widgets/indexed_stack.dart new file mode 100644 index 00000000000..1833412763d --- /dev/null +++ b/examples/widgets/indexed_stack.dart @@ -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 { + int _itemCount = 7; + int _itemIndex = 0; + + void _handleTap() { + setState(() { + _itemIndex = (_itemIndex + 1) % _itemCount; + }); + } + + List _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 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), + } + )); +} diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 66445d2858a..f887c11cb6f 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -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); diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index 0ea40b060c0..41484f2121f 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -44,32 +44,14 @@ class StackParentData extends BoxParentData with ContainerParentDataMixin '${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, - RenderBoxContainerDefaultsMixin { - RenderStack({ - List children - }) { +abstract class RenderStackBase extends RenderBox + with ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + RenderStackBase({ + List 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 _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 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 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); + } +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 7b176901789..ea3a835f990 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -513,8 +513,57 @@ class BlockBody extends MultiChildRenderObjectWidget { } class Stack extends MultiChildRenderObjectWidget { - Stack(List children, { Key key }) : super(key: key, children: children); - RenderStack createRenderObject() => new RenderStack(); + Stack(List 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 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 { diff --git a/packages/unit/test/widget/stack_test.dart b/packages/unit/test/widget/stack_test.dart index 1866bd64364..d7e9ac8afee 100644 --- a/packages/unit/test/widget/stack_test.dart +++ b/packages/unit/test/widget/stack_test.dart @@ -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 itemsPainted; + + Widget buildFrame(int index) { + itemsPainted = []; + List 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 itemsTapped; + + Widget buildFrame(int index) { + itemsTapped = []; + List 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]); + }); + }); + }