From 43a8b6af53cac91f569bf73d91416bbc2e643357 Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" <98614782+auto-submit[bot]@users.noreply.github.com> Date: Wed, 26 Jun 2024 19:47:18 +0000 Subject: [PATCH] Reverts "Remove `dual_screen` from `new_gallery` integration test (#150808)" (#150871) Reverts: flutter/flutter#150808 Initiated by: gmackall Reason for reverting: Causing the new_gallery tests to hang. I can repro now though, so should be able to find a fix shortly Original PR Author: gmackall Reviewed By: {christopherfujino, johnmccutchan, jtmcdole, jonahwilliams} This change reverts the following previous change: Removes the `dual_screen` package from `new_gallery`. Unblocks the fourth attempt to land https://github.com/flutter/engine/pull/53001. --- .../new_gallery/lib/data/demos.dart | 50 ++++ .../lib/demos/reference/two_pane_demo.dart | 226 ++++++++++++++++++ .../new_gallery/lib/layout/adaptive.dart | 20 +- .../new_gallery/lib/main.dart | 4 +- .../new_gallery/lib/pages/demo.dart | 15 +- .../new_gallery/lib/pages/home.dart | 41 ++++ .../new_gallery/lib/pages/splash.dart | 67 +++++- .../new_gallery/lib/routes.dart | 40 +++- .../new_gallery/pubspec.yaml | 3 +- 9 files changed, 442 insertions(+), 24 deletions(-) create mode 100644 dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart diff --git a/dev/integration_tests/new_gallery/lib/data/demos.dart b/dev/integration_tests/new_gallery/lib/data/demos.dart index c5fcabdbb08f..3f63904662b5 100644 --- a/dev/integration_tests/new_gallery/lib/data/demos.dart +++ b/dev/integration_tests/new_gallery/lib/data/demos.dart @@ -26,6 +26,8 @@ import '../demos/reference/motion_demo_shared_y_axis_transition.dart'; import '../demos/reference/motion_demo_shared_z_axis_transition.dart'; import '../demos/reference/transformations_demo.dart' deferred as transformations_demo; +import '../demos/reference/two_pane_demo.dart' + deferred as twopane_demo; import '../demos/reference/typography_demo.dart' deferred as typography; import '../gallery_localizations.dart'; @@ -1156,6 +1158,54 @@ class Demos { static List otherDemos(GalleryLocalizations localizations) { return [ + GalleryDemo( + title: localizations.demoTwoPaneTitle, + icon: GalleryIcons.bottomSheetPersistent, + slug: 'two-pane', + subtitle: localizations.demoTwoPaneSubtitle, + configurations: [ + GalleryDemoConfiguration( + title: localizations.demoTwoPaneFoldableLabel, + description: localizations.demoTwoPaneFoldableDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.foldable, + restorationId: 'two_pane_foldable', + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTwoPaneTabletLabel, + description: localizations.demoTwoPaneTabletDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.tablet, + restorationId: 'two_pane_tablet', + ), + ), + ), + GalleryDemoConfiguration( + title: localizations.demoTwoPaneSmallScreenLabel, + description: localizations.demoTwoPaneSmallScreenDescription, + documentationUrl: + 'https://pub.dev/documentation/dual_screen/latest/dual_screen/TwoPane-class.html', + buildRoute: (_) => DeferredWidget( + twopane_demo.loadLibrary, + () => twopane_demo.TwoPaneDemo( + type: twopane_demo.TwoPaneDemoType.smallScreen, + restorationId: 'two_pane_single', + ), + ), + ), + ], + category: GalleryDemoCategory.other, + ), GalleryDemo( title: localizations.demoMotionTitle, icon: GalleryIcons.animation, diff --git a/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart b/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart new file mode 100644 index 000000000000..894479b2c567 --- /dev/null +++ b/dev/integration_tests/new_gallery/lib/demos/reference/two_pane_demo.dart @@ -0,0 +1,226 @@ +// 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:ui'; +import 'package:dual_screen/dual_screen.dart'; +import 'package:flutter/material.dart'; +import '../../gallery_localizations.dart'; + +// BEGIN twoPaneDemo + +enum TwoPaneDemoType { + foldable, + tablet, + smallScreen, +} + +class TwoPaneDemo extends StatefulWidget { + const TwoPaneDemo({ + super.key, + required this.restorationId, + required this.type, + }); + + final String restorationId; + final TwoPaneDemoType type; + + @override + TwoPaneDemoState createState() => TwoPaneDemoState(); +} + +class TwoPaneDemoState extends State with RestorationMixin { + final RestorableInt _currentIndex = RestorableInt(-1); + + @override + String get restorationId => widget.restorationId; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_currentIndex, 'two_pane_selected_item'); + } + + @override + void dispose() { + _currentIndex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + TwoPanePriority panePriority = TwoPanePriority.both; + if (widget.type == TwoPaneDemoType.smallScreen) { + panePriority = _currentIndex.value == -1 + ? TwoPanePriority.start + : TwoPanePriority.end; + } + return SimulateScreen( + type: widget.type, + child: TwoPane( + paneProportion: 0.3, + panePriority: panePriority, + startPane: ListPane( + selectedIndex: _currentIndex.value, + onSelect: (int index) { + setState(() { + _currentIndex.value = index; + }); + }, + ), + endPane: DetailsPane( + selectedIndex: _currentIndex.value, + onClose: switch (widget.type) { + TwoPaneDemoType.smallScreen => () => setState(() { _currentIndex.value = -1; }), + TwoPaneDemoType.foldable || TwoPaneDemoType.tablet => null, + }, + ), + ), + ); + } +} + +class ListPane extends StatelessWidget { + + const ListPane({ + super.key, + required this.onSelect, + required this.selectedIndex, + }); + final ValueChanged onSelect; + final int selectedIndex; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text(GalleryLocalizations.of(context)!.demoTwoPaneList), + ), + body: Scrollbar( + child: ListView( + restorationId: 'list_demo_list_view', + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + for (int index = 1; index < 21; index++) + ListTile( + onTap: () { + onSelect(index); + }, + selected: selectedIndex == index, + leading: ExcludeSemantics( + child: CircleAvatar(child: Text('$index')), + ), + title: Text( + GalleryLocalizations.of(context)!.demoTwoPaneItem(index), + ), + ), + ], + ), + ), + ); + } +} + +class DetailsPane extends StatelessWidget { + + const DetailsPane({ + super.key, + required this.selectedIndex, + this.onClose, + }); + final VoidCallback? onClose; + final int selectedIndex; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + leading: onClose == null + ? null + : IconButton(icon: const Icon(Icons.close), onPressed: onClose), + title: Text( + GalleryLocalizations.of(context)!.demoTwoPaneDetails, + ), + ), + body: ColoredBox( + color: const Color(0xfffafafa), + child: Center( + child: Text( + selectedIndex == -1 + ? GalleryLocalizations.of(context)!.demoTwoPaneSelectItem + : GalleryLocalizations.of(context)! + .demoTwoPaneItemDetails(selectedIndex), + ), + ), + ), + ); + } +} + +class SimulateScreen extends StatelessWidget { + const SimulateScreen({ + super.key, + required this.type, + required this.child, + }); + + final TwoPaneDemoType type; + final TwoPane child; + + // An approximation of a real foldable + static const double foldableAspectRatio = 20 / 18; + // 16x9 candy bar phone + static const double singleScreenAspectRatio = 9 / 16; + // Taller desktop / tablet + static const double tabletAspectRatio = 4 / 3; + // How wide should the hinge be, as a proportion of total width + static const double hingeProportion = 1 / 35; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(14), + child: AspectRatio( + aspectRatio: switch (type) { + TwoPaneDemoType.foldable => foldableAspectRatio, + TwoPaneDemoType.tablet => tabletAspectRatio, + TwoPaneDemoType.smallScreen => singleScreenAspectRatio, + }, + child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { + final Size size = Size(constraints.maxWidth, constraints.maxHeight); + final Size hingeSize = Size(size.width * hingeProportion, size.height); + // Position the hinge in the middle of the display + final Rect hingeBounds = Rect.fromLTWH( + (size.width - hingeSize.width) / 2, + 0, + hingeSize.width, + hingeSize.height, + ); + return MediaQuery( + data: MediaQueryData( + size: size, + displayFeatures: [ + if (type == TwoPaneDemoType.foldable) + DisplayFeature( + bounds: hingeBounds, + type: DisplayFeatureType.hinge, + state: DisplayFeatureState.postureFlat, + ), + ], + ), + child: child, + ); + }), + ), + ), + ); + } +} + +// END diff --git a/dev/integration_tests/new_gallery/lib/layout/adaptive.dart b/dev/integration_tests/new_gallery/lib/layout/adaptive.dart index 283c018f357e..4e198508d81f 100644 --- a/dev/integration_tests/new_gallery/lib/layout/adaptive.dart +++ b/dev/integration_tests/new_gallery/lib/layout/adaptive.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:adaptive_breakpoints/adaptive_breakpoints.dart'; +import 'package:dual_screen/dual_screen.dart'; import 'package:flutter/material.dart'; /// The maximum width taken up by each item on the home screen. @@ -10,12 +13,14 @@ const double maxHomeItemWidth = 1400.0; /// Returns a boolean value whether the window is considered medium or large size. /// -/// Widgets using this method might consider the display is +/// When running on a desktop device that is also foldable, the display is not +/// considered desktop. Widgets using this method might consider the display is /// large enough for certain layouts, which is not the case on foldable devices, /// where only part of the display is available to said widgets. /// /// Used to build adaptive and responsive layouts. bool isDisplayDesktop(BuildContext context) => + !isDisplayFoldable(context) && getWindowType(context) >= AdaptiveWindowType.medium; /// Returns boolean value whether the window is considered medium size. @@ -24,3 +29,16 @@ bool isDisplayDesktop(BuildContext context) => bool isDisplaySmallDesktop(BuildContext context) { return getWindowType(context) == AdaptiveWindowType.medium; } + +/// Returns a boolean value whether the display has a hinge that splits the +/// screen into two, left and right sub-screens. Horizontal splits (top and +/// bottom sub-screens) are ignored for this application. +bool isDisplayFoldable(BuildContext context) { + final DisplayFeature? hinge = MediaQuery.of(context).hinge; + if (hinge == null) { + return false; + } else { + // Vertical + return hinge.bounds.size.aspectRatio < 1; + } +} diff --git a/dev/integration_tests/new_gallery/lib/main.dart b/dev/integration_tests/new_gallery/lib/main.dart index 315fe00b8fb8..223135962c93 100644 --- a/dev/integration_tests/new_gallery/lib/main.dart +++ b/dev/integration_tests/new_gallery/lib/main.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:dual_screen/dual_screen.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; @@ -49,6 +50,7 @@ class GalleryApp extends StatelessWidget { child: Builder( builder: (BuildContext context) { final GalleryOptions options = GalleryOptions.of(context); + final bool hasHinge = MediaQuery.of(context).hinge?.bounds != null; return MaterialApp( restorationScopeId: 'rootGallery', title: 'Flutter Gallery', @@ -72,7 +74,7 @@ class GalleryApp extends StatelessWidget { return basicLocaleListResolution(locales, supportedLocales); }, onGenerateRoute: (RouteSettings settings) => - RouteConfiguration.onGenerateRoute(settings), + RouteConfiguration.onGenerateRoute(settings, hasHinge), ); }, ), diff --git a/dev/integration_tests/new_gallery/lib/pages/demo.dart b/dev/integration_tests/new_gallery/lib/pages/demo.dart index 937a1cb7beb8..46ad1ecc258c 100644 --- a/dev/integration_tests/new_gallery/lib/pages/demo.dart +++ b/dev/integration_tests/new_gallery/lib/pages/demo.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:dual_screen/dual_screen.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -187,12 +188,13 @@ class _GalleryDemoPageState extends State void _resolveState(BuildContext context) { final bool isDesktop = isDisplayDesktop(context); + final bool isFoldable = isDisplayFoldable(context); if (_DemoState.values[_demoStateIndex.value] == _DemoState.fullscreen && !isDesktop) { // Do not allow fullscreen state for mobile. _demoStateIndex.value = _DemoState.normal.index; } else if (_DemoState.values[_demoStateIndex.value] == _DemoState.normal && - isDesktop) { + (isDesktop || isFoldable)) { // Do not allow normal state for desktop. _demoStateIndex.value = _hasOptions ? _DemoState.options.index : _DemoState.info.index; @@ -207,6 +209,7 @@ class _GalleryDemoPageState extends State @override Widget build(BuildContext context) { + final bool isFoldable = isDisplayFoldable(context); final bool isDesktop = isDisplayDesktop(context); _resolveState(context); @@ -369,6 +372,14 @@ class _GalleryDemoPageState extends State child: sectionAndDemo, ), ); + } else if (isFoldable) { + body = Padding( + padding: const EdgeInsets.only(top: 12.0), + child: TwoPane( + startPane: demoContent, + endPane: section, + ), + ); } else { section = AnimatedSize( duration: const Duration(milliseconds: 200), @@ -418,7 +429,7 @@ class _GalleryDemoPageState extends State Widget page; - if (isDesktop) { + if (isDesktop || isFoldable) { page = AnimatedBuilder( animation: _codeBackgroundColorController, builder: (BuildContext context, Widget? child) { diff --git a/dev/integration_tests/new_gallery/lib/pages/home.dart b/dev/integration_tests/new_gallery/lib/pages/home.dart index 0394acb2ff5a..3f160ae6e5a2 100644 --- a/dev/integration_tests/new_gallery/lib/pages/home.dart +++ b/dev/integration_tests/new_gallery/lib/pages/home.dart @@ -1154,6 +1154,8 @@ class StudyWrapper extends StatefulWidget { class _StudyWrapperState extends State { @override Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + final TextTheme textTheme = Theme.of(context).textTheme; return ApplyTextOptions( child: Stack( children: [ @@ -1164,8 +1166,47 @@ class _StudyWrapperState extends State { child: widget.study, ), ), + if (!isDisplayFoldable(context)) + SafeArea( + child: Align( + alignment: widget.alignment, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 16.0, + vertical: widget.hasBottomNavBar + ? kBottomNavigationBarHeight + 16.0 + : 16.0), + child: Semantics( + sortKey: const OrdinalSortKey(0), + label: GalleryLocalizations.of(context)!.backToGallery, + button: true, + enabled: true, + excludeSemantics: true, + child: FloatingActionButton.extended( + heroTag: _BackButtonHeroTag(), + key: const ValueKey('Back'), + onPressed: () { + Navigator.of(context) + .popUntil((Route route) => route.settings.name == '/'); + }, + icon: IconTheme( + data: IconThemeData(color: colorScheme.onPrimary), + child: const BackButtonIcon(), + ), + label: Text( + MaterialLocalizations.of(context).backButtonTooltip, + style: textTheme.labelLarge! + .apply(color: colorScheme.onPrimary), + ), + ), + ), + ), + ), + ), ], ), ); } } + +class _BackButtonHeroTag {} diff --git a/dev/integration_tests/new_gallery/lib/pages/splash.dart b/dev/integration_tests/new_gallery/lib/pages/splash.dart index d7914048fafd..898903503a2a 100644 --- a/dev/integration_tests/new_gallery/lib/pages/splash.dart +++ b/dev/integration_tests/new_gallery/lib/pages/splash.dart @@ -4,9 +4,11 @@ import 'dart:math'; +import 'package:dual_screen/dual_screen.dart'; import 'package:flutter/material.dart'; import '../constants.dart'; +import '../gallery_localizations.dart'; import '../layout/adaptive.dart'; import 'home.dart'; @@ -143,19 +145,36 @@ class _SplashPageState extends State ); } - return Stack( - children: [ - _SplashBackLayer( - isSplashCollapsed: !_isSplashVisible, - effect: _effect, - onTap: _controller.forward, - ), - PositionedTransition( - rect: animation, - child: frontLayer, + if (isDisplayFoldable(context)) { + return TwoPane( + startPane: frontLayer, + endPane: GestureDetector( + onTap: () { + if (_isSplashVisible) { + _controller.reverse(); + } else { + _controller.forward(); + } + }, + child: _SplashBackLayer( + isSplashCollapsed: !_isSplashVisible, effect: _effect), ), - ], - ); + ); + } else { + return Stack( + children: [ + _SplashBackLayer( + isSplashCollapsed: !_isSplashVisible, + effect: _effect, + onTap: _controller.forward, + ), + PositionedTransition( + rect: animation, + child: frontLayer, + ), + ], + ); + } }, ), ), @@ -199,6 +218,26 @@ class _SplashBackLayer extends StatelessWidget { ), ); } + if (isDisplayFoldable(context)) { + child = ColoredBox( + color: Theme.of(context).colorScheme.background, + child: Stack( + children: [ + Center( + child: flutterLogo, + ), + Padding( + padding: const EdgeInsets.only(top: 100.0), + child: Center( + child: Text( + GalleryLocalizations.of(context)!.splashSelectDemo, + ), + ), + ) + ], + ), + ); + } } else { child = Stack( children: [ @@ -221,7 +260,9 @@ class _SplashBackLayer extends StatelessWidget { padding: EdgeInsets.only( bottom: isDisplayDesktop(context) ? homePeekDesktop - : homePeekMobile, + : isDisplayFoldable(context) + ? 0 + : homePeekMobile, ), child: child, ), diff --git a/dev/integration_tests/new_gallery/lib/routes.dart b/dev/integration_tests/new_gallery/lib/routes.dart index d5cc07f1f06c..06f5eb367270 100644 --- a/dev/integration_tests/new_gallery/lib/routes.dart +++ b/dev/integration_tests/new_gallery/lib/routes.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:dual_screen/dual_screen.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'deferred_widget.dart'; @@ -24,7 +25,7 @@ import 'studies/starter/routes.dart' as starter_app_routes; typedef PathWidgetBuilder = Widget Function(BuildContext, String?); class Path { - const Path(this.pattern, this.builder); + const Path(this.pattern, this.builder, {this.openInSecondScreen = false}); /// A RegEx string for route matching. final String pattern; @@ -40,6 +41,9 @@ class Path { /// ) /// ``` final PathWidgetBuilder builder; + + /// If the route should open on the second screen on foldables. + final bool openInSecondScreen; } class RouteConfiguration { @@ -59,6 +63,7 @@ class RouteConfiguration { study: DeferredWidget(rally.loadLibrary, () => rally.RallyApp()), // ignore: prefer_const_constructors ), + openInSecondScreen: true, ), Path( r'^' + shrine_routes.homeRoute, @@ -66,6 +71,7 @@ class RouteConfiguration { study: DeferredWidget(shrine.loadLibrary, () => shrine.ShrineApp()), // ignore: prefer_const_constructors ), + openInSecondScreen: true, ), Path( r'^' + crane_routes.defaultRoute, @@ -74,6 +80,7 @@ class RouteConfiguration { () => crane.CraneApp(), // ignore: prefer_const_constructors placeholder: const DeferredLoadingPlaceholder(name: 'Crane')), ), + openInSecondScreen: true, ), Path( r'^' + fortnightly_routes.defaultRoute, @@ -83,18 +90,21 @@ class RouteConfiguration { // ignore: prefer_const_constructors () => fortnightly.FortnightlyApp()), ), + openInSecondScreen: true, ), Path( r'^' + reply_routes.homeRoute, // ignore: prefer_const_constructors (BuildContext context, String? match) => const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true), + openInSecondScreen: true, ), Path( r'^' + starter_app_routes.defaultRoute, (BuildContext context, String? match) => const StudyWrapper( study: starter_app.StarterApp(), ), + openInSecondScreen: true, ), Path( r'^/', @@ -108,6 +118,7 @@ class RouteConfiguration { /// matching. static Route? onGenerateRoute( RouteSettings settings, + bool hasHinge, ) { for (final Path path in paths) { final RegExp regExpPattern = RegExp(path.pattern); @@ -120,10 +131,17 @@ class RouteConfiguration { settings: settings, ); } - return MaterialPageRoute( - builder: (BuildContext context) => path.builder(context, match), - settings: settings, - ); + if (path.openInSecondScreen && hasHinge) { + return TwoPanePageRoute( + builder: (BuildContext context) => path.builder(context, match), + settings: settings, + ); + } else { + return MaterialPageRoute( + builder: (BuildContext context) => path.builder(context, match), + settings: settings, + ); + } } } @@ -160,7 +178,17 @@ class TwoPanePageRoute extends OverlayRoute { @override Iterable createOverlayEntries() sync* { yield OverlayEntry(builder: (BuildContext context) { - return builder.call(context); + final Rect? hinge = MediaQuery.of(context).hinge?.bounds; + if (hinge == null) { + return builder.call(context); + } else { + return Positioned( + top: 0, + left: hinge.right, + right: 0, + bottom: 0, + child: builder.call(context)); + } }); } } diff --git a/dev/integration_tests/new_gallery/pubspec.yaml b/dev/integration_tests/new_gallery/pubspec.yaml index a931c4810104..99bb107ba88a 100644 --- a/dev/integration_tests/new_gallery/pubspec.yaml +++ b/dev/integration_tests/new_gallery/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: animations: 2.0.11 collection: 1.18.0 cupertino_icons: 1.0.8 + dual_screen: 1.0.4 flutter_gallery_assets: 1.0.2 flutter_localized_locales: 2.0.5 flutter_staggered_grid_view: 0.7.0 @@ -311,4 +312,4 @@ flutter: fonts: - asset: packages/flutter_gallery_assets/fonts/GalleryIcons.ttf -# PUBSPEC CHECKSUM: 579a +# PUBSPEC CHECKSUM: f270