
LazyBlock is intended as a replacement for MixedViewport. Rather than maintaining a table of all the observed child sizes (like MixedViewport), LazyBlock works by dead reckoning the location of the children based on the existing viewport. This approach makes it easier to resize children because LazyBlock doesn't cache any additional information that would need to be invalidated. This patch contains a first draft of LazyBlock that works in a simple usage scenario. Subsequent patches will replace ScrollableMixedWidgetList with LazyBlock and port the existing ScrollableMixedWidgetList tests over to LazyBlock. Related to #3075
645 lines
19 KiB
Dart
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"); },
|
|
items: <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
|
|
);
|
|
}
|
|
}
|