diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart new file mode 100644 index 0000000000..3e639a06cf --- /dev/null +++ b/examples/catalog/lib/expansion_tile_sample.dart @@ -0,0 +1,87 @@ +// Copyright 2017 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 Entry { + Entry(this.title, [this.children = const []]); + final String title; + final List children; +} + +final List data = [ + new Entry('Chapter A', + [ + new Entry('Section A0', + [ + new Entry('Item A0.1'), + new Entry('Item A0.2'), + new Entry('Item A0.3'), + ], + ), + new Entry('Section A1'), + new Entry('Section A2'), + ], + ), + new Entry('Chapter B', + [ + new Entry('Section B0'), + new Entry('Section B1'), + ], + ), + new Entry('Chapter C', + [ + new Entry('Section C0'), + new Entry('Section C1'), + new Entry('Section C2', + [ + new Entry('Item C2.0'), + new Entry('Item C2.1'), + new Entry('Item C2.2'), + new Entry('Item C2.3'), + ], + ), + ], + ), +]; + +class EntryItem extends StatelessWidget { + EntryItem(this.entry); + + final Entry entry; + + Widget _buildTiles(Entry root) { + if (root.children.isEmpty) + return new ListTile(title: new Text(root.title)); + return new ExpansionTile( + key: new ValueKey(root), + title: new Text(root.title), + children: root.children.map(_buildTiles).toList(), + ); + } + + @override + Widget build(BuildContext context) { + return _buildTiles(entry); + } +} + +class ExpansionTileSample extends StatelessWidget { + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar( + title: const Text('ExpansionTile'), + ), + body: new ListView.builder( + itemBuilder: (BuildContext context, int index) => new EntryItem(data[index]), + itemCount: data.length, + ), + ); + } +} + +void main() { + runApp(new MaterialApp(home: new ExpansionTileSample())); +} diff --git a/examples/catalog/test/expansion_tile_sample_test.dart b/examples/catalog/test/expansion_tile_sample_test.dart new file mode 100644 index 0000000000..55fbf4ed9f --- /dev/null +++ b/examples/catalog/test/expansion_tile_sample_test.dart @@ -0,0 +1,91 @@ +// Copyright 2017 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'; +import 'package:flutter_test/flutter_test.dart'; + +import '../lib/expansion_tile_sample.dart' as expansion_tile_sample; +import '../lib/expansion_tile_sample.dart' show Entry; + +void main() { + testWidgets("expansion_tile sample smoke test", (WidgetTester tester) async { + expansion_tile_sample.main(); + await tester.pump(); + + // Initially only the top level EntryItems (the "chapters") are present. + for (Entry chapter in expansion_tile_sample.data) { + expect(find.text(chapter.title), findsOneWidget); + for (Entry section in chapter.children) { + expect(find.text(section.title), findsNothing); + for (Entry item in section.children) + expect(find.text(item.title), findsNothing); + } + } + + Future scrollUpOneEntry() async { + await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -88.00)); + await tester.pumpAndSettle(); + } + + Future tapEntry(String title) async { + await tester.tap(find.text(title)); + await tester.pumpAndSettle(); + } + + // Expand the chapters. Now the chapter and sections, but not the + // items, should be present. + for (Entry chapter in expansion_tile_sample.data.reversed) + await tapEntry(chapter.title); + + for (Entry chapter in expansion_tile_sample.data) { + expect(find.text(chapter.title), findsOneWidget); + for (Entry section in chapter.children) { + expect(find.text(section.title), findsOneWidget); + await scrollUpOneEntry(); + for (Entry item in section.children) + expect(find.text(item.title), findsNothing); + } + await scrollUpOneEntry(); + } + + // - scroll to the top - + await tester.flingFrom(const Offset(200.0, 200.0), const Offset(0.0, 100.0), 5000.0); + await tester.pumpAndSettle(); + + // Expand the sections. Now Widgets for all three levels should be present. + for (Entry chapter in expansion_tile_sample.data) { + for (Entry section in chapter.children) { + await tapEntry(section.title); + await scrollUpOneEntry(); + } + await scrollUpOneEntry(); + } + + // We're scrolled to the bottom so the very last item is visible. + // Working in reverse order, so we don't need to do anymore scrolling, + // check that everything is visible and close the sections and + // chapters as we go up. + for (Entry chapter in expansion_tile_sample.data.reversed) { + expect(find.text(chapter.title), findsOneWidget); + for (Entry section in chapter.children.reversed) { + expect(find.text(section.title), findsOneWidget); + for (Entry item in section.children.reversed) + expect(find.text(item.title), findsOneWidget); + await tapEntry(section.title); // close the section + } + await tapEntry(chapter.title); // close the chapter + } + + // Finally only the top level EntryItems (the "chapters") are present. + for (Entry chapter in expansion_tile_sample.data) { + expect(find.text(chapter.title), findsOneWidget); + for (Entry section in chapter.children) { + expect(find.text(section.title), findsNothing); + for (Entry item in section.children) + expect(find.text(item.title), findsNothing); + } + } + + }); +} diff --git a/packages/flutter/lib/src/material/expansion_tile.dart b/packages/flutter/lib/src/material/expansion_tile.dart index 8c1e7f882a..e288c2d0dd 100644 --- a/packages/flutter/lib/src/material/expansion_tile.dart +++ b/packages/flutter/lib/src/material/expansion_tile.dart @@ -20,7 +20,9 @@ const Duration _kExpand = const Duration(milliseconds: 200); /// the tile to reveal or hide the [children]. /// /// This widget is typically used with [ListView] to create an -/// "expand / collapse" list entry. +/// "expand / collapse" list entry. When used with scrolling widgets like +/// [ListView], a unique [key] must be specified to enable the [ExpansionTile] to +/// save and restore its expanded state when it is scrolled in and out of view. /// /// See also: /// @@ -110,7 +112,11 @@ class _ExpansionTileState extends State with SingleTickerProvider if (_isExpanded) _controller.forward(); else - _controller.reverse(); + _controller.reverse().then((Null value) { + setState(() { + // Rebuild without widget.children. + }); + }); PageStorage.of(context)?.writeState(context, _isExpanded); }); if (widget.onExpansionChanged != null) @@ -172,10 +178,12 @@ class _ExpansionTileState extends State with SingleTickerProvider ..begin = Colors.transparent ..end = widget.backgroundColor ?? Colors.transparent; + final bool closed = !_isExpanded && _controller.isDismissed; return new AnimatedBuilder( animation: _controller.view, builder: _buildChildren, - child: new Column(children: widget.children), + child: closed ? null : new Column(children: widget.children), ); + } }