mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
577 lines
18 KiB
Dart
577 lines
18 KiB
Dart
// 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/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
testWidgets('PageTransitionsBuilder buildTransitions method is called correctly', (
|
|
WidgetTester tester,
|
|
) async {
|
|
var buildTransitionsCalled = false;
|
|
PageRoute<dynamic>? capturedRoute;
|
|
BuildContext? capturedContext;
|
|
Animation<double>? capturedAnimation;
|
|
Animation<double>? capturedSecondaryAnimation;
|
|
Widget? capturedChild;
|
|
|
|
final builderWithCapture = _TestPageTransitionsBuilder(
|
|
onBuildTransitions:
|
|
<T>(
|
|
PageRoute<T> route,
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
buildTransitionsCalled = true;
|
|
capturedRoute = route;
|
|
capturedContext = context;
|
|
capturedAnimation = animation;
|
|
capturedSecondaryAnimation = secondaryAnimation;
|
|
capturedChild = child;
|
|
|
|
return SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(1.0, 0.0),
|
|
end: Offset.zero,
|
|
).animate(animation),
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
|
|
final routes = <String, WidgetBuilder>{
|
|
'/': (BuildContext context) => Material(
|
|
child: TextButton(
|
|
child: const Text('push'),
|
|
onPressed: () {
|
|
Navigator.of(context).pushNamed('/test');
|
|
},
|
|
),
|
|
),
|
|
'/test': (BuildContext context) => const Material(child: Text('test page')),
|
|
};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(
|
|
pageTransitionsTheme: PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android: builderWithCapture,
|
|
},
|
|
),
|
|
),
|
|
routes: routes,
|
|
),
|
|
);
|
|
|
|
// Trigger navigation
|
|
await tester.tap(find.text('push'));
|
|
await tester.pump();
|
|
|
|
// Verify buildTransitions was called with correct parameters
|
|
expect(buildTransitionsCalled, isTrue);
|
|
expect(capturedRoute, isNotNull);
|
|
expect(capturedContext, isNotNull);
|
|
expect(capturedAnimation, isNotNull);
|
|
expect(capturedSecondaryAnimation, isNotNull);
|
|
expect(capturedChild, isNotNull);
|
|
expect(capturedRoute!.settings.name, '/');
|
|
});
|
|
|
|
testWidgets('PageTransitionsBuilder works with custom Navigator and PageRoute', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final customTransitionsBuilder = _TestPageTransitionsBuilder(
|
|
onBuildTransitions:
|
|
<T>(
|
|
PageRoute<T> route,
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
return FadeTransition(
|
|
opacity: animation,
|
|
child: ScaleTransition(
|
|
scale: animation.drive(
|
|
Tween<double>(begin: 0.5, end: 1.0).chain(CurveTween(curve: Curves.easeInOut)),
|
|
),
|
|
child: child,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Navigator(
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
return _CustomPageRoute<void>(
|
|
settings: settings,
|
|
transitionsBuilder: customTransitionsBuilder,
|
|
builder: (BuildContext context) {
|
|
if (settings.name == '/') {
|
|
return Center(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed('/second');
|
|
},
|
|
child: Container(
|
|
width: 200,
|
|
height: 50,
|
|
color: const Color(0xFF2196F3),
|
|
child: const Center(
|
|
child: Text('Navigate', style: TextStyle(color: Color(0xFFFFFFFF))),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const ColoredBox(
|
|
color: Color(0xFF4CAF50),
|
|
child: Center(
|
|
child: Text(
|
|
'Second Page',
|
|
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('Navigate'), findsOneWidget);
|
|
expect(find.text('Second Page'), findsNothing);
|
|
|
|
await tester.tap(find.text('Navigate'));
|
|
await tester.pump();
|
|
|
|
expect(find.text('Navigate'), findsOneWidget);
|
|
expect(find.text('Second Page'), findsOneWidget);
|
|
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
|
|
final FadeTransition fadeTransition = tester.widget<FadeTransition>(
|
|
find.byType(FadeTransition).first,
|
|
);
|
|
expect(fadeTransition.opacity.value, greaterThan(0.0));
|
|
expect(fadeTransition.opacity.value, lessThanOrEqualTo(1.0));
|
|
|
|
final ScaleTransition scaleTransition = tester.widget<ScaleTransition>(
|
|
find.byType(ScaleTransition).first,
|
|
);
|
|
expect(scaleTransition.scale.value, greaterThanOrEqualTo(0.5));
|
|
expect(scaleTransition.scale.value, lessThanOrEqualTo(1.0));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Navigate'), findsNothing);
|
|
expect(find.text('Second Page'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('FadeUpwardsPageTransitionsBuilder test', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Navigator(
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
return _CustomPageRoute<void>(
|
|
settings: settings,
|
|
transitionsBuilder: const FadeUpwardsPageTransitionsBuilder(),
|
|
builder: (BuildContext context) {
|
|
if (settings.name == '/') {
|
|
return ColoredBox(
|
|
color: const Color(0xFF2196F3),
|
|
child: Center(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed('/second');
|
|
},
|
|
child: const Text(
|
|
'Page 1',
|
|
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const ColoredBox(
|
|
color: Color(0xFF4CAF50),
|
|
child: Center(
|
|
child: Text('Page 2', style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24)),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
|
|
|
|
await tester.tap(find.text('Page 1'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
FadeTransition widget2Opacity = tester
|
|
.element(find.text('Page 2'))
|
|
.findAncestorWidgetOfExactType<FadeTransition>()!;
|
|
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
|
|
expect(widget1TopLeft.dx == widget2TopLeft.dx, true);
|
|
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
|
|
expect(widget2Opacity.opacity.value < 0.01, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(find.text('Page 1'), findsNothing);
|
|
expect(find.text('Page 2'), isOnstage);
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
widget2Opacity = tester
|
|
.element(find.text('Page 2'))
|
|
.findAncestorWidgetOfExactType<FadeTransition>()!;
|
|
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
|
|
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
|
|
expect(widget2Opacity.opacity.value < 1.0, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(find.text('Page 1'), isOnstage);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'FadeUpwardsPageTransitionsBuilder test with Material PageTransitionTheme',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: const Material(child: Text('Page 1')),
|
|
theme: ThemeData(
|
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
|
|
},
|
|
),
|
|
),
|
|
routes: <String, WidgetBuilder>{
|
|
'/next': (BuildContext context) {
|
|
return const Material(child: Text('Page 2'));
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
FadeTransition widget2Opacity = tester
|
|
.element(find.text('Page 2'))
|
|
.findAncestorWidgetOfExactType<FadeTransition>()!;
|
|
Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
final Size widget2Size = tester.getSize(find.text('Page 2'));
|
|
|
|
// Android transition is vertical only.
|
|
expect(widget1TopLeft.dx == widget2TopLeft.dx, true);
|
|
// Page 1 is above page 2 mid-transition.
|
|
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
|
|
// Animation begins 3/4 of the way up the page.
|
|
expect(widget2TopLeft.dy < widget2Size.height / 4.0, true);
|
|
// Animation starts with page 2 being near transparent.
|
|
expect(widget2Opacity.opacity.value < 0.01, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
// Page 2 covers page 1.
|
|
expect(find.text('Page 1'), findsNothing);
|
|
expect(find.text('Page 2'), isOnstage);
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
widget2Opacity = tester
|
|
.element(find.text('Page 2'))
|
|
.findAncestorWidgetOfExactType<FadeTransition>()!;
|
|
widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
|
|
// Page 2 starts to move down.
|
|
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
|
|
// Page 2 starts to lose opacity.
|
|
expect(widget2Opacity.opacity.value < 1.0, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(find.text('Page 1'), isOnstage);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
|
);
|
|
|
|
testWidgets(
|
|
'PageTransitionsTheme override builds a _OpenUpwardsPageTransition',
|
|
(WidgetTester tester) async {
|
|
final routes = <String, WidgetBuilder>{
|
|
'/': (BuildContext context) => Material(
|
|
child: TextButton(
|
|
child: const Text('push'),
|
|
onPressed: () {
|
|
Navigator.of(context).pushNamed('/b');
|
|
},
|
|
),
|
|
),
|
|
'/b': (BuildContext context) => const Text('page b'),
|
|
};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(
|
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android:
|
|
OpenUpwardsPageTransitionsBuilder(), // creates a _OpenUpwardsPageTransition
|
|
},
|
|
),
|
|
),
|
|
routes: routes,
|
|
),
|
|
);
|
|
|
|
Finder findOpenUpwardsPageTransition() {
|
|
return find.descendant(
|
|
of: find.byType(MaterialApp),
|
|
matching: find.byWidgetPredicate(
|
|
(Widget w) => '${w.runtimeType}' == '_OpenUpwardsPageTransition',
|
|
),
|
|
);
|
|
}
|
|
|
|
expect(
|
|
Theme.of(tester.element(find.text('push'))).platform,
|
|
debugDefaultTargetPlatformOverride,
|
|
);
|
|
expect(findOpenUpwardsPageTransition(), findsOneWidget);
|
|
|
|
await tester.tap(find.text('push'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('page b'), findsOneWidget);
|
|
expect(findOpenUpwardsPageTransition(), findsOneWidget);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
|
);
|
|
|
|
testWidgets('OpenUpwardsPageTransitionsBuilder test', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Navigator(
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
return _CustomPageRoute<void>(
|
|
settings: settings,
|
|
transitionsBuilder: const OpenUpwardsPageTransitionsBuilder(),
|
|
builder: (BuildContext context) {
|
|
if (settings.name == '/') {
|
|
return ColoredBox(
|
|
color: const Color(0xFF2196F3),
|
|
child: Center(
|
|
child: GestureDetector(
|
|
onTap: () {
|
|
Navigator.of(context).pushNamed('/second');
|
|
},
|
|
child: const Text(
|
|
'Page 1',
|
|
style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const ColoredBox(
|
|
color: Color(0xFF4CAF50),
|
|
child: Center(
|
|
child: Text('Page 2', style: TextStyle(color: Color(0xFFFFFFFF), fontSize: 24)),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
|
|
|
|
await tester.tap(find.text('Page 1'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
expect(find.text('Page 1'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsOneWidget);
|
|
|
|
final Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
|
|
expect(widget1TopLeft.dx, widget2TopLeft.dx);
|
|
expect(widget1TopLeft.dy <= widget2TopLeft.dy, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
// After animation, only Page 2 should be visible.
|
|
expect(find.text('Page 1'), findsNothing);
|
|
expect(find.text('Page 2'), isOnstage);
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
expect(find.text('Page 1'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsOneWidget);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
// After reverse animation, only Page 1 should be visible.
|
|
expect(find.text('Page 1'), isOnstage);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
});
|
|
|
|
testWidgets(
|
|
'OpenUpwardsPageTransitionsBuilder test with Material PageTransitionTheme',
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: const Material(child: Text('Page 1')),
|
|
theme: ThemeData(
|
|
pageTransitionsTheme: const PageTransitionsTheme(
|
|
builders: <TargetPlatform, PageTransitionsBuilder>{
|
|
TargetPlatform.android: OpenUpwardsPageTransitionsBuilder(),
|
|
},
|
|
),
|
|
),
|
|
routes: <String, WidgetBuilder>{
|
|
'/next': (BuildContext context) {
|
|
return const Material(child: Text('Page 2'));
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
expect(find.text('Page 1'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsOneWidget);
|
|
|
|
final Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
|
|
|
|
expect(widget1TopLeft.dx, widget2TopLeft.dx);
|
|
expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
// Page 2 covers page 1.
|
|
expect(find.text('Page 1'), findsNothing);
|
|
expect(find.text('Page 2'), isOnstage);
|
|
|
|
tester.state<NavigatorState>(find.byType(Navigator)).pop();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
expect(find.text('Page 1'), findsOneWidget);
|
|
expect(find.text('Page 2'), findsOneWidget);
|
|
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
// Back to page 1.
|
|
expect(find.text('Page 1'), isOnstage);
|
|
expect(find.text('Page 2'), findsNothing);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.android),
|
|
);
|
|
}
|
|
|
|
class _CustomPageRoute<T> extends PageRoute<T> {
|
|
_CustomPageRoute({required this.builder, required this.transitionsBuilder, super.settings});
|
|
|
|
final WidgetBuilder builder;
|
|
final PageTransitionsBuilder transitionsBuilder;
|
|
|
|
@override
|
|
Duration get transitionDuration => const Duration(milliseconds: 300);
|
|
|
|
@override
|
|
bool get maintainState => true;
|
|
|
|
@override
|
|
Color? get barrierColor => null;
|
|
|
|
@override
|
|
String? get barrierLabel => null;
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
return builder(context);
|
|
}
|
|
|
|
@override
|
|
Widget buildTransitions(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
return transitionsBuilder.buildTransitions<T>(
|
|
this,
|
|
context,
|
|
animation,
|
|
secondaryAnimation,
|
|
child,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TestPageTransitionsBuilder extends PageTransitionsBuilder {
|
|
const _TestPageTransitionsBuilder({required this.onBuildTransitions});
|
|
|
|
final Widget Function<T>(
|
|
PageRoute<T> route,
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
)
|
|
onBuildTransitions;
|
|
|
|
@override
|
|
Widget buildTransitions<T>(
|
|
PageRoute<T> route,
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
Widget child,
|
|
) {
|
|
return onBuildTransitions(route, context, animation, secondaryAnimation, child);
|
|
}
|
|
}
|