From 5ae1b41ca4099a9148b7581790c328517a47fb93 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 21 Jan 2016 12:58:37 -0800 Subject: [PATCH] Added TwoLevelList --- examples/material_gallery/flutter.yaml | 2 + .../lib/demo/two_level_list_demo.dart | 32 ++++ examples/material_gallery/lib/main.dart | 2 + packages/flutter/lib/material.dart | 1 + .../lib/src/material/material_list.dart | 4 +- .../lib/src/material/two_level_list.dart | 159 ++++++++++++++++++ .../test/widget/two_level_list_test.dart | 71 ++++++++ .../flutter_test/lib/src/instrumentation.dart | 7 + 8 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 examples/material_gallery/lib/demo/two_level_list_demo.dart create mode 100644 packages/flutter/lib/src/material/two_level_list.dart create mode 100644 packages/flutter/test/widget/two_level_list_test.dart diff --git a/examples/material_gallery/flutter.yaml b/examples/material_gallery/flutter.yaml index fadd83829c3..bf8bef737d5 100644 --- a/examples/material_gallery/flutter.yaml +++ b/examples/material_gallery/flutter.yaml @@ -12,6 +12,8 @@ material-design-icons: - name: navigation/arrow_forward - name: navigation/arrow_back - name: navigation/cancel + - name: navigation/expand_less + - name: navigation/expand_more - name: navigation/menu - name: action/event - name: action/home diff --git a/examples/material_gallery/lib/demo/two_level_list_demo.dart b/examples/material_gallery/lib/demo/two_level_list_demo.dart new file mode 100644 index 00000000000..0604ac859b2 --- /dev/null +++ b/examples/material_gallery/lib/demo/two_level_list_demo.dart @@ -0,0 +1,32 @@ +// Copyright 2016 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/material.dart'; + +class TwoLevelListDemo extends StatelessComponent { + Widget build(BuildContext context) { + return new Scaffold( + toolBar: new ToolBar(center: new Text('Expand/Collapse List Control')), + body: new Padding( + padding: const EdgeDims.all(0.0), + child: new TwoLevelList( + type: MaterialListType.oneLine, + items: [ + new TwoLevelListItem(center: new Text('Top')), + new TwoLevelSublist( + center: new Text('Sublist'), + children: [ + new TwoLevelListItem(center: new Text('One')), + new TwoLevelListItem(center: new Text('Two')), + new TwoLevelListItem(center: new Text('Free')), + new TwoLevelListItem(center: new Text('Four')) + ] + ), + new TwoLevelListItem(center: new Text('Bottom')) + ] + ) + ) + ); + } +} diff --git a/examples/material_gallery/lib/main.dart b/examples/material_gallery/lib/main.dart index 3301ca3e760..10c2ef99856 100644 --- a/examples/material_gallery/lib/main.dart +++ b/examples/material_gallery/lib/main.dart @@ -15,6 +15,7 @@ import 'demo/toggle_controls_demo.dart'; import 'demo/slider_demo.dart'; import 'demo/tabs_demo.dart'; import 'demo/time_picker_demo.dart'; +import 'demo/two_level_list_demo.dart'; class GalleryDemo { GalleryDemo({ this.title, this.builder }); @@ -162,6 +163,7 @@ class GalleryHome extends StatelessComponent { new GalleryDemo(title: 'Toggle Controls', builder: (_) => new ToggleControlsDemo()), new GalleryDemo(title: 'Dropdown Button', builder: (_) => new DropDownDemo()), new GalleryDemo(title: 'Tabs', builder: (_) => new TabsDemo()), + new GalleryDemo(title: 'Expland/Collapse List Control', builder: (_) => new TwoLevelListDemo()), new GalleryDemo(title: 'Page Selector', builder: (_) => new PageSelectorDemo()), new GalleryDemo(title: 'Date Picker', builder: (_) => new DatePickerDemo()), new GalleryDemo(title: 'Time Picker', builder: (_) => new TimePickerDemo()) diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index f18e12becf3..74c1b588e4c 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -54,6 +54,7 @@ export 'src/material/time_picker_dialog.dart'; export 'src/material/toggleable.dart'; export 'src/material/tool_bar.dart'; export 'src/material/tooltip.dart'; +export 'src/material/two_level_list.dart'; export 'src/material/typography.dart'; export 'widgets.dart'; diff --git a/packages/flutter/lib/src/material/material_list.dart b/packages/flutter/lib/src/material/material_list.dart index 6e5fea25e63..aa5434cf5ad 100644 --- a/packages/flutter/lib/src/material/material_list.dart +++ b/packages/flutter/lib/src/material/material_list.dart @@ -14,7 +14,7 @@ enum MaterialListType { threeLine } -Map _kItemExtent = const { +Map kListItemExtent = const { MaterialListType.oneLine: kOneLineListItemHeight, MaterialListType.oneLineWithAvatar: kOneLineListItemWithAvatarHeight, MaterialListType.twoLine: kTwoLineListItemHeight, @@ -46,7 +46,7 @@ class _MaterialListState extends State { initialScrollOffset: config.initialScrollOffset, scrollDirection: Axis.vertical, onScroll: config.onScroll, - itemExtent: _kItemExtent[config.type], + itemExtent: kListItemExtent[config.type], padding: const EdgeDims.symmetric(vertical: 8.0), scrollableListPainter: _scrollbarPainter, children: config.children diff --git a/packages/flutter/lib/src/material/two_level_list.dart b/packages/flutter/lib/src/material/two_level_list.dart new file mode 100644 index 00000000000..6af08a6b0c2 --- /dev/null +++ b/packages/flutter/lib/src/material/two_level_list.dart @@ -0,0 +1,159 @@ +// Copyright 2016 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/animation.dart'; +import 'package:flutter/widgets.dart'; + +import 'colors.dart'; +import 'icon.dart'; +import 'list_item.dart'; +import 'material_list.dart'; +import 'theme.dart'; +import 'theme_data.dart'; + +const Duration _kExpand = const Duration(milliseconds: 200); + +class TwoLevelListItem extends StatelessComponent { + TwoLevelListItem({ + Key key, + this.left, + this.center, + this.right, + this.onTap, + this.onLongPress + }) : super(key: key) { + assert(center != null); + } + + final Widget left; + final Widget center; + final Widget right; + final GestureTapCallback onTap; + final GestureLongPressCallback onLongPress; + + Widget build(BuildContext context) { + final TwoLevelList parentList = context.ancestorWidgetOfExactType(TwoLevelList); + assert(parentList != null); + + return new SizedBox( + height: kListItemExtent[parentList.type], + child: new ListItem( + left: left, + center: center, + right: right, + onTap: onTap, + onLongPress: onLongPress + ) + ); + } +} + +class TwoLevelSublist extends StatefulComponent { + TwoLevelSublist({ Key key, this.left, this.center, this.children }) : super(key: key); + + final Widget left; + final Widget center; + final List children; + + _TwoLevelSublistState createState() => new _TwoLevelSublistState(); +} + +class _TwoLevelSublistState extends State { + AnimationController _controller; + CurvedAnimation _easeOutAnimation; + CurvedAnimation _easeInAnimation; + ColorTween _borderColor; + ColorTween _headerColor; + ColorTween _iconColor; + Animation _iconTurns; + + bool _isExpanded = false; + + void initState() { + super.initState(); + _controller = new AnimationController(duration: _kExpand); + _easeOutAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeOut); + _easeInAnimation = new CurvedAnimation(parent: _controller, curve: Curves.easeIn); + _borderColor = new ColorTween(begin: Colors.transparent); + _headerColor = new ColorTween(); + _iconColor = new ColorTween(); + _iconTurns = new Tween(begin: 0.0, end: 0.5).animate(_easeInAnimation); + + _isExpanded = PageStorage.of(context)?.readState(context) ?? false; + if (_isExpanded) + _controller.value = 1.0; + } + + void _handleOnTap() { + setState(() { + _isExpanded = !_isExpanded; + if (_isExpanded) + _controller.forward(); + else + _controller.reverse(); + PageStorage.of(context)?.writeState(context, _isExpanded); + }); + } + + Widget buildList(BuildContext context, Widget child) { + return new Container( + decoration: new BoxDecoration( + border: new Border( + top: new BorderSide(color: _borderColor.evaluate(_easeOutAnimation)), + bottom: new BorderSide(color: _borderColor.evaluate(_easeOutAnimation)) + ) + ), + child: new Column( + children: [ + new TwoLevelListItem( + onTap: _handleOnTap, + left: config.left, + center: new DefaultTextStyle( + style: Theme.of(context).text.body1.copyWith(color: _headerColor.evaluate(_easeInAnimation)), + child: config.center + ), + right: new RotationTransition( + turns: _iconTurns, + child: new Icon( + icon: 'navigation/expand_more', + colorFilter: new ColorFilter.mode(_iconColor.evaluate(_easeInAnimation), TransferMode.srcATop) + ) + ) + ), + new ClipRect( + child: new Align( + heightFactor: _easeInAnimation.value, + child: new Column(children: config.children) + ) + ) + ] + ) + ); + } + + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + _borderColor.end = theme.dividerColor; + _headerColor + ..begin = theme.text.body1.color + ..end = theme.accentColor; + _iconColor + ..begin = theme.unselectedColor + ..end = theme.accentColor; + + return new AnimatedBuilder( + animation: _controller.view, + builder: buildList + ); + } +} + +class TwoLevelList extends StatelessComponent { + TwoLevelList({ Key key, this.items, this.type: MaterialListType.twoLine }) : super(key: key); + + final List items; + final MaterialListType type; + + Widget build(BuildContext context) => new Block(items); +} diff --git a/packages/flutter/test/widget/two_level_list_test.dart b/packages/flutter/test/widget/two_level_list_test.dart new file mode 100644 index 00000000000..b55f3e30f4e --- /dev/null +++ b/packages/flutter/test/widget/two_level_list_test.dart @@ -0,0 +1,71 @@ +// Copyright 2016 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:flutter/material.dart'; +import 'package:test/test.dart'; + +void main() { + test('TwoLeveList basics', () { + final Key topKey = new UniqueKey(); + final Key sublistKey = new UniqueKey(); + final Key bottomKey = new UniqueKey(); + + final Map routes = { + '/': (_) { + return new Material( + child: new Viewport( + child: new TwoLevelList( + items: [ + new TwoLevelListItem(center: new Text('Top'), key: topKey), + new TwoLevelSublist( + key: sublistKey, + center: new Text('Sublist'), + children: [ + new TwoLevelListItem(center: new Text('0')), + new TwoLevelListItem(center: new Text('1')) + ] + ), + new TwoLevelListItem(center: new Text('Bottom'), key: bottomKey) + ] + ) + ) + ); + } + }; + + testWidgets((WidgetTester tester) { + tester.pumpWidget(new MaterialApp(routes: routes)); + + expect(tester.findText('Top'), isNotNull); + expect(tester.findText('Sublist'), isNotNull); + expect(tester.findText('Bottom'), isNotNull); + + double getY(Key key) => tester.getTopLeft(tester.findElementByKey(key)).y; + double getHeight(Key key) => tester.getSize(tester.findElementByKey(key)).height; + + expect(getY(topKey), lessThan(getY(sublistKey))); + expect(getY(sublistKey), lessThan(getY(bottomKey))); + + expect(getHeight(topKey), equals(getHeight(sublistKey))); + expect(getHeight(sublistKey), equals(getHeight(bottomKey))); + + tester.tap(tester.findText('Sublist')); + tester.pump(const Duration(seconds: 1)); + tester.pump(const Duration(seconds: 1)); + + expect(tester.findText('Top'), isNotNull); + expect(tester.findText('Sublist'), isNotNull); + expect(tester.findText('0'), isNotNull); + expect(tester.findText('1'), isNotNull); + expect(tester.findText('Bottom'), isNotNull); + + expect(getY(topKey), lessThan(getY(sublistKey))); + expect(getY(sublistKey), lessThan(getY(bottomKey))); + expect(getY(bottomKey) - getY(sublistKey), greaterThan(getHeight(topKey))); + expect(getY(bottomKey) - getY(sublistKey), greaterThan(getHeight(bottomKey))); + }); + }); +} diff --git a/packages/flutter_test/lib/src/instrumentation.dart b/packages/flutter_test/lib/src/instrumentation.dart index 07306febcd5..1c00ba00ad3 100644 --- a/packages/flutter_test/lib/src/instrumentation.dart +++ b/packages/flutter_test/lib/src/instrumentation.dart @@ -99,6 +99,13 @@ class Instrumentation { return _getElementPoint(element, (Size size) => size.bottomRight(Point.origin)); } + Size getSize(Element element) { + assert(element != null); + RenderBox box = element.renderObject as RenderBox; + assert(box != null); + return box.size; + } + Point _getElementPoint(Element element, SizeToPointFunction sizeToPoint) { assert(element != null); RenderBox box = element.renderObject as RenderBox;