Adam Barth 7ab122e557 PopupMenuButton should lazily build menu items
Previously, the client of PopupMenuButton needed to build all the menu times

when building the PopupMenuButton. This can get expensive if, for example, each
item in a scrollable list has a popup menu associated with it.

Now the client passes a builder function to the PopupMenuButton that gets
invoked only when its time to show the menu items.
2016-04-06 13:28:09 -07:00

645 lines
19 KiB
Dart

// 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:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
void main() {
runApp(
new ComplexLayoutApp()
);
}
class ComplexLayoutApp extends StatefulWidget {
@override
ComplexLayoutAppState createState() => new ComplexLayoutAppState();
static ComplexLayoutAppState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ComplexLayoutAppState>());
}
class ComplexLayoutAppState extends State<ComplexLayoutApp> {
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: lightTheme ? new ThemeData.light() : new ThemeData.dark(),
title: 'Advanced Layout',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => new ComplexLayout(),
}
);
}
bool _lightTheme = true;
bool get lightTheme => _lightTheme;
void set lightTheme(bool value) {
setState(() {
_lightTheme = value;
});
}
}
class ComplexLayout extends StatefulWidget {
ComplexLayout({ Key key }) : super(key: key);
@override
ComplexLayoutState createState() => new ComplexLayoutState();
static ComplexLayoutState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher<ComplexLayoutState>());
}
class FancyItemDelegate extends LazyBlockDelegate {
@override
Widget buildItem(BuildContext context, int index) {
if (index % 2 == 0)
return new FancyImageItem(index, key: new Key("Item $index"));
else
return new FancyGalleryItem(index, key: new Key("Item $index"));
}
@override
bool shouldRebuild(FancyItemDelegate oldDelegate) => false;
}
class ComplexLayoutState extends State<ComplexLayout> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Advanced Layout'),
actions: <Widget>[
new IconButton(
icon: Icons.create,
tooltip: 'Search',
onPressed: () {
print("Pressed search");
}
),
new TopBarMenu()
]
),
body: new Column(
children: <Widget>[
new Flexible(
child: new LazyBlock(
key: new Key("main-scroll"),
delegate: new FancyItemDelegate()
)
),
new BottomBar()
]
),
drawer: new GalleryDrawer()
);
}
}
class TopBarMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new PopupMenuButton<String>(
onSelected: (String value) { print("Selected: $value"); },
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
value: "Friends",
child: new MenuItemWithIcon(Icons.people, "Friends", "5 new")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.event, "Events", "12 upcoming")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.group, "Groups", "14")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.image, "Pictures", "12")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.near_me, "Nearby", "33")
),
new PopupMenuItem<String>(
value: "Friends",
child: new MenuItemWithIcon(Icons.people, "Friends", "5")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.event, "Events", "12")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.group, "Groups", "14")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.image, "Pictures", "12")
),
new PopupMenuItem<String>(
value: "Events",
child: new MenuItemWithIcon(Icons.near_me, "Nearby", "33")
)
]
);
}
}
class MenuItemWithIcon extends StatelessWidget {
MenuItemWithIcon(this.icon, this.title, this.subtitle);
final IconData icon;
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return new Row(
children: <Widget>[
new Icon(icon: icon),
new Padding(
padding: new EdgeInsets.only(left: 8.0, right: 8.0),
child: new Text(title)
),
new Text(subtitle, style: Theme.of(context).textTheme.caption)
]
);
}
}
class FancyImageItem extends StatelessWidget {
FancyImageItem(this.index, {Key key}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
return new BlockBody(
children: <Widget>[
new UserHeader("Ali Connors $index"),
new ItemDescription(),
new ItemImageBox(),
new InfoBar(),
new Padding(
padding: new EdgeInsets.symmetric(horizontal: 8.0),
child: new Divider()
),
new IconBar(),
new FatDivider()
]
);
}
}
class FancyGalleryItem extends StatelessWidget {
FancyGalleryItem(this.index, {Key key}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
return new BlockBody(
children: <Widget>[
new UserHeader("Ali Connors"),
new ItemGalleryBox(index),
new InfoBar(),
new Padding(
padding: new EdgeInsets.symmetric(horizontal: 8.0),
child: new Divider()
),
new IconBar(),
new FatDivider()
]
);
}
}
class InfoBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.all(8.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new MiniIconWithText(Icons.thumb_up, "42"),
new Text("3 Comments", style: Theme.of(context).textTheme.caption)
]
)
);
}
}
class IconBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.only(left: 16.0, right: 16.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new IconWithText(Icons.thumb_up, "Like"),
new IconWithText(Icons.comment, "Comment"),
new IconWithText(Icons.share, "Share"),
]
)
);
}
}
class IconWithText extends StatelessWidget {
IconWithText(this.icon, this.title);
final IconData icon;
final String title;
@override
Widget build(BuildContext context) {
return new Row(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[
new IconButton(icon: icon, onPressed: () { print("Pressed $title button"); } ),
new Text(title)
]
);
}
}
class MiniIconWithText extends StatelessWidget {
MiniIconWithText(this.icon, this.title);
final IconData icon;
final String title;
@override
Widget build(BuildContext context) {
return new Row(
mainAxisAlignment: MainAxisAlignment.collapse,
children: <Widget>[
new Padding(
padding: new EdgeInsets.only(right: 8.0),
child: new Container(
width: 16.0,
height: 16.0,
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).primaryColor,
shape: BoxShape.circle
),
child: new Icon(icon: icon, color: Colors.white, size: 12.0)
)
),
new Text(title, style: Theme.of(context).textTheme.caption)
]
);
}
}
class FatDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
height: 8.0,
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).dividerColor
)
);
}
}
class UserHeader extends StatelessWidget {
UserHeader(this.userName);
final String userName;
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.all(8.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Padding(
padding: new EdgeInsets.only(right: 8.0),
child: new AssetImage(
name: "packages/flutter_gallery_assets/ali_connors_sml.png",
width: 32.0,
height: 32.0
)
),
new Flexible(
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new RichText(text: new TextSpan(
style: Theme.of(context).textTheme.body1,
children: <TextSpan>[
new TextSpan(text: userName, style: new TextStyle(fontWeight: FontWeight.bold)),
new TextSpan(text: " shared a new "),
new TextSpan(text: "photo", style: new TextStyle(fontWeight: FontWeight.bold))
]
)),
new Row(
children: <Widget>[
new Text("Yesterday at 11:55 • ", style: Theme.of(context).textTheme.caption),
new Icon(icon: Icons.people, size: 16.0, color: Theme.of(context).textTheme.caption.color)
]
)
]
)
),
new TopBarMenu()
]
)
);
}
}
class ItemDescription extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.all(8.0),
child: new Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
);
}
}
class ItemImageBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.all(8.0),
child: new Card(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Stack(
children: <Widget>[
new SizedBox(
height: 230.0,
child: new AssetImage(
name: "packages/flutter_gallery_assets/top_10_australian_beaches.png"
)
),
new Theme(
data: new ThemeData.dark(),
child: new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
new IconButton(icon: Icons.edit, onPressed: () { print("Pressed edit button"); }),
new IconButton(icon: Icons.zoom_in, onPressed: () { print("Pressed zoom button"); })
]
)
),
new Positioned(
bottom: 4.0,
left: 4.0,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: Colors.black54,
borderRadius: 2.0
),
padding: new EdgeInsets.all(4.0),
child: new RichText(
text: new TextSpan(
style: new TextStyle(color: Colors.white),
children: <TextSpan>[
new TextSpan(
text: "Photo by "
),
new TextSpan(
style: new TextStyle(fontWeight: FontWeight.bold),
text: "Magic Mike"
)
]
)
)
)
)
]
)
,
new Padding(
padding: new EdgeInsets.all(8.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Text("Where can you find that amazing sunset?", style: Theme.of(context).textTheme.body2),
new Text("The sun sets over stinson beach", style: Theme.of(context).textTheme.body1),
new Text("flutter.io/amazingsunsets", style: Theme.of(context).textTheme.caption)
]
)
)
]
)
)
);
}
}
class ItemGalleryBox extends StatelessWidget {
ItemGalleryBox(this.index);
final int index;
@override
Widget build(BuildContext context) {
List<String> tabNames = <String>[
"A", "B", "C", "D"
];
return new SizedBox(
height: 200.0,
child: new TabBarSelection<String>(
values: tabNames,
child: new Column(
children: <Widget>[
new Flexible(
child: new TabBarView(
children: tabNames.map((String tabName) {
return new Container(
key: new Key("Tab $index - $tabName"),
child: new Padding(
padding: new EdgeInsets.all(8.0),
child: new Card(
child: new Column(
children: <Widget>[
new Flexible(
child: new Container(
decoration: new BoxDecoration(
backgroundColor: Theme.of(context).primaryColor
),
child: new Center(
child: new Text(tabName, style: Theme.of(context).textTheme.headline.copyWith(color: Colors.white))
)
)
),
new Row(
children: <Widget>[
new IconButton(
icon: Icons.share,
onPressed: () { print("Pressed share"); }
),
new IconButton(
icon: Icons.event,
onPressed: () { print("Pressed event"); }
),
new Flexible(
child: new Padding(
padding: new EdgeInsets.only(left: 8.0),
child: new Text("This is item $tabName")
)
)
]
)
]
)
)
)
);
}).toList()
)
),
new Container(
child: new TabPageSelector<String>()
)
]
)
)
);
}
}
class BottomBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(
border: new Border(
top: new BorderSide(
color: Theme.of(context).dividerColor,
width: 1.0
)
)
),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new BottomBarButton(Icons.new_releases, "News"),
new BottomBarButton(Icons.people, "Requests"),
new BottomBarButton(Icons.chat, "Messenger"),
new BottomBarButton(Icons.bookmark, "Bookmark"),
new BottomBarButton(Icons.alarm, "Alarm")
]
)
);
}
}
class BottomBarButton extends StatelessWidget {
BottomBarButton(this.icon, this.title);
final IconData icon;
final String title;
@override
Widget build(BuildContext context) {
return new Padding(
padding: new EdgeInsets.all(8.0),
child: new Column(
children: <Widget>[
new IconButton(
icon: icon,
onPressed: () { print("Pressed: $title"); }
),
new Text(title, style: Theme.of(context).textTheme.caption)
]
)
);
}
}
class GalleryDrawer extends StatelessWidget {
GalleryDrawer({ Key key }) : super(key: key);
void _changeTheme(BuildContext context, bool value) {
ComplexLayoutApp.of(context).lightTheme = value;
}
void _toggleAnimationSpeed(BuildContext context) {
ComplexLayoutApp.of(context).setState(() {
timeDilation = (timeDilation != 1.0) ? 1.0 : 5.0;
});
}
@override
Widget build(BuildContext context) {
return new Drawer(
child: new Block(
children: <Widget>[
new FancyDrawerHeader(),
new DrawerItem(
icon: Icons.brightness_5,
onPressed: () { _changeTheme(context, true); },
selected: ComplexLayoutApp.of(context).lightTheme,
child: new Row(
children: <Widget>[
new Flexible(child: new Text('Light')),
new Radio<bool>(
value: true,
groupValue: ComplexLayoutApp.of(context).lightTheme,
onChanged: (bool value) { _changeTheme(context, value); }
)
]
)
),
new DrawerItem(
icon: Icons.brightness_7,
onPressed: () { _changeTheme(context, false); },
selected: !ComplexLayoutApp.of(context).lightTheme,
child: new Row(
children: <Widget>[
new Flexible(child: new Text('Dark')),
new Radio<bool>(
value: false,
groupValue: ComplexLayoutApp.of(context).lightTheme,
onChanged: (bool value) { _changeTheme(context, value); }
)
]
)
),
new Divider(),
new DrawerItem(
icon: Icons.hourglass_empty,
selected: timeDilation != 1.0,
onPressed: () { _toggleAnimationSpeed(context); },
child: new Row(
children: <Widget>[
new Flexible(child: new Text('Animate Slowly')),
new Checkbox(
value: timeDilation != 1.0,
onChanged: (bool value) { _toggleAnimationSpeed(context); }
)
]
)
)
]
)
);
}
}
class FancyDrawerHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
decoration: new BoxDecoration(
backgroundColor: Colors.purple[500]
),
height: 200.0
);
}
}