mirror of
https://github.com/flutter/flutter
synced 2024-10-14 04:02:56 +00:00
Build routes even less. (#62588)
This commit is contained in:
parent
90aad51d3b
commit
8f3805f5af
|
@ -175,9 +175,21 @@ class TransitionBuilderPage<T> extends Page<T> {
|
|||
final bool barrierDismissible;
|
||||
|
||||
/// {@macro flutter.widgets.modalRoute.barrierColor}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [barrierDismissible], which controls the behavior of the barrier when
|
||||
/// tapped.
|
||||
/// * [ModalBarrier], the widget that implements this feature.
|
||||
final Color barrierColor;
|
||||
|
||||
/// {@macro flutter.widgets.modalRoute.barrierLabel}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [barrierDismissible], which controls the behavior of the barrier when
|
||||
/// tapped.
|
||||
/// * [ModalBarrier], the widget that implements this feature.
|
||||
final String barrierLabel;
|
||||
|
||||
/// {@macro flutter.widgets.modalRoute.maintainState}
|
||||
|
|
|
@ -24,6 +24,7 @@ import 'transitions.dart';
|
|||
|
||||
// Examples can assume:
|
||||
// dynamic routeObserver;
|
||||
// NavigatorState navigator;
|
||||
|
||||
const Color _kTransparent = Color(0x00000000);
|
||||
|
||||
|
@ -193,7 +194,6 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
changedInternalState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -201,16 +201,19 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||
assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.');
|
||||
_controller = createAnimationController();
|
||||
assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
|
||||
_animation = createAnimation();
|
||||
_animation = createAnimation()
|
||||
..addStatusListener(_handleStatusChanged);
|
||||
assert(_animation != null, '$runtimeType.createAnimation() returned null.');
|
||||
super.install();
|
||||
if (_animation.isCompleted && overlayEntries.isNotEmpty) {
|
||||
overlayEntries.first.opaque = opaque;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TickerFuture didPush() {
|
||||
assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
|
||||
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
|
||||
_didPushOrReplace();
|
||||
super.didPush();
|
||||
return _controller.forward();
|
||||
}
|
||||
|
@ -219,7 +222,6 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||
void didAdd() {
|
||||
assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
|
||||
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
|
||||
_didPushOrReplace();
|
||||
super.didAdd();
|
||||
_controller.value = _controller.upperBound;
|
||||
}
|
||||
|
@ -230,19 +232,9 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
|
||||
if (oldRoute is TransitionRoute)
|
||||
_controller.value = oldRoute._controller.value;
|
||||
_didPushOrReplace();
|
||||
super.didReplace(oldRoute);
|
||||
}
|
||||
|
||||
void _didPushOrReplace() {
|
||||
_animation.addStatusListener(_handleStatusChanged);
|
||||
// If the animation is already completed, _handleStatusChanged will not get
|
||||
// a chance to set opaqueness of OverlayEntry.
|
||||
if (_animation.isCompleted && overlayEntries.isNotEmpty) {
|
||||
overlayEntries.first.opaque = opaque;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool didPop(T result) {
|
||||
assert(_controller != null, '$runtimeType.didPop called before calling install() or after calling dispose().');
|
||||
|
@ -878,7 +870,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
/// ```
|
||||
///
|
||||
/// The given [BuildContext] will be rebuilt if the state of the route changes
|
||||
/// (specifically, if [isCurrent] or [canPop] change value).
|
||||
/// while it is visible (specifically, if [isCurrent] or [canPop] change value).
|
||||
@optionalTypeArgs
|
||||
static ModalRoute<T> of<T extends Object>(BuildContext context) {
|
||||
final _ModalScopeStatus widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
|
||||
|
@ -958,7 +950,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
/// is not wrapped in any transition widgets.
|
||||
///
|
||||
/// The [buildTransitions] method, in contrast to [buildPage], is called each
|
||||
/// time the [Route]'s state changes (e.g. the value of [canPop]).
|
||||
/// time the [Route]'s state changes while it is visible (e.g. if the value of
|
||||
/// [canPop] changes on the active route).
|
||||
///
|
||||
/// The [buildTransitions] method is typically used to define transitions
|
||||
/// that animate the new topmost route's comings and goings. When the
|
||||
|
@ -1155,17 +1148,31 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
///
|
||||
/// While the route is animating into position, the color is animated from
|
||||
/// transparent to the specified color.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// If this getter would ever start returning a different color, the
|
||||
/// [Route.changedInternalState] should be invoked so that the change can take
|
||||
/// effect.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// It is safe to use `navigator.context` here. For example, to make
|
||||
/// the barrier color use the theme's background color, one could say:
|
||||
///
|
||||
/// ```dart
|
||||
/// Color get barrierColor => Theme.of(navigator.context).backgroundColor;
|
||||
/// ```
|
||||
///
|
||||
/// The [Navigator] causes the [ModalRoute]'s modal barrier overlay entry
|
||||
/// to rebuild any time its dependencies change.
|
||||
///
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [barrierDismissible], which controls the behavior of the barrier when
|
||||
/// tapped.
|
||||
/// * [ModalBarrier], the widget that implements this feature.
|
||||
/// {@endtemplate}
|
||||
Color get barrierColor;
|
||||
|
||||
/// {@template flutter.widgets.modalRoute.barrierLabel}
|
||||
|
@ -1180,6 +1187,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
///
|
||||
/// For example, when a dialog is on the screen, the page below the dialog is
|
||||
/// usually darkened by the modal barrier.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// If this getter would ever start returning a different label, the
|
||||
/// [Route.changedInternalState] should be invoked so that the change can take
|
||||
|
@ -1190,7 +1198,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
/// * [barrierDismissible], which controls the behavior of the barrier when
|
||||
/// tapped.
|
||||
/// * [ModalBarrier], the widget that implements this feature.
|
||||
/// {@endtemplate}
|
||||
String get barrierLabel;
|
||||
|
||||
/// The curve that is used for animating the modal barrier in and out.
|
||||
|
@ -1247,6 +1254,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
///
|
||||
/// The modal barrier, if any, is not rendered if [offstage] is true (see
|
||||
/// [barrierColor]).
|
||||
///
|
||||
/// Whenever this changes value, [changedInternalState] is called.
|
||||
bool get offstage => _offstage;
|
||||
bool _offstage = false;
|
||||
set offstage(bool value) {
|
||||
|
@ -1257,6 +1266,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
});
|
||||
_animationProxy.parent = _offstage ? kAlwaysCompleteAnimation : super.animation;
|
||||
_secondaryAnimationProxy.parent = _offstage ? kAlwaysDismissedAnimation : super.secondaryAnimation;
|
||||
changedInternalState();
|
||||
}
|
||||
|
||||
/// The build context for the subtree containing the primary content of this route.
|
||||
|
@ -1423,8 +1433,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
|
||||
/// Whether this route can be popped.
|
||||
///
|
||||
/// When this changes, the route will rebuild, and any widgets that used
|
||||
/// [ModalRoute.of] will be notified.
|
||||
/// When this changes, if the route is visible, the route will
|
||||
/// rebuild, and any widgets that used [ModalRoute.of] will be
|
||||
/// notified.
|
||||
bool get canPop => !isFirst || willHandlePopInternally;
|
||||
|
||||
// Internals
|
||||
|
|
|
@ -1208,7 +1208,50 @@ void main() {
|
|||
expect(log, <String>['building B', 'building C', 'found C', 'building D']);
|
||||
key.currentState.pop<void>();
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 10));
|
||||
expect(log, <String>['building B', 'building C', 'found C', 'building D', 'building C', 'found C']);
|
||||
expect(log, <String>['building B', 'building C', 'found C', 'building D']);
|
||||
});
|
||||
|
||||
testWidgets('Routes don\'t rebuild just because their animations ended', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
|
||||
final List<String> log = <String>[];
|
||||
Route<dynamic> nextRoute = PageRouteBuilder<int>(
|
||||
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
log.add('building page 1 - ${ModalRoute.of(context).canPop}');
|
||||
return const Placeholder();
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
navigatorKey: key,
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
assert(nextRoute != null);
|
||||
final Route<dynamic> result = nextRoute;
|
||||
nextRoute = null;
|
||||
return result;
|
||||
},
|
||||
));
|
||||
expect(log, <String>['building page 1 - false']);
|
||||
key.currentState.pushReplacement(PageRouteBuilder<int>(
|
||||
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
log.add('building page 2 - ${ModalRoute.of(context).canPop}');
|
||||
return const Placeholder();
|
||||
},
|
||||
));
|
||||
expect(log, <String>['building page 1 - false']);
|
||||
await tester.pump();
|
||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
||||
key.currentState.pushReplacement(PageRouteBuilder<int>(
|
||||
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||
log.add('building page 3 - ${ModalRoute.of(context).canPop}');
|
||||
return const Placeholder();
|
||||
},
|
||||
));
|
||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
||||
await tester.pump();
|
||||
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']);
|
||||
});
|
||||
|
||||
testWidgets('route semantics', (WidgetTester tester) async {
|
||||
|
|
95
packages/flutter/test/widgets/page_route_builder_test.dart
Normal file
95
packages/flutter/test/widgets/page_route_builder_test.dart
Normal file
|
@ -0,0 +1,95 @@
|
|||
// 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.
|
||||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class TestPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Test',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@override
|
||||
State<StatefulWidget> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
void _presentModalPage() {
|
||||
Navigator.of(context).push(PageRouteBuilder<void>(
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
barrierColor: Colors.black54,
|
||||
opaque: false,
|
||||
pageBuilder: (BuildContext context, _, __) {
|
||||
return ModalPage();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: const Center(
|
||||
child: Text('Test Home'),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _presentModalPage,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ModalPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
InkWell(
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
height: 150,
|
||||
color: Colors.teal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('Barriers show when using PageRouteBuilder', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(TestPage());
|
||||
await tester.tap(find.byType(FloatingActionButton));
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byType(TestPage),
|
||||
matchesGoldenFile('page_route_builder.barrier.png'),
|
||||
);
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue