diff --git a/.ci.yaml b/.ci.yaml index 1dd0dd84ba0..6ebd3b73a29 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -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 diff --git a/TESTOWNERS b/TESTOWNERS index b1c6013dd9f..de8b14cc3e0 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -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 diff --git a/dev/benchmarks/complex_layout/lib/main.dart b/dev/benchmarks/complex_layout/lib/main.dart index 9d61d636720..ac618e58c14 100644 --- a/dev/benchmarks/complex_layout/lib/main.dart +++ b/dev/benchmarks/complex_layout/lib/main.dart @@ -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(); -} - -class ComplexLayoutAppState extends State { - @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(); -} - -class ComplexLayoutState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Advanced Layout'), - actions: [ - IconButton( - icon: const Icon(Icons.create), - tooltip: 'Search', - onPressed: () { - print('Pressed search'); - }, - ), - const TopBarMenu(), - ], - ), - body: Column( - children: [ - 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(index)); - } else { - return FancyGalleryItem(index, key: PageStorageKey(index)); - } - }, - ), - ), - const BottomBar(), - ], - ), - drawer: const GalleryDrawer(), - ); - } -} - -class TopBarMenu extends StatelessWidget { - const TopBarMenu({super.key}); - - @override - Widget build(BuildContext context) { - return PopupMenuButton( - onSelected: (String value) { print('Selected: $value'); }, - itemBuilder: (BuildContext context) => >[ - const PopupMenuItem( - value: 'Friends', - child: MenuItemWithIcon(Icons.people, 'Friends', '5 new'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.event, 'Events', '12 upcoming'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.group, 'Groups', '14'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.image, 'Pictures', '12'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'), - ), - const PopupMenuItem( - value: 'Friends', - child: MenuItemWithIcon(Icons.people, 'Friends', '5'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.event, 'Events', '12'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.group, 'Groups', '14'), - ), - const PopupMenuItem( - value: 'Events', - child: MenuItemWithIcon(Icons.image, 'Pictures', '12'), - ), - const PopupMenuItem( - 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: [ - 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: [ - 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: [ - 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: [ - 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 [ - 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: [ - 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: [ - 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: [ - 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: [ - RichText(text: TextSpan( - style: Theme.of(context).textTheme.bodyMedium, - children: [ - 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: [ - 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: [ - Stack( - children: [ - 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: [ - 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( - 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: [ - 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 tabNames = [ - 'A', 'B', 'C', 'D', - ]; - - return SizedBox( - height: 200.0, - child: DefaultTabController( - length: tabNames.length, - child: Column( - children: [ - Expanded( - child: TabBarView( - children: tabNames.map((String tabName) { - return Container( - key: PageStorageKey(tabName), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - child: Column( - children: [ - 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: [ - 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 [ - 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: [ - 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('gallery-drawer'), - padding: EdgeInsets.zero, - children: [ - 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( - 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( - 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(), - ), - ); - } -} diff --git a/dev/benchmarks/complex_layout/lib/main_bad.dart b/dev/benchmarks/complex_layout/lib/main_bad.dart new file mode 100644 index 00000000000..5b2d5f627c0 --- /dev/null +++ b/dev/benchmarks/complex_layout/lib/main_bad.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/widgets.dart'; +import 'src/app.dart'; + +void main() { + runApp( + const ComplexLayoutApp(badScroll: true) + ); +} diff --git a/dev/benchmarks/complex_layout/lib/src/app.dart b/dev/benchmarks/complex_layout/lib/src/app.dart new file mode 100644 index 00000000000..d3950e4b745 --- /dev/null +++ b/dev/benchmarks/complex_layout/lib/src/app.dart @@ -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(); +} + +class ComplexLayoutAppState extends State { + @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(); +} + +class ComplexLayoutState extends State { + @override + Widget build(BuildContext context) { + print(widget.badScroll); + return Scaffold( + appBar: AppBar( + title: const Text('Advanced Layout'), + actions: [ + IconButton( + icon: const Icon(Icons.create), + tooltip: 'Search', + onPressed: () { + print('Pressed search'); + }, + ), + const TopBarMenu(), + ], + ), + body: Column( + children: [ + 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(index)); + } else { + return FancyGalleryItem(index, key: PageStorageKey(index)); + } + }, + ), + ), + const BottomBar(), + ], + ), + drawer: const GalleryDrawer(), + ); + } +} + +class TopBarMenu extends StatelessWidget { + const TopBarMenu({super.key}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + onSelected: (String value) { print('Selected: $value'); }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'Friends', + child: MenuItemWithIcon(Icons.people, 'Friends', '5 new'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.event, 'Events', '12 upcoming'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.group, 'Groups', '14'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.image, 'Pictures', '12'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.near_me, 'Nearby', '33'), + ), + const PopupMenuItem( + value: 'Friends', + child: MenuItemWithIcon(Icons.people, 'Friends', '5'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.event, 'Events', '12'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.group, 'Groups', '14'), + ), + const PopupMenuItem( + value: 'Events', + child: MenuItemWithIcon(Icons.image, 'Pictures', '12'), + ), + const PopupMenuItem( + 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: [ + 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: [ + 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: [ + 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: [ + 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 [ + 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: [ + 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: [ + 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: [ + 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: [ + RichText(text: TextSpan( + style: Theme.of(context).textTheme.bodyMedium, + children: [ + 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: [ + 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: [ + Stack( + children: [ + 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: [ + 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( + 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: [ + 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 tabNames = [ + 'A', 'B', 'C', 'D', + ]; + + return SizedBox( + height: 200.0, + child: DefaultTabController( + length: tabNames.length, + child: Column( + children: [ + Expanded( + child: TabBarView( + children: tabNames.map((String tabName) { + return Container( + key: PageStorageKey(tabName), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + child: Column( + children: [ + 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: [ + 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 [ + 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: [ + 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('gallery-drawer'), + padding: EdgeInsets.zero, + children: [ + 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( + 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( + 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(), + ), + ); + } +} diff --git a/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad.dart b/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad.dart new file mode 100644 index 00000000000..41ccc9ff694 --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad.dart @@ -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(); +} diff --git a/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart b/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart new file mode 100644 index 00000000000..106b32f41cf --- /dev/null +++ b/dev/benchmarks/complex_layout/test_driver/scroll_perf_bad_test.dart @@ -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 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.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.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.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); + }); +} diff --git a/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart b/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart index 7e7426be9a5..b06849c8a5f 100644 --- a/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart +++ b/dev/benchmarks/complex_layout/test_memory/scroll_perf.dart @@ -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'; diff --git a/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_impeller_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_impeller_ios__timeline_summary.dart new file mode 100644 index 00000000000..996d04b728e --- /dev/null +++ b/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_impeller_ios__timeline_summary.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 main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createComplexLayoutScrollPerfTest(badScroll: true, enableImpeller: true)); +} diff --git a/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_ios__timeline_summary.dart b/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_ios__timeline_summary.dart new file mode 100644 index 00000000000..640441e12b5 --- /dev/null +++ b/dev/devicelab/bin/tasks/complex_layout_scroll_perf_bad_ios__timeline_summary.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 main() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + await task(createComplexLayoutScrollPerfTest(badScroll: true)); +} diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart index 34863ebd1e4..88d84e0e72d 100644 --- a/dev/devicelab/lib/tasks/perf_tests.dart +++ b/dev/devicelab/lib/tasks/perf_tests.dart @@ -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; }