Add bad scroller benchmark (#110362)

This commit is contained in:
Jonah Williams 2022-08-26 11:50:07 -07:00 committed by GitHub
parent 114ebeac93
commit f0ffc85698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 838 additions and 690 deletions

View File

@ -3119,6 +3119,26 @@ targets:
["devicelab", "ios", "mac"]
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
bringup: true # Flaky https://github.com/flutter/flutter/issues/106806
recipe: devicelab/devicelab_drone

View File

@ -150,6 +150,8 @@
/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_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/external_ui_integration_test_ios.dart @zanderso @flutter/engine
/dev/devicelab/bin/tasks/flavors_test_ios.dart @jmagman @flutter/tool

View File

@ -2,696 +2,11 @@
// 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;
import 'package:flutter/widgets.dart';
import 'src/app.dart';
void main() {
runApp(
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(),
),
);
}
}

View 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)
);
}

View 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(),
),
);
}
}

View File

@ -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();
}

View File

@ -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);
});
}

View File

@ -4,7 +4,7 @@
import 'dart:async';
import 'package:complex_layout/main.dart';
import 'package:complex_layout/src/app.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';

View 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_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));
}

View 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_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));
}

View File

@ -23,12 +23,19 @@ String _testOutputDirectory(String testDirectory) {
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(
'${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',
measureCpuGpu: measureCpuGpu,
enableImpeller: enableImpeller,
).run;
}