mirror of
https://github.com/flutter/flutter
synced 2024-10-02 14:34:22 +00:00
Add bad scroller benchmark (#110362)
This commit is contained in:
parent
114ebeac93
commit
f0ffc85698
20
.ci.yaml
20
.ci.yaml
|
@ -3119,6 +3119,26 @@ targets:
|
||||||
["devicelab", "ios", "mac"]
|
["devicelab", "ios", "mac"]
|
||||||
task_name: complex_layout_scroll_perf_ios__timeline_summary
|
task_name: complex_layout_scroll_perf_ios__timeline_summary
|
||||||
|
|
||||||
|
- name: Mac_ios complex_layout_scroll_perf_bad_ios__timeline_summary
|
||||||
|
recipe: devicelab/devicelab_drone
|
||||||
|
presubmit: false
|
||||||
|
bringup: true
|
||||||
|
timeout: 60
|
||||||
|
properties:
|
||||||
|
tags: >
|
||||||
|
["devicelab", "ios", "mac"]
|
||||||
|
task_name: complex_layout_scroll_perf_ios__timeline_summary
|
||||||
|
|
||||||
|
- name: Mac_ios complex_layout_scroll_perf_bad_impeller_ios__timeline_summary
|
||||||
|
recipe: devicelab/devicelab_drone
|
||||||
|
presubmit: false
|
||||||
|
bringup: true
|
||||||
|
timeout: 60
|
||||||
|
properties:
|
||||||
|
tags: >
|
||||||
|
["devicelab", "ios", "mac"]
|
||||||
|
task_name: complex_layout_scroll_perf_ios__timeline_summary
|
||||||
|
|
||||||
- name: Mac_ios external_ui_integration_test_ios
|
- name: Mac_ios external_ui_integration_test_ios
|
||||||
bringup: true # Flaky https://github.com/flutter/flutter/issues/106806
|
bringup: true # Flaky https://github.com/flutter/flutter/issues/106806
|
||||||
recipe: devicelab/devicelab_drone
|
recipe: devicelab/devicelab_drone
|
||||||
|
|
|
@ -150,6 +150,8 @@
|
||||||
/dev/devicelab/bin/tasks/complex_layout_ios__compile.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/complex_layout_ios__compile.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/complex_layout_ios__start_up.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/complex_layout_ios__start_up.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/complex_layout_scroll_perf_ios__timeline_summary.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/complex_layout_scroll_perf_ios__timeline_summary.dart @zanderso @flutter/engine
|
||||||
|
/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_ios__timeline_summary.dart @jonahwilliams @flutter/engine
|
||||||
|
/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_impeller_ios__timeline_summary.dart @jonahwilliams @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/cubic_bezier_perf_ios_sksl_warmup__timeline_summary.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/cubic_bezier_perf_ios_sksl_warmup__timeline_summary.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/external_ui_integration_test_ios.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/external_ui_integration_test_ios.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/flavors_test_ios.dart @jmagman @flutter/tool
|
/dev/devicelab/bin/tasks/flavors_test_ios.dart @jmagman @flutter/tool
|
||||||
|
|
|
@ -2,696 +2,11 @@
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/scheduler.dart' show timeDilation;
|
import 'src/app.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(
|
runApp(
|
||||||
const ComplexLayoutApp()
|
const ComplexLayoutApp()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ScrollMode { complex, tile }
|
|
||||||
|
|
||||||
class ComplexLayoutApp extends StatefulWidget {
|
|
||||||
const ComplexLayoutApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ComplexLayoutAppState createState() => ComplexLayoutAppState();
|
|
||||||
|
|
||||||
static ComplexLayoutAppState? of(BuildContext context) => context.findAncestorStateOfType<ComplexLayoutAppState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComplexLayoutAppState extends State<ComplexLayoutApp> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
theme: lightTheme ? ThemeData.light() : ThemeData.dark(),
|
|
||||||
title: 'Advanced Layout',
|
|
||||||
home: scrollMode == ScrollMode.complex ? const ComplexLayout() : const TileScrollLayout());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _lightTheme = true;
|
|
||||||
bool get lightTheme => _lightTheme;
|
|
||||||
set lightTheme(bool value) {
|
|
||||||
setState(() {
|
|
||||||
_lightTheme = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollMode _scrollMode = ScrollMode.complex;
|
|
||||||
ScrollMode get scrollMode => _scrollMode;
|
|
||||||
set scrollMode(ScrollMode mode) {
|
|
||||||
setState(() {
|
|
||||||
_scrollMode = mode;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleAnimationSpeed() {
|
|
||||||
setState(() {
|
|
||||||
timeDilation = (timeDilation != 1.0) ? 1.0 : 5.0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TileScrollLayout extends StatelessWidget {
|
|
||||||
const TileScrollLayout({ super.key });
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Tile Scrolling Layout')),
|
|
||||||
body: ListView.builder(
|
|
||||||
key: const Key('tiles-scroll'),
|
|
||||||
itemCount: 200,
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(5.0),
|
|
||||||
child: Material(
|
|
||||||
elevation: (index % 5 + 1).toDouble(),
|
|
||||||
color: Colors.white,
|
|
||||||
child: const IconBar(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
drawer: const GalleryDrawer(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComplexLayout extends StatefulWidget {
|
|
||||||
const ComplexLayout({ super.key });
|
|
||||||
|
|
||||||
@override
|
|
||||||
ComplexLayoutState createState() => ComplexLayoutState();
|
|
||||||
|
|
||||||
static ComplexLayoutState? of(BuildContext context) => context.findAncestorStateOfType<ComplexLayoutState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComplexLayoutState extends State<ComplexLayout> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Advanced Layout'),
|
|
||||||
actions: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.create),
|
|
||||||
tooltip: 'Search',
|
|
||||||
onPressed: () {
|
|
||||||
print('Pressed search');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const TopBarMenu(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
key: const Key('complex-scroll'), // this key is used by the driver test
|
|
||||||
controller: ScrollController(), // So that the scroll offset can be tracked
|
|
||||||
itemBuilder: (BuildContext context, int index) {
|
|
||||||
if (index.isEven) {
|
|
||||||
return FancyImageItem(index, key: PageStorageKey<int>(index));
|
|
||||||
} else {
|
|
||||||
return FancyGalleryItem(index, key: PageStorageKey<int>(index));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const BottomBar(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: const GalleryDrawer(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TopBarMenu extends StatelessWidget {
|
|
||||||
const TopBarMenu({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
onSelected: (String value) { print('Selected: $value'); },
|
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Friends',
|
|
||||||
child: MenuItemWithIcon(Icons.people, 'Friends', '5 new'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.event, 'Events', '12 upcoming'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.group, 'Groups', '14'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.image, 'Pictures', '12'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Friends',
|
|
||||||
child: MenuItemWithIcon(Icons.people, 'Friends', '5'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.event, 'Events', '12'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.group, 'Groups', '14'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.image, 'Pictures', '12'),
|
|
||||||
),
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
value: 'Events',
|
|
||||||
child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MenuItemWithIcon extends StatelessWidget {
|
|
||||||
const MenuItemWithIcon(this.icon, this.title, this.subtitle, {super.key});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Icon(icon),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
|
||||||
child: Text(title),
|
|
||||||
),
|
|
||||||
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FancyImageItem extends StatelessWidget {
|
|
||||||
const FancyImageItem(this.index, {super.key});
|
|
||||||
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListBody(
|
|
||||||
children: <Widget>[
|
|
||||||
UserHeader('Ali Connors $index'),
|
|
||||||
const ItemDescription(),
|
|
||||||
const ItemImageBox(),
|
|
||||||
const InfoBar(),
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: Divider(),
|
|
||||||
),
|
|
||||||
const IconBar(),
|
|
||||||
const FatDivider(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FancyGalleryItem extends StatelessWidget {
|
|
||||||
const FancyGalleryItem(this.index, {super.key});
|
|
||||||
|
|
||||||
final int index;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListBody(
|
|
||||||
children: <Widget>[
|
|
||||||
const UserHeader('Ali Connors'),
|
|
||||||
ItemGalleryBox(index),
|
|
||||||
const InfoBar(),
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: Divider(),
|
|
||||||
),
|
|
||||||
const IconBar(),
|
|
||||||
const FatDivider(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InfoBar extends StatelessWidget {
|
|
||||||
const InfoBar({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
const MiniIconWithText(Icons.thumb_up, '42'),
|
|
||||||
Text('3 Comments', style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IconBar extends StatelessWidget {
|
|
||||||
const IconBar({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: const <Widget>[
|
|
||||||
IconWithText(Icons.thumb_up, 'Like'),
|
|
||||||
IconWithText(Icons.comment, 'Comment'),
|
|
||||||
IconWithText(Icons.share, 'Share'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IconWithText extends StatelessWidget {
|
|
||||||
const IconWithText(this.icon, this.title, {super.key});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(icon),
|
|
||||||
onPressed: () { print('Pressed $title button'); },
|
|
||||||
),
|
|
||||||
Text(title),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MiniIconWithText extends StatelessWidget {
|
|
||||||
const MiniIconWithText(this.icon, this.title, {super.key});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
child: Container(
|
|
||||||
width: 16.0,
|
|
||||||
height: 16.0,
|
|
||||||
decoration: ShapeDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
shape: const CircleBorder(),
|
|
||||||
),
|
|
||||||
child: Icon(icon, color: Colors.white, size: 12.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(title, style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FatDivider extends StatelessWidget {
|
|
||||||
const FatDivider({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: 8.0,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UserHeader extends StatelessWidget {
|
|
||||||
const UserHeader(this.userName, {super.key});
|
|
||||||
|
|
||||||
final String userName;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(right: 8.0),
|
|
||||||
child: Image(
|
|
||||||
image: AssetImage('packages/flutter_gallery_assets/people/square/ali.png'),
|
|
||||||
width: 32.0,
|
|
||||||
height: 32.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
RichText(text: TextSpan(
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(text: userName, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
const TextSpan(text: ' shared a new '),
|
|
||||||
const TextSpan(text: 'photo', style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
Text('Yesterday at 11:55 • ', style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
Icon(Icons.people, size: 16.0, color: Theme.of(context).textTheme.bodySmall!.color),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TopBarMenu(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ItemDescription extends StatelessWidget {
|
|
||||||
const ItemDescription({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ItemImageBox extends StatelessWidget {
|
|
||||||
const ItemImageBox({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Card(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
height: 230.0,
|
|
||||||
child: Image(
|
|
||||||
image: AssetImage('packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png')
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Theme(
|
|
||||||
data: ThemeData.dark(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onPressed: () { print('Pressed edit button'); },
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.zoom_in),
|
|
||||||
onPressed: () { print('Pressed zoom button'); },
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 4.0,
|
|
||||||
left: 4.0,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black54,
|
|
||||||
borderRadius: BorderRadius.circular(2.0),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(4.0),
|
|
||||||
child: RichText(
|
|
||||||
text: const TextSpan(
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(
|
|
||||||
text: 'Photo by '
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
text: 'Chris Godley',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
,
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
Text('Artisans of Southern India', style: Theme.of(context).textTheme.bodyLarge),
|
|
||||||
Text('Silk Spinners', style: Theme.of(context).textTheme.bodyMedium),
|
|
||||||
Text('Sivaganga, Tamil Nadu', style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ItemGalleryBox extends StatelessWidget {
|
|
||||||
const ItemGalleryBox(this.index, {super.key});
|
|
||||||
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final List<String> tabNames = <String>[
|
|
||||||
'A', 'B', 'C', 'D',
|
|
||||||
];
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 200.0,
|
|
||||||
child: DefaultTabController(
|
|
||||||
length: tabNames.length,
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
children: tabNames.map<Widget>((String tabName) {
|
|
||||||
return Container(
|
|
||||||
key: PageStorageKey<String>(tabName),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Card(
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
child: Center(
|
|
||||||
child: Text(tabName, style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: () { print('Pressed share'); },
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.event),
|
|
||||||
onPressed: () { print('Pressed event'); },
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Text('This is item $tabName'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TabPageSelector(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BottomBar extends StatelessWidget {
|
|
||||||
const BottomBar({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
top: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: const <Widget>[
|
|
||||||
BottomBarButton(Icons.new_releases, 'News'),
|
|
||||||
BottomBarButton(Icons.people, 'Requests'),
|
|
||||||
BottomBarButton(Icons.chat, 'Messenger'),
|
|
||||||
BottomBarButton(Icons.bookmark, 'Bookmark'),
|
|
||||||
BottomBarButton(Icons.alarm, 'Alarm'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BottomBarButton extends StatelessWidget {
|
|
||||||
const BottomBarButton(this.icon, this.title, {super.key});
|
|
||||||
|
|
||||||
final IconData icon;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(icon),
|
|
||||||
onPressed: () { print('Pressed: $title'); },
|
|
||||||
),
|
|
||||||
Text(title, style: Theme.of(context).textTheme.bodySmall),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GalleryDrawer extends StatelessWidget {
|
|
||||||
const GalleryDrawer({ super.key });
|
|
||||||
|
|
||||||
void _changeTheme(BuildContext context, bool value) {
|
|
||||||
ComplexLayoutApp.of(context)?.lightTheme = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _changeScrollMode(BuildContext context, ScrollMode mode) {
|
|
||||||
ComplexLayoutApp.of(context)?.scrollMode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final ScrollMode currentMode = ComplexLayoutApp.of(context)!.scrollMode;
|
|
||||||
return Drawer(
|
|
||||||
// Note: for real apps, see the Gallery material Drawer demo. More
|
|
||||||
// typically, a drawer would have a fixed header with a scrolling body
|
|
||||||
// below it.
|
|
||||||
child: ListView(
|
|
||||||
key: const PageStorageKey<String>('gallery-drawer'),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
children: <Widget>[
|
|
||||||
const FancyDrawerHeader(),
|
|
||||||
ListTile(
|
|
||||||
key: const Key('scroll-switcher'),
|
|
||||||
title: const Text('Scroll Mode'),
|
|
||||||
onTap: () {
|
|
||||||
_changeScrollMode(context, currentMode == ScrollMode.complex ? ScrollMode.tile : ScrollMode.complex);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
trailing: Text(currentMode == ScrollMode.complex ? 'Tile' : 'Complex'),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.brightness_5),
|
|
||||||
title: const Text('Light'),
|
|
||||||
onTap: () { _changeTheme(context, true); },
|
|
||||||
selected: ComplexLayoutApp.of(context)!.lightTheme,
|
|
||||||
trailing: Radio<bool>(
|
|
||||||
value: true,
|
|
||||||
groupValue: ComplexLayoutApp.of(context)!.lightTheme,
|
|
||||||
onChanged: (bool? value) { _changeTheme(context, value!); },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.brightness_7),
|
|
||||||
title: const Text('Dark'),
|
|
||||||
onTap: () { _changeTheme(context, false); },
|
|
||||||
selected: !ComplexLayoutApp.of(context)!.lightTheme,
|
|
||||||
trailing: Radio<bool>(
|
|
||||||
value: false,
|
|
||||||
groupValue: ComplexLayoutApp.of(context)!.lightTheme,
|
|
||||||
onChanged: (bool? value) { _changeTheme(context, value!); },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.hourglass_empty),
|
|
||||||
title: const Text('Animate Slowly'),
|
|
||||||
selected: timeDilation != 1.0,
|
|
||||||
onTap: () { ComplexLayoutApp.of(context)!.toggleAnimationSpeed(); },
|
|
||||||
trailing: Checkbox(
|
|
||||||
value: timeDilation != 1.0,
|
|
||||||
onChanged: (bool? value) { ComplexLayoutApp.of(context)!.toggleAnimationSpeed(); },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FancyDrawerHeader extends StatelessWidget {
|
|
||||||
const FancyDrawerHeader({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.purple,
|
|
||||||
height: 200.0,
|
|
||||||
child: const SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: Placeholder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
12
dev/benchmarks/complex_layout/lib/main_bad.dart
Normal file
12
dev/benchmarks/complex_layout/lib/main_bad.dart
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2014 The Flutter 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/widgets.dart';
|
||||||
|
import 'src/app.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(
|
||||||
|
const ComplexLayoutApp(badScroll: true)
|
||||||
|
);
|
||||||
|
}
|
698
dev/benchmarks/complex_layout/lib/src/app.dart
Normal file
698
dev/benchmarks/complex_layout/lib/src/app.dart
Normal file
|
@ -0,0 +1,698 @@
|
||||||
|
// Copyright 2014 The Flutter 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;
|
||||||
|
|
||||||
|
enum ScrollMode { complex, tile }
|
||||||
|
|
||||||
|
class ComplexLayoutApp extends StatefulWidget {
|
||||||
|
const ComplexLayoutApp({super.key, this.badScroll = false});
|
||||||
|
|
||||||
|
final bool badScroll;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ComplexLayoutAppState createState() => ComplexLayoutAppState();
|
||||||
|
|
||||||
|
static ComplexLayoutAppState? of(BuildContext context) => context.findAncestorStateOfType<ComplexLayoutAppState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComplexLayoutAppState extends State<ComplexLayoutApp> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: lightTheme ? ThemeData.light() : ThemeData.dark(),
|
||||||
|
title: 'Advanced Layout',
|
||||||
|
home: scrollMode == ScrollMode.complex ? ComplexLayout(badScroll: widget.badScroll) : const TileScrollLayout());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _lightTheme = true;
|
||||||
|
bool get lightTheme => _lightTheme;
|
||||||
|
set lightTheme(bool value) {
|
||||||
|
setState(() {
|
||||||
|
_lightTheme = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollMode _scrollMode = ScrollMode.complex;
|
||||||
|
ScrollMode get scrollMode => _scrollMode;
|
||||||
|
set scrollMode(ScrollMode mode) {
|
||||||
|
setState(() {
|
||||||
|
_scrollMode = mode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleAnimationSpeed() {
|
||||||
|
setState(() {
|
||||||
|
timeDilation = (timeDilation != 1.0) ? 1.0 : 5.0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TileScrollLayout extends StatelessWidget {
|
||||||
|
const TileScrollLayout({ super.key });
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Tile Scrolling Layout')),
|
||||||
|
body: ListView.builder(
|
||||||
|
key: const Key('tiles-scroll'),
|
||||||
|
itemCount: 200,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: Material(
|
||||||
|
elevation: (index % 5 + 1).toDouble(),
|
||||||
|
color: Colors.white,
|
||||||
|
child: const IconBar(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
drawer: const GalleryDrawer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComplexLayout extends StatefulWidget {
|
||||||
|
const ComplexLayout({ super.key, required this.badScroll });
|
||||||
|
|
||||||
|
final bool badScroll;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ComplexLayoutState createState() => ComplexLayoutState();
|
||||||
|
|
||||||
|
static ComplexLayoutState? of(BuildContext context) => context.findAncestorStateOfType<ComplexLayoutState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComplexLayoutState extends State<ComplexLayout> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
print(widget.badScroll);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Advanced Layout'),
|
||||||
|
actions: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.create),
|
||||||
|
tooltip: 'Search',
|
||||||
|
onPressed: () {
|
||||||
|
print('Pressed search');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const TopBarMenu(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
key: const Key('complex-scroll'), // this key is used by the driver test
|
||||||
|
controller: ScrollController(), // So that the scroll offset can be tracked
|
||||||
|
itemCount: widget.badScroll ? 500 : null,
|
||||||
|
shrinkWrap: widget.badScroll,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
if (index.isEven) {
|
||||||
|
return FancyImageItem(index, key: PageStorageKey<int>(index));
|
||||||
|
} else {
|
||||||
|
return FancyGalleryItem(index, key: PageStorageKey<int>(index));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const BottomBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer: const GalleryDrawer(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopBarMenu extends StatelessWidget {
|
||||||
|
const TopBarMenu({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
onSelected: (String value) { print('Selected: $value'); },
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Friends',
|
||||||
|
child: MenuItemWithIcon(Icons.people, 'Friends', '5 new'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.event, 'Events', '12 upcoming'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.group, 'Groups', '14'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.image, 'Pictures', '12'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Friends',
|
||||||
|
child: MenuItemWithIcon(Icons.people, 'Friends', '5'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.event, 'Events', '12'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.group, 'Groups', '14'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.image, 'Pictures', '12'),
|
||||||
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'Events',
|
||||||
|
child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuItemWithIcon extends StatelessWidget {
|
||||||
|
const MenuItemWithIcon(this.icon, this.title, this.subtitle, {super.key});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(icon),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
|
||||||
|
child: Text(title),
|
||||||
|
),
|
||||||
|
Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FancyImageItem extends StatelessWidget {
|
||||||
|
const FancyImageItem(this.index, {super.key});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
UserHeader('Ali Connors $index'),
|
||||||
|
const ItemDescription(),
|
||||||
|
const ItemImageBox(),
|
||||||
|
const InfoBar(),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
const IconBar(),
|
||||||
|
const FatDivider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FancyGalleryItem extends StatelessWidget {
|
||||||
|
const FancyGalleryItem(this.index, {super.key});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
const UserHeader('Ali Connors'),
|
||||||
|
ItemGalleryBox(index),
|
||||||
|
const InfoBar(),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Divider(),
|
||||||
|
),
|
||||||
|
const IconBar(),
|
||||||
|
const FatDivider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InfoBar extends StatelessWidget {
|
||||||
|
const InfoBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
const MiniIconWithText(Icons.thumb_up, '42'),
|
||||||
|
Text('3 Comments', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconBar extends StatelessWidget {
|
||||||
|
const IconBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const <Widget>[
|
||||||
|
IconWithText(Icons.thumb_up, 'Like'),
|
||||||
|
IconWithText(Icons.comment, 'Comment'),
|
||||||
|
IconWithText(Icons.share, 'Share'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IconWithText extends StatelessWidget {
|
||||||
|
const IconWithText(this.icon, this.title, {super.key});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(icon),
|
||||||
|
onPressed: () { print('Pressed $title button'); },
|
||||||
|
),
|
||||||
|
Text(title),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MiniIconWithText extends StatelessWidget {
|
||||||
|
const MiniIconWithText(this.icon, this.title, {super.key});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: Container(
|
||||||
|
width: 16.0,
|
||||||
|
height: 16.0,
|
||||||
|
decoration: ShapeDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FatDivider extends StatelessWidget {
|
||||||
|
const FatDivider({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 8.0,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserHeader extends StatelessWidget {
|
||||||
|
const UserHeader(this.userName, {super.key});
|
||||||
|
|
||||||
|
final String userName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8.0),
|
||||||
|
child: Image(
|
||||||
|
image: AssetImage('packages/flutter_gallery_assets/people/square/ali.png'),
|
||||||
|
width: 32.0,
|
||||||
|
height: 32.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
RichText(text: TextSpan(
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(text: userName, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
const TextSpan(text: ' shared a new '),
|
||||||
|
const TextSpan(text: 'photo', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Yesterday at 11:55 • ', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
Icon(Icons.people, size: 16.0, color: Theme.of(context).textTheme.bodySmall!.color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TopBarMenu(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ItemDescription extends StatelessWidget {
|
||||||
|
const ItemDescription({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Text('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ItemImageBox extends StatelessWidget {
|
||||||
|
const ItemImageBox({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: 230.0,
|
||||||
|
child: Image(
|
||||||
|
image: AssetImage('packages/flutter_gallery_assets/places/india_chettinad_silk_maker.png')
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Theme(
|
||||||
|
data: ThemeData.dark(),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
onPressed: () { print('Pressed edit button'); },
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.zoom_in),
|
||||||
|
onPressed: () { print('Pressed zoom button'); },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 4.0,
|
||||||
|
left: 4.0,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(2.0),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(4.0),
|
||||||
|
child: RichText(
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: 'Photo by '
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
text: 'Chris Godley',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
,
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Artisans of Southern India', style: Theme.of(context).textTheme.bodyLarge),
|
||||||
|
Text('Silk Spinners', style: Theme.of(context).textTheme.bodyMedium),
|
||||||
|
Text('Sivaganga, Tamil Nadu', style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ItemGalleryBox extends StatelessWidget {
|
||||||
|
const ItemGalleryBox(this.index, {super.key});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final List<String> tabNames = <String>[
|
||||||
|
'A', 'B', 'C', 'D',
|
||||||
|
];
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 200.0,
|
||||||
|
child: DefaultTabController(
|
||||||
|
length: tabNames.length,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: tabNames.map<Widget>((String tabName) {
|
||||||
|
return Container(
|
||||||
|
key: PageStorageKey<String>(tabName),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
child: Center(
|
||||||
|
child: Text(tabName, style: Theme.of(context).textTheme.headlineSmall!.copyWith(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.share),
|
||||||
|
onPressed: () { print('Pressed share'); },
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.event),
|
||||||
|
onPressed: () { print('Pressed event'); },
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text('This is item $tabName'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TabPageSelector(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomBar extends StatelessWidget {
|
||||||
|
const BottomBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: const <Widget>[
|
||||||
|
BottomBarButton(Icons.new_releases, 'News'),
|
||||||
|
BottomBarButton(Icons.people, 'Requests'),
|
||||||
|
BottomBarButton(Icons.chat, 'Messenger'),
|
||||||
|
BottomBarButton(Icons.bookmark, 'Bookmark'),
|
||||||
|
BottomBarButton(Icons.alarm, 'Alarm'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomBarButton extends StatelessWidget {
|
||||||
|
const BottomBarButton(this.icon, this.title, {super.key});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(icon),
|
||||||
|
onPressed: () { print('Pressed: $title'); },
|
||||||
|
),
|
||||||
|
Text(title, style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryDrawer extends StatelessWidget {
|
||||||
|
const GalleryDrawer({ super.key });
|
||||||
|
|
||||||
|
void _changeTheme(BuildContext context, bool value) {
|
||||||
|
ComplexLayoutApp.of(context)?.lightTheme = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _changeScrollMode(BuildContext context, ScrollMode mode) {
|
||||||
|
ComplexLayoutApp.of(context)?.scrollMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ScrollMode currentMode = ComplexLayoutApp.of(context)!.scrollMode;
|
||||||
|
return Drawer(
|
||||||
|
// Note: for real apps, see the Gallery material Drawer demo. More
|
||||||
|
// typically, a drawer would have a fixed header with a scrolling body
|
||||||
|
// below it.
|
||||||
|
child: ListView(
|
||||||
|
key: const PageStorageKey<String>('gallery-drawer'),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
children: <Widget>[
|
||||||
|
const FancyDrawerHeader(),
|
||||||
|
ListTile(
|
||||||
|
key: const Key('scroll-switcher'),
|
||||||
|
title: const Text('Scroll Mode'),
|
||||||
|
onTap: () {
|
||||||
|
_changeScrollMode(context, currentMode == ScrollMode.complex ? ScrollMode.tile : ScrollMode.complex);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
trailing: Text(currentMode == ScrollMode.complex ? 'Tile' : 'Complex'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.brightness_5),
|
||||||
|
title: const Text('Light'),
|
||||||
|
onTap: () { _changeTheme(context, true); },
|
||||||
|
selected: ComplexLayoutApp.of(context)!.lightTheme,
|
||||||
|
trailing: Radio<bool>(
|
||||||
|
value: true,
|
||||||
|
groupValue: ComplexLayoutApp.of(context)!.lightTheme,
|
||||||
|
onChanged: (bool? value) { _changeTheme(context, value!); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.brightness_7),
|
||||||
|
title: const Text('Dark'),
|
||||||
|
onTap: () { _changeTheme(context, false); },
|
||||||
|
selected: !ComplexLayoutApp.of(context)!.lightTheme,
|
||||||
|
trailing: Radio<bool>(
|
||||||
|
value: false,
|
||||||
|
groupValue: ComplexLayoutApp.of(context)!.lightTheme,
|
||||||
|
onChanged: (bool? value) { _changeTheme(context, value!); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.hourglass_empty),
|
||||||
|
title: const Text('Animate Slowly'),
|
||||||
|
selected: timeDilation != 1.0,
|
||||||
|
onTap: () { ComplexLayoutApp.of(context)!.toggleAnimationSpeed(); },
|
||||||
|
trailing: Checkbox(
|
||||||
|
value: timeDilation != 1.0,
|
||||||
|
onChanged: (bool? value) { ComplexLayoutApp.of(context)!.toggleAnimationSpeed(); },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FancyDrawerHeader extends StatelessWidget {
|
||||||
|
const FancyDrawerHeader({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.purple,
|
||||||
|
height: 200.0,
|
||||||
|
child: const SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: Placeholder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2014 The Flutter 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:complex_layout/main_bad.dart' as app;
|
||||||
|
import 'package:flutter_driver/driver_extension.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
enableFlutterDriverExtension();
|
||||||
|
app.main();
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright 2014 The Flutter 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 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
|
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('scrolling performance test', () {
|
||||||
|
late FlutterDriver driver;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
driver = await FlutterDriver.connect();
|
||||||
|
|
||||||
|
await driver.waitUntilFirstFrameRasterized();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
if (driver != null) {
|
||||||
|
driver.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> testScrollPerf(String listKey, String summaryName) async {
|
||||||
|
// The slight initial delay avoids starting the timing during a
|
||||||
|
// period of increased load on the device. Without this delay, the
|
||||||
|
// benchmark has greater noise.
|
||||||
|
// See: https://github.com/flutter/flutter/issues/19434
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 250));
|
||||||
|
|
||||||
|
final Timeline timeline = await driver.traceAction(() async {
|
||||||
|
// Find the scrollable stock list
|
||||||
|
final SerializableFinder list = find.byValueKey(listKey);
|
||||||
|
expect(list, isNotNull);
|
||||||
|
|
||||||
|
// Scroll down
|
||||||
|
for (int i = 0; i < 5; i += 1) {
|
||||||
|
await driver.scroll(list, 0.0, -300.0, const Duration(milliseconds: 300));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll up
|
||||||
|
for (int i = 0; i < 5; i += 1) {
|
||||||
|
await driver.scroll(list, 0.0, 300.0, const Duration(milliseconds: 300));
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimelineSummary summary = TimelineSummary.summarize(timeline);
|
||||||
|
await summary.writeTimelineToFile(summaryName, pretty: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('complex_layout_scroll_perf', () async {
|
||||||
|
await testScrollPerf('complex-scroll', 'complex_layout_scroll_perf');
|
||||||
|
}, timeout: Timeout.none);
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:complex_layout/main.dart';
|
import 'package:complex_layout/src/app.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2014 The Flutter 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_devicelab/framework/devices.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.ios;
|
||||||
|
await task(createComplexLayoutScrollPerfTest(badScroll: true, enableImpeller: true));
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright 2014 The Flutter 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_devicelab/framework/devices.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.ios;
|
||||||
|
await task(createComplexLayoutScrollPerfTest(badScroll: true));
|
||||||
|
}
|
|
@ -23,12 +23,19 @@ String _testOutputDirectory(String testDirectory) {
|
||||||
return Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '$testDirectory/build';
|
return Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? '$testDirectory/build';
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFunction createComplexLayoutScrollPerfTest({bool measureCpuGpu = true}) {
|
TaskFunction createComplexLayoutScrollPerfTest({
|
||||||
|
bool measureCpuGpu = true,
|
||||||
|
bool badScroll = false,
|
||||||
|
bool enableImpeller = false,
|
||||||
|
}) {
|
||||||
return PerfTest(
|
return PerfTest(
|
||||||
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
'${flutterDirectory.path}/dev/benchmarks/complex_layout',
|
||||||
'test_driver/scroll_perf.dart',
|
badScroll
|
||||||
|
? 'test_driver/scroll_perf_bad.dart'
|
||||||
|
: 'test_driver/scroll_perf.dart',
|
||||||
'complex_layout_scroll_perf',
|
'complex_layout_scroll_perf',
|
||||||
measureCpuGpu: measureCpuGpu,
|
measureCpuGpu: measureCpuGpu,
|
||||||
|
enableImpeller: enableImpeller,
|
||||||
).run;
|
).run;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue