mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Fix the losing of state when pushing opaque routes (#5624)
Fixes https://github.com/flutter/flutter/issues/5283 Other changes in this patch: Rename OffStage to Offstage. Fixes https://github.com/flutter/flutter/issues/5378 Add a lot of docs. Some minor punctuation and whitespace fixes.
This commit is contained in:
parent
43d0eeb8e1
commit
ea6bf4706a
@ -148,12 +148,17 @@ class _CupertinoBackGestureController extends NavigationGestureController {
|
||||
///
|
||||
/// [MaterialApp] creates material page routes for entries in the
|
||||
/// [MaterialApp.routes] map.
|
||||
///
|
||||
/// By default, when a modal route is replaced by another, the previous route
|
||||
/// remains in memory. To free all the resources when this is not necessary, set
|
||||
/// [maintainState] to false.
|
||||
class MaterialPageRoute<T> extends PageRoute<T> {
|
||||
/// Creates a page route for use in a material design app.
|
||||
MaterialPageRoute({
|
||||
this.builder,
|
||||
Completer<T> completer,
|
||||
RouteSettings settings: const RouteSettings()
|
||||
RouteSettings settings: const RouteSettings(),
|
||||
this.maintainState: true,
|
||||
}) : super(completer: completer, settings: settings) {
|
||||
assert(builder != null);
|
||||
assert(opaque);
|
||||
@ -162,6 +167,9 @@ class MaterialPageRoute<T> extends PageRoute<T> {
|
||||
/// Builds the primary contents of the route.
|
||||
final WidgetBuilder builder;
|
||||
|
||||
@override
|
||||
final bool maintainState;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
|
@ -148,7 +148,53 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
|
||||
|
||||
/// Pump the rendering pipeline to generate a frame.
|
||||
///
|
||||
/// Called automatically by the engine when it is time to lay out and paint a frame.
|
||||
/// This method is called by [handleBeginFrame], which itself is called
|
||||
/// automatically by the engine when when it is time to lay out and paint a
|
||||
/// frame.
|
||||
///
|
||||
/// Each frame consists of the following phases:
|
||||
///
|
||||
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
|
||||
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
|
||||
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
|
||||
/// registration order. This includes all the [Ticker] instances that are
|
||||
/// driving [AnimationController] objects, which means all of the active
|
||||
/// [Animation] objects tick at this point.
|
||||
///
|
||||
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
|
||||
/// the most notable is this method, [beginFrame], which proceeds as follows:
|
||||
///
|
||||
/// 2. The layout phase: All the dirty [RenderObject]s in the system are laid
|
||||
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
|
||||
/// for further details on marking an object dirty for layout.
|
||||
///
|
||||
/// 3. The compositing bits phase: The compositing bits on any dirty
|
||||
/// [RenderObject] objects are updated. See
|
||||
/// [RenderObject.markNeedsCompositingBitsUpdate].
|
||||
///
|
||||
/// 4. The paint phase: All the dirty [RenderObject]s in the system are
|
||||
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
|
||||
/// [RenderObject.markNeedsPaint] for further details on marking an object
|
||||
/// dirty for paint.
|
||||
///
|
||||
/// 5. The compositing phase: The layer tree is turned into a [ui.Scene] and
|
||||
/// sent to the GPU.
|
||||
///
|
||||
/// 6. The semantics phase: All the dirty [RenderObject]s in the system have
|
||||
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
|
||||
/// generates the [SemanticsNode] tree. See
|
||||
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
|
||||
/// object dirty for semantics.
|
||||
///
|
||||
/// For more details on steps 2-6, see [PipelineOwner].
|
||||
///
|
||||
/// 7. The finalization phase: After [beginFrame] returns, [handleBeginFrame]
|
||||
/// then invokes post-frame callbacks (registered with [addPostFrameCallback].
|
||||
///
|
||||
/// Some bindings (for example, the [WidgetsBinding]) add extra steps to this
|
||||
/// list.
|
||||
//
|
||||
// When editing the above, also update widgets/binding.dart's copy.
|
||||
void beginFrame() {
|
||||
assert(renderView != null);
|
||||
pipelineOwner.flushLayout();
|
||||
|
@ -12,7 +12,7 @@ import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as mo
|
||||
|
||||
import 'debug.dart';
|
||||
|
||||
/// A composited layer
|
||||
/// A composited layer.
|
||||
///
|
||||
/// During painting, the render tree generates a tree of composited layers that
|
||||
/// are uploaded into the engine and displayed by the compositor. This class is
|
||||
|
@ -1956,26 +1956,24 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
/// Mark this node as needing an update to its semantics
|
||||
/// description.
|
||||
///
|
||||
/// If the change did not involve a removal or addition of
|
||||
/// semantics, only the change of semantics (e.g. isChecked changing
|
||||
/// from true to false, as opposed to isChecked changing from being
|
||||
/// true to not being changed at all), then you can pass the
|
||||
/// onlyChanges argument with the value true to reduce the cost. If
|
||||
/// semantics are being added or removed, more work needs to be done
|
||||
/// to update the semantics tree. If you pass 'onlyChanges: true'
|
||||
/// but this node, which previously had a SemanticsNode, no longer
|
||||
/// has one, or previously did not set any semantics, but now does,
|
||||
/// or previously had a child that returned annotators, but no
|
||||
/// longer does, or other such combinations, then you will either
|
||||
/// assert during the subsequent call to [flushSemantics()] or you
|
||||
/// will have out-of-date information in the semantics tree.
|
||||
/// If the change did not involve a removal or addition of semantics, only the
|
||||
/// change of semantics (e.g. isChecked changing from true to false, as
|
||||
/// opposed to isChecked changing from being true to not being changed at
|
||||
/// all), then you can pass the onlyChanges argument with the value true to
|
||||
/// reduce the cost. If semantics are being added or removed, more work needs
|
||||
/// to be done to update the semantics tree. If you pass 'onlyChanges: true'
|
||||
/// but this node, which previously had a SemanticsNode, no longer has one, or
|
||||
/// previously did not set any semantics, but now does, or previously had a
|
||||
/// child that returned annotators, but no longer does, or other such
|
||||
/// combinations, then you will either assert during the subsequent call to
|
||||
/// [PipelineOwner.flushSemantics()] or you will have out-of-date information
|
||||
/// in the semantics tree.
|
||||
///
|
||||
/// If the geometry might have changed in any way, then again, more
|
||||
/// work needs to be done to update the semantics tree (to deal with
|
||||
/// clips). You can pass the noGeometry argument to avoid this work
|
||||
/// in the case where only the labels or flags changed. If you pass
|
||||
/// 'noGeometry: true' when the geometry did change, the semantic
|
||||
/// tree will be out of date.
|
||||
/// If the geometry might have changed in any way, then again, more work needs
|
||||
/// to be done to update the semantics tree (to deal with clips). You can pass
|
||||
/// the noGeometry argument to avoid this work in the case where only the
|
||||
/// labels or flags changed. If you pass 'noGeometry: true' when the geometry
|
||||
/// did change, the semantic tree will be out of date.
|
||||
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
|
||||
assert(!attached || !owner._debugDoingSemantics);
|
||||
if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
|
||||
@ -2089,31 +2087,30 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
||||
visitChildren(visitor);
|
||||
}
|
||||
|
||||
/// Returns functions that will annotate a SemanticsNode with the
|
||||
/// semantics of this RenderObject.
|
||||
/// Returns a function that will annotate a [SemanticsNode] with the semantics
|
||||
/// of this [RenderObject].
|
||||
///
|
||||
/// To annotate a SemanticsNode for this node, return all the
|
||||
/// annotators provided by the superclass, plus an annotator that
|
||||
/// adds the annotations. When the behavior of the annotators would
|
||||
/// To annotate a SemanticsNode for this node, return an annotator that
|
||||
/// adds the annotations. When the behavior of the annotator would
|
||||
/// change (e.g. the box is now checked rather than unchecked), call
|
||||
/// [markNeedsSemanticsUpdate()] to indicate to the rendering system
|
||||
/// [markNeedsSemanticsUpdate] to indicate to the rendering system
|
||||
/// that the semantics tree needs to be rebuilt.
|
||||
///
|
||||
/// To introduce a new SemanticsNode, set hasSemantics to true for
|
||||
/// this object. The functions returned by this function will be used
|
||||
/// this object. The function returned by this function will be used
|
||||
/// to annotate the SemanticsNode for this object.
|
||||
///
|
||||
/// Semantic annotations are persistent. Values set in one pass will
|
||||
/// still be set in the next pass. Therefore it is important to
|
||||
/// explicitly set fields to false once they are no longer true --
|
||||
/// explicitly set fields to false once they are no longer true;
|
||||
/// setting them to true when they are to be enabled, and not
|
||||
/// setting them at all when they are not, will mean they remain set
|
||||
/// once enabled once and will never get unset.
|
||||
///
|
||||
/// If the number of annotators you return will change from zero to
|
||||
/// non-zero, and hasSemantics isn't true, then the associated call
|
||||
/// to markNeedsSemanticsUpdate() must not have 'onlyChanges' set, as
|
||||
/// it is possible that the node should be entirely removed.
|
||||
/// If the value return will change from null to non-null (or vice versa), and
|
||||
/// [hasSemantics] isn't true, then the associated call to
|
||||
/// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is
|
||||
/// possible that the node should be entirely removed.
|
||||
SemanticAnnotator get semanticAnnotator => null;
|
||||
|
||||
|
||||
@ -2247,6 +2244,12 @@ abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implem
|
||||
_child.detach();
|
||||
}
|
||||
|
||||
@override
|
||||
void redepthChildren() {
|
||||
if (_child != null)
|
||||
redepthChild(_child);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(RenderObjectVisitor visitor) {
|
||||
if (_child != null)
|
||||
|
@ -32,7 +32,7 @@ export 'package:flutter/gestures.dart' show
|
||||
/// the proxy box with its child. However, RenderProxyBox is a useful base class
|
||||
/// for render objects that wish to mimic most, but not all, of the properties
|
||||
/// of their child.
|
||||
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
|
||||
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {
|
||||
/// Creates a proxy render box.
|
||||
///
|
||||
/// Proxy render boxes are rarely created directly because they simply proxy
|
||||
@ -41,7 +41,15 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
|
||||
RenderProxyBox([RenderBox child = null]) {
|
||||
this.child = child;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [RenderProxyBox].
|
||||
///
|
||||
/// This class can be used as a mixin for situations where the proxying behavior
|
||||
/// of [RenderProxyBox] is desired but inheriting from [RenderProxyBox] is
|
||||
/// impractical (e.g. because you want to mix in other classes as well).
|
||||
// TODO(ianh): Remove this class once https://github.com/dart-lang/sdk/issues/15101 is fixed
|
||||
abstract class RenderProxyBoxMixin implements RenderBox, RenderObjectWithChildMixin<RenderBox> {
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
if (child != null)
|
||||
@ -2016,9 +2024,9 @@ class RenderIgnorePointer extends RenderProxyBox {
|
||||
/// Lays the child out as if it was in the tree, but without painting anything,
|
||||
/// without making the child available for hit testing, and without taking any
|
||||
/// room in the parent.
|
||||
class RenderOffStage extends RenderProxyBox {
|
||||
/// Creates an off-stage render object.
|
||||
RenderOffStage({
|
||||
class RenderOffstage extends RenderProxyBox {
|
||||
/// Creates an offstage render object.
|
||||
RenderOffstage({
|
||||
bool offstage: true,
|
||||
RenderBox child
|
||||
}) : _offstage = offstage, super(child) {
|
||||
|
@ -422,14 +422,14 @@ class RenderPositionedBox extends RenderAligningShiftedBox {
|
||||
/// ignoring the child's dimensions.
|
||||
///
|
||||
/// For example, if you wanted a box to always render 50 pixels high, regardless
|
||||
/// of where it was rendered, you would wrap it in a RenderOverflow with
|
||||
/// minHeight and maxHeight set to 50.0. Generally speaking, to avoid confusing
|
||||
/// behavior around hit testing, a RenderOverflowBox should usually be wrapped
|
||||
/// in a RenderClipRect.
|
||||
/// of where it was rendered, you would wrap it in a
|
||||
/// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0.
|
||||
/// Generally speaking, to avoid confusing behavior around hit testing, a
|
||||
/// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect.
|
||||
///
|
||||
/// The child is positioned at the top left of the box. To position a smaller
|
||||
/// The child is positioned according to [alignment]. To position a smaller
|
||||
/// child inside a larger parent, use [RenderPositionedBox] and
|
||||
/// [RenderConstrainedBox] rather than RenderOverflowBox.
|
||||
/// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox.
|
||||
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
|
||||
/// Creates a render object that lets its child overflow itself.
|
||||
RenderConstrainedOverflowBox({
|
||||
|
@ -199,13 +199,13 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether overflowing children should be clipped, or their overflows be
|
||||
/// Whether overflowing children should be clipped, or their overflow be
|
||||
/// visible.
|
||||
enum Overflow {
|
||||
/// Children's overflows will be visible.
|
||||
/// Overflowing children will be visible.
|
||||
visible,
|
||||
/// Children's overflows will be clipped.
|
||||
clip
|
||||
/// Overflowing children will be clipped to the bounds of their parent.
|
||||
clip,
|
||||
}
|
||||
|
||||
/// Implements the stack layout algorithm
|
||||
|
@ -1077,9 +1077,9 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
|
||||
/// A widget that lays the child out as if it was in the tree, but without painting anything,
|
||||
/// without making the child available for hit testing, and without taking any
|
||||
/// room in the parent.
|
||||
class OffStage extends SingleChildRenderObjectWidget {
|
||||
class Offstage extends SingleChildRenderObjectWidget {
|
||||
/// Creates a widget that visually hides its child.
|
||||
OffStage({ Key key, this.offstage: true, Widget child })
|
||||
Offstage({ Key key, this.offstage: true, Widget child })
|
||||
: super(key: key, child: child) {
|
||||
assert(offstage != null);
|
||||
}
|
||||
@ -1094,10 +1094,10 @@ class OffStage extends SingleChildRenderObjectWidget {
|
||||
final bool offstage;
|
||||
|
||||
@override
|
||||
RenderOffStage createRenderObject(BuildContext context) => new RenderOffStage(offstage: offstage);
|
||||
RenderOffstage createRenderObject(BuildContext context) => new RenderOffstage(offstage: offstage);
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderOffStage renderObject) {
|
||||
void updateRenderObject(BuildContext context, RenderOffstage renderObject) {
|
||||
renderObject.offstage = offstage;
|
||||
}
|
||||
|
||||
@ -1106,6 +1106,22 @@ class OffStage extends SingleChildRenderObjectWidget {
|
||||
super.debugFillDescription(description);
|
||||
description.add('offstage: $offstage');
|
||||
}
|
||||
|
||||
@override
|
||||
_OffstageElement createElement() => new _OffstageElement(this);
|
||||
}
|
||||
|
||||
class _OffstageElement extends SingleChildRenderObjectElement {
|
||||
_OffstageElement(Offstage widget) : super(widget);
|
||||
|
||||
@override
|
||||
Offstage get widget => super.widget;
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(ElementVisitor visitor) {
|
||||
if (!widget.offstage)
|
||||
super.visitChildrenForSemantics(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that attempts to size the child to a specific aspect ratio.
|
||||
@ -2646,6 +2662,14 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
|
||||
..ignoring = ignoring
|
||||
..ignoringSemantics = ignoringSemantics;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('ignoring: $ignoring');
|
||||
if (ignoringSemantics != null)
|
||||
description.add('ignoringSemantics: $ignoringSemantics');
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that absorbs pointers during hit testing.
|
||||
|
@ -201,6 +201,63 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
|
||||
|
||||
bool _buildingDirtyElements = false;
|
||||
|
||||
/// Pump the build and rendering pipeline to generate a frame.
|
||||
///
|
||||
/// This method is called by [handleBeginFrame], which itself is called
|
||||
/// automatically by the engine when when it is time to lay out and paint a
|
||||
/// frame.
|
||||
///
|
||||
/// Each frame consists of the following phases:
|
||||
///
|
||||
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
|
||||
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
|
||||
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
|
||||
/// registration order. This includes all the [Ticker] instances that are
|
||||
/// driving [AnimationController] objects, which means all of the active
|
||||
/// [Animation] objects tick at this point.
|
||||
///
|
||||
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
|
||||
/// the most notable is this method, [beginFrame], which proceeds as follows:
|
||||
///
|
||||
/// 2. The build phase: All the dirty [Element]s in the widget tree are
|
||||
/// rebuilt (see [State.build]). See [State.setState] for further details on
|
||||
/// marking a widget dirty for building. See [BuildOwner] for more information
|
||||
/// on this step.
|
||||
///
|
||||
/// 3. The layout phase: All the dirty [RenderObject]s in the system are laid
|
||||
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
|
||||
/// for further details on marking an object dirty for layout.
|
||||
///
|
||||
/// 4. The compositing bits phase: The compositing bits on any dirty
|
||||
/// [RenderObject] objects are updated. See
|
||||
/// [RenderObject.markNeedsCompositingBitsUpdate].
|
||||
///
|
||||
/// 5. The paint phase: All the dirty [RenderObject]s in the system are
|
||||
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
|
||||
/// [RenderObject.markNeedsPaint] for further details on marking an object
|
||||
/// dirty for paint.
|
||||
///
|
||||
/// 6. The compositing phase: The layer tree is turned into a [ui.Scene] and
|
||||
/// sent to the GPU.
|
||||
///
|
||||
/// 7. The semantics phase: All the dirty [RenderObject]s in the system have
|
||||
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
|
||||
/// generates the [SemanticsNode] tree. See
|
||||
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
|
||||
/// object dirty for semantics.
|
||||
///
|
||||
/// For more details on steps 3-7, see [PipelineOwner].
|
||||
///
|
||||
/// 8. The finalization phase in the widgets layer: The widgets tree is
|
||||
/// finalized. This causes [State.dispose] to be invoked on any objects that
|
||||
/// were removed from the widgets tree this frame. See
|
||||
/// [BuildOwner.finalizeTree] for more details.
|
||||
///
|
||||
/// 9. The finalization phase in the scheduler layer: After [beginFrame]
|
||||
/// returns, [handleBeginFrame] then invokes post-frame callbacks (registered
|
||||
/// with [addPostFrameCallback].
|
||||
//
|
||||
// When editing the above, also update rendering/binding.dart's copy.
|
||||
@override
|
||||
void beginFrame() {
|
||||
assert(!_buildingDirtyElements);
|
||||
@ -210,6 +267,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
|
||||
super.beginFrame();
|
||||
buildOwner.finalizeTree();
|
||||
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes.
|
||||
// See https://github.com/dart-lang/sdk/issues/27192
|
||||
if (_needToReportFirstFrame) {
|
||||
if (_thisFrameWasUseful) {
|
||||
developer.Timeline.instantSync('Widgets completed first useful frame');
|
||||
@ -298,7 +356,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
|
||||
|
||||
/// Inflate this widget and actually set the resulting [RenderObject] as the child of [container].
|
||||
/// Inflate this widget and actually set the resulting [RenderObject] as the
|
||||
/// child of [container].
|
||||
///
|
||||
/// If `element` is null, this function will create a new element. Otherwise,
|
||||
/// the given element will be updated with this widget.
|
||||
|
@ -11,6 +11,12 @@ import 'table.dart';
|
||||
/// Log the dirty widgets that are built each frame.
|
||||
bool debugPrintRebuildDirtyWidgets = false;
|
||||
|
||||
/// Log when widgets with global keys are deactivated and log when they are
|
||||
/// reactivated (retaken).
|
||||
///
|
||||
/// This can help track down framework bugs relating to the [GlobalKey] logic.
|
||||
bool debugPrintGlobalKeyedWidgetLifecycle = false;
|
||||
|
||||
Key _firstNonUniqueKey(Iterable<Widget> widgets) {
|
||||
Set<Key> keySet = new HashSet<Key>();
|
||||
for (Widget widget in widgets) {
|
||||
|
@ -968,6 +968,16 @@ abstract class _ProxyWidget extends Widget {
|
||||
final Widget child;
|
||||
}
|
||||
|
||||
/// Base class for widgets that hook [ParentData] information to children of
|
||||
/// [RenderObjectWidget]s.
|
||||
///
|
||||
/// This can be used to provide per-child configuration for
|
||||
/// [RenderObjectWidget]s with more than one child. For example, [Stack] uses
|
||||
/// the [Positioned] parent data widget to position each child.
|
||||
///
|
||||
/// A [ParentDataWidget] is specific to a particular kind of [RenderObject], and
|
||||
/// thus also to a particular [RenderObjectWidget] class. That class is `T`, the
|
||||
/// [ParentDataWidget] type argument.
|
||||
abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidget {
|
||||
const ParentDataWidget({ Key key, Widget child })
|
||||
: super(key: key, child: child);
|
||||
@ -989,7 +999,7 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidg
|
||||
/// Subclasses should override this to describe the requirements for using the
|
||||
/// ParentDataWidget subclass. It is called when debugIsValidAncestor()
|
||||
/// returned false for an ancestor, or when there are extraneous
|
||||
/// ParentDataWidgets in the ancestor chain.
|
||||
/// [ParentDataWidget]s in the ancestor chain.
|
||||
String debugDescribeInvalidAncestorChain({ String description, String ownershipChain, bool foundValidAncestor, Iterable<Widget> badAncestors }) {
|
||||
assert(T != dynamic);
|
||||
assert(T != RenderObjectWidget);
|
||||
@ -1018,6 +1028,13 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends _ProxyWidg
|
||||
void applyParentData(RenderObject renderObject);
|
||||
}
|
||||
|
||||
/// Base class for widgets that efficiently propagate information down the tree.
|
||||
///
|
||||
/// To obtain the nearest instance of a particular type of inherited widget from
|
||||
/// a build context, use [BuildContext.inheritFromWidgetOfExactType].
|
||||
///
|
||||
/// Inherited widgets, when referenced in this way, will cause the consumer to
|
||||
/// rebuild when the inherited widget itself changes state.
|
||||
abstract class InheritedWidget extends _ProxyWidget {
|
||||
const InheritedWidget({ Key key, Widget child })
|
||||
: super(key: key, child: child);
|
||||
@ -1088,6 +1105,11 @@ abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
|
||||
assert(!children.any((Widget child) => child == null));
|
||||
}
|
||||
|
||||
/// The widgets below this widget in the tree.
|
||||
///
|
||||
/// If this list is going to be mutated, it is usually wise to put [Key]s on
|
||||
/// the widgets, so that the framework can match old configurations to new
|
||||
/// configurations and maintain the underlying render objects.
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
@ -1163,21 +1185,226 @@ class _InactiveElements {
|
||||
/// this callback.
|
||||
typedef void ElementVisitor(Element element);
|
||||
|
||||
/// A handle to the location of a widget in the widget tree.
|
||||
///
|
||||
/// This class presents a set of methods that can be used from
|
||||
/// [StatelessWidget.build] methods and from methods on [State] objects.
|
||||
///
|
||||
/// [BuildContext] objects are passed to [WidgetBuilder] functions (such as
|
||||
/// [StatelessWidget.build]), and are available from the [State.context] member.
|
||||
/// Some static functions (e.g. [showDialog], [Theme.of], and so forth) also
|
||||
/// take build contexts so that they can act on behalf of the calling widget, or
|
||||
/// obtain data specifically for the given context.
|
||||
///
|
||||
/// Each widget has its own [BuildContext], which becomes the parent of the
|
||||
/// widget returned by the [StatelessWidget.build] or [State.build] function.
|
||||
/// (And similarly, the parent of any children for [RenderObjectWidget]s.)
|
||||
///
|
||||
/// In particular, this means that within a build method, the build context of
|
||||
/// the widget of the build method is not the same as the build context of the
|
||||
/// widgets returned by that build method. This can lead to some tricky cases.
|
||||
/// For example, [Theme.of(context)] looks for the nearest enclosing [Theme] of
|
||||
/// the given build context. If a build method for a widget Q includes a [Theme]
|
||||
/// within its returned widget tree, and attempts to use [Theme.of] passing its
|
||||
/// own context, the build method for Q will not find that [Theme] object. It
|
||||
/// will instead find whatever [Theme] was an ancestor to the widget Q. If the
|
||||
/// build context for a subpart of the returned tree is needed, a [Builder]
|
||||
/// widget can be used: the build context passed to the [Builder.builder]
|
||||
/// callback will be that of the [Builder] itself.
|
||||
///
|
||||
/// For example, in the following snippet, the [ScaffoldState.showSnackBar]
|
||||
/// method is called on the [Scaffold] widget that the build method itself
|
||||
/// creates. If a [Builder] had not been used, and instead the `context`
|
||||
/// argument of the build method itself had been used, no [Scaffold] would have
|
||||
/// been found, and the [Scaffold.of] function would have returned null.
|
||||
///
|
||||
/// ```dart
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// // here, Scaffold.of(context) returns null
|
||||
/// return new Scaffold(
|
||||
/// appBar: new AppBar(title: new Text('Demo')),
|
||||
/// body: new Builder(
|
||||
/// builder: (BuildContext context) {
|
||||
/// return new FlatButton(
|
||||
/// child: new Text('BUTTON'),
|
||||
/// onPressed: () {
|
||||
/// // here, Scaffold.of(context) returns the locally created Scaffold
|
||||
/// Scaffold.of(context).showSnackBar(new SnackBar(
|
||||
/// content: new Text('Hello.')
|
||||
/// ));
|
||||
/// }
|
||||
/// );
|
||||
/// }
|
||||
/// )
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The [BuildContext] for a particular widget can change location over time as
|
||||
/// the widget is moved around the tree. Because of this, values returned from
|
||||
/// the methods on this class should not be cached beyond the execution of a
|
||||
/// single synchronous function.
|
||||
///
|
||||
/// [BuildContext] objects are actually [Element] objects. The [BuildContext]
|
||||
/// interface is used to discourage direct manipulation of [Element] objects.
|
||||
abstract class BuildContext {
|
||||
/// The current configuration of the [Element] that is this [BuildContext].
|
||||
Widget get widget;
|
||||
|
||||
/// The current [RenderObject] for the widget. If the widget is a
|
||||
/// [RenderObjectWidget], this is the render object that the widget created
|
||||
/// for itself. Otherwise, it is the render object of the first descendant
|
||||
/// [RenderObjectWidget].
|
||||
///
|
||||
/// This method will only return a valid result after the build phase is
|
||||
/// complete. It is therefore not valid to call this from the [build] function
|
||||
/// itself. It should only be called from interaction event handlers (e.g.
|
||||
/// gesture callbacks) or layout or paint callbacks.
|
||||
///
|
||||
/// If the render object is a [RenderBox], which is the common case, then the
|
||||
/// size of the render object can be obtained from the [RenderBox.size]
|
||||
/// getter. This is only valid after the layout phase, and should therefore
|
||||
/// only be examined from paint callbacks or interaction event handlers (e.g.
|
||||
/// gesture callbacks).
|
||||
///
|
||||
/// For details on the different phases of a frame, see
|
||||
/// [WidgetsBinding.beginFrame].
|
||||
///
|
||||
/// Calling this method is theoretically relatively expensive (O(N) in the
|
||||
/// depth of the tree), but in practice is usually cheap because the tree
|
||||
/// usually has many render objects and therefore the distance to the nearest
|
||||
/// render object is usually short.
|
||||
RenderObject findRenderObject();
|
||||
|
||||
/// Obtains the nearest widget of the given type, which must be the type of a
|
||||
/// concrete [InheritedWidget] subclass, and registers this build context with
|
||||
/// that widget such that when that widget changes (or a new widget of that
|
||||
/// type is introduced, or the widget goes away), this build context is
|
||||
/// rebuilt so that it can obtain new values from that widget.
|
||||
///
|
||||
/// This is typically called implicitly from `of()` static methods, e.g.
|
||||
/// [Theme.of].
|
||||
///
|
||||
/// This should not be called from widget constructors or from
|
||||
/// [State.initState] methods, because those methods would not get called
|
||||
/// again if the inherited value were to change. To ensure that the widget
|
||||
/// correctly updates itself when the inherited value changes, only call this
|
||||
/// (directly or indirectly) from build methods or layout and paint callbacks.
|
||||
///
|
||||
/// It is also possible to call this from interaction event handlers (e.g.
|
||||
/// gesture callbacks) or timers, to obtain a value once, if that value is not
|
||||
/// going to be cached and reused later.
|
||||
///
|
||||
/// Calling this method is O(1) with a small constant factor, but will lead to
|
||||
/// the widget being rebuilt more often.
|
||||
InheritedWidget inheritFromWidgetOfExactType(Type targetType);
|
||||
|
||||
/// Returns the nearest ancestor widget of the given type, which must be the
|
||||
/// type of a concrete [Widget] subclass.
|
||||
///
|
||||
/// This should not be used from build methods, because the build context will
|
||||
/// not be rebuilt if the value that would be returned by this method changes.
|
||||
/// In general, [inheritFromWidgetOfExactType] is more useful. This method is
|
||||
/// appropriate when used in interaction event handlers (e.g. gesture
|
||||
/// callbacks), or for performing one-off tasks.
|
||||
///
|
||||
/// Calling this method is relatively expensive (O(N) in the depth of the
|
||||
/// tree). Only call this method if the distance from this widget to the
|
||||
/// desired ancestor is known to be small and bounded.
|
||||
Widget ancestorWidgetOfExactType(Type targetType);
|
||||
|
||||
/// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
|
||||
/// that matches the given [TypeMatcher].
|
||||
///
|
||||
/// This should not be used from build methods, because the build context will
|
||||
/// not be rebuilt if the value that would be returned by this method changes.
|
||||
/// In general, [inheritFromWidgetOfExactType] is more appropriate for such
|
||||
/// cases. This method is useful to change the state of an ancestor widget in
|
||||
/// a one-off manner, for example, to cause an ancestor scrolling list to
|
||||
/// scroll this build context's widget into view, or to move the focus in
|
||||
/// response to user interaction.
|
||||
///
|
||||
/// In general, though, consider using a callback that triggers a stateful
|
||||
/// change in the ancestor rather than using the imperative style implied by
|
||||
/// this method. This will usually lead to more maintainable and reusable code
|
||||
/// since it decouples widgets from each other.
|
||||
///
|
||||
/// Calling this method is relatively expensive (O(N) in the depth of the
|
||||
/// tree). Only call this method if the distance from this widget to the
|
||||
/// desired ancestor is known to be small and bounded.
|
||||
State ancestorStateOfType(TypeMatcher matcher);
|
||||
|
||||
/// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
|
||||
/// that matches the given [TypeMatcher].
|
||||
///
|
||||
/// This should not be used from build methods, because the build context will
|
||||
/// not be rebuilt if the value that would be returned by this method changes.
|
||||
/// In general, [inheritFromWidgetOfExactType] is more appropriate for such
|
||||
/// cases. This method is useful only in esoteric cases where a widget needs
|
||||
/// to cause an ancestor to change its layout or paint behavior. For example,
|
||||
/// it is used by [Material] so that [InkWell] widgets can trigger the ink
|
||||
/// splash on the [Material]'s actual render object.
|
||||
///
|
||||
/// Calling this method is relatively expensive (O(N) in the depth of the
|
||||
/// tree). Only call this method if the distance from this widget to the
|
||||
/// desired ancestor is known to be small and bounded.
|
||||
RenderObject ancestorRenderObjectOfType(TypeMatcher matcher);
|
||||
|
||||
/// Walks the ancestor chain, starting with the parent of this build context's
|
||||
/// widget, invoking the argument for each ancestor. The callback is given a
|
||||
/// reference to the ancestor widget's corresponding [Element] object. The
|
||||
/// walk stops when it reaches the root widget or when the callback returns
|
||||
/// false. The callback must not return null.
|
||||
///
|
||||
/// This is useful for inspecting the widget tree.
|
||||
///
|
||||
/// Calling this method is relatively expensive (O(N) in the depth of the tree).
|
||||
void visitAncestorElements(bool visitor(Element element));
|
||||
|
||||
/// Walks the children of this widget.
|
||||
///
|
||||
/// This is useful for applying changes to children after they are built
|
||||
/// without waiting for the next frame, especially if the children are known,
|
||||
/// and especially if there is exactly one child (as is always the case for
|
||||
/// [StatefulWidget]s or [StatelessWidget]s).
|
||||
///
|
||||
/// Calling this method is very cheap for build contexts that correspond to
|
||||
/// [StatefulWidget]s or [StatelessWidget]s (O(1), since there's only one
|
||||
/// child).
|
||||
///
|
||||
/// Calling this method is potentially expensive for build contexts that
|
||||
/// correspond to [RenderObjectWidget]s (O(N) in the number of children).
|
||||
///
|
||||
/// Calling this method recursively is extremely expensive (O(N) in the number
|
||||
/// of descendants), and should be avoided if possible. Generally it is
|
||||
/// significantly cheaper to use an [InheritedWidget] and have the descendants
|
||||
/// pull data down, than it is to use [visitChildElements] recursively to push
|
||||
/// data down to them.
|
||||
void visitChildElements(ElementVisitor visitor);
|
||||
}
|
||||
|
||||
/// Manager class for the widgets framework.
|
||||
///
|
||||
/// This class tracks which widgets need rebuilding, and handles other tasks
|
||||
/// that apply to widget trees as a whole, such as managing the inactive element
|
||||
/// list for the tree and triggering the "reassemble" command when necessary
|
||||
/// during debugging.
|
||||
///
|
||||
/// The main build owner is typically owned by the [WidgetsBinding], and is
|
||||
/// driven from the operating system along with the rest of the
|
||||
/// build/layout/paint pipeline.
|
||||
///
|
||||
/// Additional build owners can be built to manage off-screen widget trees.
|
||||
///
|
||||
/// To assign a build owner to a tree, use the
|
||||
/// [RootRenderObjectElement.assignOwner] method on the root element of the
|
||||
/// widget tree.
|
||||
class BuildOwner {
|
||||
BuildOwner({ this.onBuildScheduled });
|
||||
|
||||
/// Called on each build pass when the first buildable element is marked dirty
|
||||
/// Called on each build pass when the first buildable element is marked
|
||||
/// dirty.
|
||||
VoidCallback onBuildScheduled;
|
||||
|
||||
final _InactiveElements _inactiveElements = new _InactiveElements();
|
||||
@ -1185,7 +1412,7 @@ class BuildOwner {
|
||||
final List<BuildableElement> _dirtyElements = <BuildableElement>[];
|
||||
|
||||
/// Adds an element to the dirty elements list so that it will be rebuilt
|
||||
/// when buildDirtyElements is called.
|
||||
/// when [buildDirtyElements] is called.
|
||||
void scheduleBuildFor(BuildableElement element) {
|
||||
assert(() {
|
||||
if (_dirtyElements.contains(element)) {
|
||||
@ -1271,10 +1498,11 @@ class BuildOwner {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Builds all the elements that were marked as dirty using schedule(), in depth order.
|
||||
/// If elements are marked as dirty while this runs, they must be deeper than the algorithm
|
||||
/// has yet reached.
|
||||
/// This is called by beginFrame().
|
||||
/// Builds all the elements that were marked as dirty using
|
||||
/// [scheduleBuildFor], in depth order. If elements are marked as dirty while
|
||||
/// this runs, they must be deeper than the algorithm has yet reached.
|
||||
///
|
||||
/// This is called by [WidgetsBinding.beginFrame].
|
||||
void buildDirtyElements() {
|
||||
if (_dirtyElements.isEmpty)
|
||||
return;
|
||||
@ -1303,7 +1531,7 @@ class BuildOwner {
|
||||
/// Complete the element build pass by unmounting any elements that are no
|
||||
/// longer active.
|
||||
///
|
||||
/// This is called by beginFrame().
|
||||
/// This is called by [WidgetsBinding.beginFrame].
|
||||
///
|
||||
/// In checked mode, this also verifies that each global key is used at most
|
||||
/// once.
|
||||
@ -1325,13 +1553,11 @@ class BuildOwner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cause the entire subtree rooted at the given [Element] to
|
||||
/// be entirely rebuilt. This is used by development tools when
|
||||
/// the application code has changed, to cause the widget tree to
|
||||
/// pick up any changed implementations.
|
||||
/// Cause the entire subtree rooted at the given [Element] to be entirely
|
||||
/// rebuilt. This is used by development tools when the application code has
|
||||
/// changed, to cause the widget tree to pick up any changed implementations.
|
||||
///
|
||||
/// This is expensive and should not be called except during
|
||||
/// development.
|
||||
/// This is expensive and should not be called except during development.
|
||||
void reassemble(Element root) {
|
||||
assert(root._parent == null);
|
||||
assert(root.owner == this);
|
||||
@ -1394,12 +1620,19 @@ abstract class Element implements BuildContext {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// This is used to verify that Element objects move through life in an orderly fashion.
|
||||
// This is used to verify that Element objects move through life in an
|
||||
// orderly fashion.
|
||||
_ElementLifecycle _debugLifecycleState = _ElementLifecycle.initial;
|
||||
|
||||
/// Calls the argument for each child. Must be overridden by subclasses that support having children.
|
||||
/// Calls the argument for each child. Must be overridden by subclasses that
|
||||
/// support having children.
|
||||
void visitChildren(ElementVisitor visitor) { }
|
||||
|
||||
/// Calls the argument for each child that is relevant for semantics. By
|
||||
/// default, defers to [visitChildren]. Classes like [Offstage] override this
|
||||
/// to hide their children.
|
||||
void visitChildrenForSemantics(ElementVisitor visitor) => visitChildren(visitor);
|
||||
|
||||
/// Wrapper around visitChildren for BuildContext.
|
||||
@override
|
||||
void visitChildElements(void visitor(Element element)) {
|
||||
@ -1554,6 +1787,11 @@ abstract class Element implements BuildContext {
|
||||
return null;
|
||||
if (!Widget.canUpdate(element.widget, newWidget))
|
||||
return null;
|
||||
assert(() {
|
||||
if (debugPrintGlobalKeyedWidgetLifecycle)
|
||||
debugPrint('Attempting to take $element from ${element._parent ?? "inactive elements list"} to put in $this');
|
||||
return true;
|
||||
});
|
||||
if (element._parent != null && !element._parent.detachChild(element))
|
||||
return null;
|
||||
assert(element._parent == null);
|
||||
@ -1601,6 +1839,13 @@ abstract class Element implements BuildContext {
|
||||
child._parent = null;
|
||||
child.detachRenderObject();
|
||||
owner._inactiveElements.add(child); // this eventually calls child.deactivate()
|
||||
assert(() {
|
||||
if (debugPrintGlobalKeyedWidgetLifecycle) {
|
||||
if (child.widget.key is GlobalKey)
|
||||
debugPrint('Deactivated $child (keyed child of $this)');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void _activateWithParent(Element parent, dynamic newSlot) {
|
||||
@ -1623,6 +1868,11 @@ abstract class Element implements BuildContext {
|
||||
/// instead of being unmounted (see [unmount]).
|
||||
@mustCallSuper
|
||||
void activate() {
|
||||
assert(() {
|
||||
if (debugPrintGlobalKeyedWidgetLifecycle)
|
||||
debugPrint('Reactivating $this (child of $_parent).');
|
||||
return true;
|
||||
});
|
||||
assert(_debugLifecycleState == _ElementLifecycle.inactive);
|
||||
assert(widget != null);
|
||||
assert(depth != null);
|
||||
@ -2027,7 +2277,7 @@ abstract class ComponentElement extends BuildableElement {
|
||||
built = _builder(this);
|
||||
debugWidgetBuilderValue(widget, built);
|
||||
} catch (e, stack) {
|
||||
_debugReportException('building $_widget', e, stack);
|
||||
_debugReportException('building $this', e, stack);
|
||||
built = new ErrorWidget(e);
|
||||
} finally {
|
||||
// We delay marking the element as clean until after calling _builder so
|
||||
@ -2039,7 +2289,7 @@ abstract class ComponentElement extends BuildableElement {
|
||||
_child = updateChild(_child, built, slot);
|
||||
assert(_child != null);
|
||||
} catch (e, stack) {
|
||||
_debugReportException('building $_widget', e, stack);
|
||||
_debugReportException('building $this', e, stack);
|
||||
built = new ErrorWidget(e);
|
||||
_child = updateChild(null, built, slot);
|
||||
}
|
||||
@ -2247,6 +2497,7 @@ abstract class _ProxyElement extends ComponentElement {
|
||||
void notifyClients(_ProxyWidget oldWidget);
|
||||
}
|
||||
|
||||
/// Base class for instantiations of [ParentDataWidget] subclasses.
|
||||
class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
|
||||
ParentDataElement(ParentDataWidget<T> widget) : super(widget);
|
||||
|
||||
@ -2297,7 +2548,7 @@ class ParentDataElement<T extends RenderObjectWidget> extends _ProxyElement {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Base class for instantiations of [InheritedWidget] subclasses.
|
||||
class InheritedElement extends _ProxyElement {
|
||||
InheritedElement(InheritedWidget widget) : super(widget);
|
||||
|
||||
@ -2356,14 +2607,14 @@ class InheritedElement extends _ProxyElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for instantiations of RenderObjectWidget subclasses
|
||||
/// Base class for instantiations of [RenderObjectWidget] subclasses.
|
||||
abstract class RenderObjectElement extends BuildableElement {
|
||||
RenderObjectElement(RenderObjectWidget widget) : super(widget);
|
||||
|
||||
@override
|
||||
RenderObjectWidget get widget => super.widget;
|
||||
|
||||
/// The underlying [RenderObject] for this element
|
||||
/// The underlying [RenderObject] for this element.
|
||||
@override
|
||||
RenderObject get renderObject => _renderObject;
|
||||
RenderObject _renderObject;
|
||||
@ -2638,27 +2889,39 @@ abstract class RenderObjectElement extends BuildableElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiation of RenderObjectWidgets at the root of the tree
|
||||
/// Instantiation of RenderObjectWidgets at the root of the tree.
|
||||
///
|
||||
/// Only root elements may have their owner set explicitly. All other
|
||||
/// elements inherit their owner from their parent.
|
||||
abstract class RootRenderObjectElement extends RenderObjectElement {
|
||||
RootRenderObjectElement(RenderObjectWidget widget): super(widget);
|
||||
|
||||
/// Set the owner of the element. The owner will be propagated to all the
|
||||
/// descendants of this element.
|
||||
///
|
||||
/// The owner manages the dirty elements list.
|
||||
///
|
||||
/// The [WidgetsBinding] introduces the primary owner,
|
||||
/// [WidgetsBinding.buildOwner], and assigns it to the widget tree in the call
|
||||
/// to [runApp]. The binding is responsible for driving the build pipeline by
|
||||
/// calling the build owner's [BuildOwner.buildDirtyElements] method. See
|
||||
/// [WidgetsBinding.beginFrame].
|
||||
void assignOwner(BuildOwner owner) {
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
@override
|
||||
void mount(Element parent, dynamic newSlot) {
|
||||
// Root elements should never have parents
|
||||
// Root elements should never have parents.
|
||||
assert(parent == null);
|
||||
assert(newSlot == null);
|
||||
super.mount(parent, newSlot);
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiation of RenderObjectWidgets that have no children
|
||||
/// Instantiation of [RenderObjectWidget]s that have no children.
|
||||
///
|
||||
/// Such widgets are expected to inherit from [LeafRenderObjectWidget].
|
||||
class LeafRenderObjectElement extends RenderObjectElement {
|
||||
LeafRenderObjectElement(LeafRenderObjectWidget widget): super(widget);
|
||||
|
||||
@ -2678,7 +2941,13 @@ class LeafRenderObjectElement extends RenderObjectElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiation of RenderObjectWidgets that have up to one child
|
||||
/// Instantiation of [RenderObjectWidget]s that have up to one child.
|
||||
///
|
||||
/// The child is optional.
|
||||
///
|
||||
/// This element subclass can be used for RenderObjectWidgets whose
|
||||
/// RenderObjects use the [RenderObjectWithChildMixin] mixin. Such widgets are
|
||||
/// expected to inherit from [SingleChildRenderObjectWidget].
|
||||
class SingleChildRenderObjectElement extends RenderObjectElement {
|
||||
SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget) : super(widget);
|
||||
|
||||
@ -2736,7 +3005,12 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiation of RenderObjectWidgets that can have a list of children
|
||||
/// Instantiation of [RenderObjectWidget]s that can have a list of children.
|
||||
///
|
||||
/// This element subclass can be used for RenderObjectWidgets whose
|
||||
/// RenderObjects use the [ContainerRenderObjectMixin] mixin with a parent data
|
||||
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
|
||||
/// are expected to inherit from [MultiChildRenderObjectWidget].
|
||||
class MultiChildRenderObjectElement extends RenderObjectElement {
|
||||
MultiChildRenderObjectElement(MultiChildRenderObjectWidget widget) : super(widget) {
|
||||
assert(!debugChildrenHaveDuplicateKeys(widget, widget.children));
|
||||
@ -2793,8 +3067,8 @@ class MultiChildRenderObjectElement extends RenderObjectElement {
|
||||
super.mount(parent, newSlot);
|
||||
_children = new List<Element>(widget.children.length);
|
||||
Element previousChild;
|
||||
for (int i = 0; i < _children.length; ++i) {
|
||||
Element newChild = inflateWidget(widget.children[i], previousChild);
|
||||
for (int i = 0; i < _children.length; i += 1) {
|
||||
final Element newChild = inflateWidget(widget.children[i], previousChild);
|
||||
_children[i] = newChild;
|
||||
previousChild = newChild;
|
||||
}
|
||||
|
@ -103,8 +103,8 @@ abstract class Route<T> {
|
||||
/// route), then [isCurrent] will also be true.
|
||||
///
|
||||
/// If a later route is entirely opaque, then the route will be active but not
|
||||
/// rendered. In particular, it's possible for a route to be active but for
|
||||
/// stateful widgets within the route to not be instantiated.
|
||||
/// rendered. It is even possible for the route to be active but for the stateful
|
||||
/// widgets within the route to not be instatiated. See [ModalRoute.maintainState].
|
||||
bool get isActive {
|
||||
if (_navigator == null)
|
||||
return false;
|
||||
@ -118,7 +118,7 @@ class RouteSettings {
|
||||
/// Creates data used to construct routes.
|
||||
const RouteSettings({
|
||||
this.name,
|
||||
this.isInitialRoute: false
|
||||
this.isInitialRoute: false,
|
||||
});
|
||||
|
||||
/// The name of the route (e.g., "/settings").
|
||||
|
@ -2,9 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'debug.dart';
|
||||
import 'framework.dart';
|
||||
|
||||
/// A place in an [Overlay] that can contain a widget.
|
||||
@ -31,6 +35,14 @@ import 'framework.dart';
|
||||
/// [Draggable] removes the entry from the overlay to remove the drag avatar
|
||||
/// from view.
|
||||
///
|
||||
/// By default, if there is an entirely [opaque] entry over this one, then this
|
||||
/// one will not be included in the widget tree (in particular, stateful widgets
|
||||
/// within the overlay entry will not be instantiated). To ensure that your
|
||||
/// overlay entry is still built even if it is not visible, set [maintainState]
|
||||
/// to true. This is more expensive, so should be done with care. In particular,
|
||||
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
|
||||
/// call [setState], the user's battery will be drained unnecessarily.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Overlay]
|
||||
@ -45,12 +57,16 @@ class OverlayEntry {
|
||||
/// call [remove] on the overlay entry itself.
|
||||
OverlayEntry({
|
||||
@required this.builder,
|
||||
bool opaque: false
|
||||
}) : _opaque = opaque {
|
||||
bool opaque: false,
|
||||
bool maintainState: false,
|
||||
}) : _opaque = opaque, _maintainState = maintainState {
|
||||
assert(builder != null);
|
||||
assert(opaque != null);
|
||||
assert(maintainState != null);
|
||||
}
|
||||
|
||||
/// This entry will include the widget built by this builder in the overlay at the entry's position.
|
||||
/// This entry will include the widget built by this builder in the overlay at
|
||||
/// the entry's position.
|
||||
///
|
||||
/// To cause this builder to be called again, call [markNeedsBuild] on this
|
||||
/// overlay entry.
|
||||
@ -58,8 +74,9 @@ class OverlayEntry {
|
||||
|
||||
/// Whether this entry occludes the entire overlay.
|
||||
///
|
||||
/// If an entry claims to be opaque, the overlay will skip building all the
|
||||
/// entries below that entry for efficiency.
|
||||
/// If an entry claims to be opaque, then, for efficiency, the overlay will
|
||||
/// skip building entries below that entry unless they have [maintainState]
|
||||
/// set.
|
||||
bool get opaque => _opaque;
|
||||
bool _opaque;
|
||||
set opaque (bool value) {
|
||||
@ -70,6 +87,31 @@ class OverlayEntry {
|
||||
_overlay._didChangeEntryOpacity();
|
||||
}
|
||||
|
||||
/// Whether this entry must be included in the tree even if there is a fully
|
||||
/// [opaque] entry above it.
|
||||
///
|
||||
/// By default, if there is an entirely [opaque] entry over this one, then this
|
||||
/// one will not be included in the widget tree (in particular, stateful widgets
|
||||
/// within the overlay entry will not be instantiated). To ensure that your
|
||||
/// overlay entry is still built even if it is not visible, set [maintainState]
|
||||
/// to true. This is more expensive, so should be done with care. In particular,
|
||||
/// if widgets in an overlay entry with [maintainState] set to true repeatedly
|
||||
/// call [setState], the user's battery will be drained unnecessarily.
|
||||
///
|
||||
/// This is used by the [Navigator] and [Route] objects to ensure that routes
|
||||
/// are kept around even when in the background, so that [Future]s promised
|
||||
/// from subsequent routes will be handled properly when they complete.
|
||||
bool get maintainState => _maintainState;
|
||||
bool _maintainState;
|
||||
set maintainState (bool value) {
|
||||
assert(_maintainState != null);
|
||||
if (_maintainState == value)
|
||||
return;
|
||||
_maintainState = value;
|
||||
assert(_overlay != null);
|
||||
_overlay._didChangeEntryOpacity();
|
||||
}
|
||||
|
||||
OverlayState _overlay;
|
||||
final GlobalKey<_OverlayEntryState> _key = new GlobalKey<_OverlayEntryState>();
|
||||
|
||||
@ -87,11 +129,13 @@ class OverlayEntry {
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType@$hashCode(opaque: $opaque)';
|
||||
String toString() => '$runtimeType@$hashCode(opaque: $opaque; maintainState: $maintainState)';
|
||||
}
|
||||
|
||||
class _OverlayEntry extends StatefulWidget {
|
||||
_OverlayEntry(OverlayEntry entry) : entry = entry, super(key: entry._key);
|
||||
_OverlayEntry(OverlayEntry entry) : entry = entry, super(key: entry._key) {
|
||||
assert(entry != null);
|
||||
}
|
||||
|
||||
final OverlayEntry entry;
|
||||
|
||||
@ -101,7 +145,9 @@ class _OverlayEntry extends StatefulWidget {
|
||||
|
||||
class _OverlayEntryState extends State<_OverlayEntry> {
|
||||
@override
|
||||
Widget build(BuildContext context) => config.entry.builder(context);
|
||||
Widget build(BuildContext context) {
|
||||
return config.entry.builder(context);
|
||||
}
|
||||
|
||||
void _markNeedsBuild() {
|
||||
setState(() { /* the state that changed is in the builder */ });
|
||||
@ -227,7 +273,8 @@ class OverlayState extends State<Overlay> {
|
||||
setState(() { /* entry was removed */ });
|
||||
}
|
||||
|
||||
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an opaque entry).
|
||||
/// (DEBUG ONLY) Check whether a given entry is visible (i.e., not behind an
|
||||
/// opaque entry).
|
||||
///
|
||||
/// This is an O(N) algorithm, and should not be necessary except for debug
|
||||
/// asserts. To avoid people depending on it, this function is implemented
|
||||
@ -259,16 +306,26 @@ class OverlayState extends State<Overlay> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> backwardsChildren = <Widget>[];
|
||||
|
||||
for (int i = _entries.length - 1; i >= 0; --i) {
|
||||
// These lists are filled backwards. For the offstage children that
|
||||
// does not matter since they aren't rendered, but for the onstage
|
||||
// children we reverse the list below before adding it to the tree.
|
||||
final List<Widget> onstageChildren = <Widget>[];
|
||||
final List<Widget> offstageChildren = <Widget>[];
|
||||
bool onstage = true;
|
||||
for (int i = _entries.length - 1; i >= 0; i -= 1) {
|
||||
OverlayEntry entry = _entries[i];
|
||||
backwardsChildren.add(new _OverlayEntry(entry));
|
||||
if (entry.opaque)
|
||||
break;
|
||||
if (onstage) {
|
||||
onstageChildren.add(new _OverlayEntry(entry));
|
||||
if (entry.opaque)
|
||||
onstage = false;
|
||||
} else if (entry.maintainState) {
|
||||
offstageChildren.add(new _OverlayEntry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
return new Stack(children: backwardsChildren.reversed.toList(growable: false));
|
||||
return new _Theatre(
|
||||
onstage: new Stack(children: onstageChildren.reversed.toList(growable: false)),
|
||||
offstage: offstageChildren,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -277,3 +334,214 @@ class OverlayState extends State<Overlay> {
|
||||
description.add('entries: $_entries');
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that has one [onstage] child which is visible, and one or more
|
||||
/// [offstage] widgets which are kept alive, and are built, but are not laid out
|
||||
/// or painted.
|
||||
///
|
||||
/// The onstage widget must be a Stack.
|
||||
///
|
||||
/// For convenience, it is legal to use [Positioned] widgets around the offstage
|
||||
/// widgets.
|
||||
class _Theatre extends RenderObjectWidget {
|
||||
_Theatre({
|
||||
this.onstage,
|
||||
this.offstage,
|
||||
}) {
|
||||
assert(offstage != null);
|
||||
assert(!offstage.any((Widget child) => child == null));
|
||||
}
|
||||
|
||||
final Stack onstage;
|
||||
|
||||
final List<Widget> offstage;
|
||||
|
||||
@override
|
||||
_TheatreElement createElement() => new _TheatreElement(this);
|
||||
|
||||
@override
|
||||
_RenderTheatre createRenderObject(BuildContext context) => new _RenderTheatre();
|
||||
}
|
||||
|
||||
class _TheatreElement extends RenderObjectElement {
|
||||
_TheatreElement(_Theatre widget) : super(widget) {
|
||||
assert(!debugChildrenHaveDuplicateKeys(widget, widget.offstage));
|
||||
}
|
||||
|
||||
@override
|
||||
_Theatre get widget => super.widget;
|
||||
|
||||
@override
|
||||
_RenderTheatre get renderObject => super.renderObject;
|
||||
|
||||
Element _onstage;
|
||||
static final Object _onstageSlot = new Object();
|
||||
|
||||
List<Element> _offstage;
|
||||
final Set<Element> _detachedOffstageChildren = new HashSet<Element>();
|
||||
|
||||
@override
|
||||
void insertChildRenderObject(RenderBox child, dynamic slot) {
|
||||
if (slot == _onstageSlot) {
|
||||
assert(child is RenderStack);
|
||||
renderObject.child = child;
|
||||
} else {
|
||||
assert(slot == null || slot is Element);
|
||||
renderObject.insert(child, after: slot?.renderObject);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void moveChildRenderObject(RenderBox child, dynamic slot) {
|
||||
if (slot == _onstageSlot) {
|
||||
renderObject.remove(child);
|
||||
assert(child is RenderStack);
|
||||
renderObject.child = child;
|
||||
} else {
|
||||
assert(slot == null || slot is Element);
|
||||
if (renderObject.child == child) {
|
||||
renderObject.child = null;
|
||||
renderObject.insert(child, after: slot?.renderObject);
|
||||
} else {
|
||||
renderObject.move(child, after: slot?.renderObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void removeChildRenderObject(RenderBox child) {
|
||||
if (renderObject.child == child) {
|
||||
renderObject.child = null;
|
||||
} else {
|
||||
renderObject.remove(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(ElementVisitor visitor) {
|
||||
if (_onstage != null)
|
||||
visitor(_onstage);
|
||||
for (Element child in _offstage) {
|
||||
if (!_detachedOffstageChildren.contains(child))
|
||||
visitor(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(ElementVisitor visitor) {
|
||||
if (_onstage != null)
|
||||
visitor(_onstage);
|
||||
}
|
||||
|
||||
@override
|
||||
bool detachChild(Element child) {
|
||||
if (child == _onstage) {
|
||||
_onstage = null;
|
||||
} else {
|
||||
_detachedOffstageChildren.add(child);
|
||||
}
|
||||
deactivateChild(child);
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void mount(Element parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
|
||||
_offstage = new List<Element>(widget.offstage.length);
|
||||
Element previousChild;
|
||||
for (int i = 0; i < _offstage.length; i += 1) {
|
||||
final Element newChild = inflateWidget(widget.offstage[i], previousChild);
|
||||
_offstage[i] = newChild;
|
||||
previousChild = newChild;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void update(_Theatre newWidget) {
|
||||
super.update(newWidget);
|
||||
assert(widget == newWidget);
|
||||
_onstage = updateChild(_onstage, widget.onstage, _onstageSlot);
|
||||
_offstage = updateChildren(_offstage, widget.offstage, detachedChildren: _detachedOffstageChildren);
|
||||
_detachedOffstageChildren.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// A render object which lays out and paints one subtree while keeping a list
|
||||
// of other subtrees alive but not laid out or painted (the "zombie" children).
|
||||
//
|
||||
// The subtree that is laid out and painted must be a [RenderStack].
|
||||
//
|
||||
// This class uses [StackParentData] objects for its parent data so that the
|
||||
// children of its primary subtree's stack can be moved to this object's list
|
||||
// of zombie children without changing their parent data objects.
|
||||
class _RenderTheatre extends RenderBox
|
||||
with RenderObjectWithChildMixin<RenderStack>, RenderProxyBoxMixin,
|
||||
ContainerRenderObjectMixin<RenderBox, StackParentData> {
|
||||
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
if (child.parentData is! StackParentData)
|
||||
child.parentData = new StackParentData();
|
||||
}
|
||||
|
||||
// Because both RenderObjectWithChildMixin and ContainerRenderObjectMixin
|
||||
// define redepthChildren, visitChildren and debugDescribeChildren and don't
|
||||
// call super, we have to define them again here to make sure the work of both
|
||||
// is done.
|
||||
//
|
||||
// We chose to put ContainerRenderObjectMixin last in the inheritance chain so
|
||||
// that we can call super to hit its more complex definitions of
|
||||
// redepthChildren and visitChildren, and then duplicate the more trivial
|
||||
// definition from RenderObjectWithChildMixin inline in our version here.
|
||||
//
|
||||
// This code duplication is suboptimal.
|
||||
// TODO(ianh): Replace this with a better solution once https://github.com/dart-lang/sdk/issues/27100 is fixed
|
||||
//
|
||||
// For debugDescribeChildren we just roll our own because otherwise the line
|
||||
// drawings won't really work as well.
|
||||
|
||||
@override
|
||||
void redepthChildren() {
|
||||
if (child != null)
|
||||
redepthChild(child);
|
||||
super.redepthChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildren(RenderObjectVisitor visitor) {
|
||||
if (child != null)
|
||||
visitor(child);
|
||||
super.visitChildren(visitor);
|
||||
}
|
||||
|
||||
@override
|
||||
String debugDescribeChildren(String prefix) {
|
||||
String result = '';
|
||||
if (child != null)
|
||||
result += '$prefix \u2502\n${child.toStringDeep('$prefix \u251C\u2500onstage: ', '$prefix \u254E')}';
|
||||
if (firstChild != null) {
|
||||
RenderBox child = firstChild;
|
||||
int count = 1;
|
||||
while (child != lastChild) {
|
||||
result += '${child.toStringDeep("$prefix \u254E\u254Coffstage $count: ", "$prefix \u254E")}';
|
||||
count += 1;
|
||||
final StackParentData childParentData = child.parentData;
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
if (child != null) {
|
||||
assert(child == lastChild);
|
||||
result += '${child.toStringDeep("$prefix \u2514\u254Coffstage $count: ", "$prefix ")}';
|
||||
}
|
||||
} else {
|
||||
result += '$prefix \u2514\u254Cno offstage children';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||
if (child != null)
|
||||
visitor(child);
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ const Color _kTransparent = const Color(0x00000000);
|
||||
/// A route that displays widgets in the [Navigator]'s [Overlay].
|
||||
abstract class OverlayRoute<T> extends Route<T> {
|
||||
/// Subclasses should override this getter to return the builders for the overlay.
|
||||
List<WidgetBuilder> get builders;
|
||||
Iterable<OverlayEntry> createOverlayEntries();
|
||||
|
||||
/// The entries this route has placed in the overlay.
|
||||
@override
|
||||
@ -30,8 +30,7 @@ abstract class OverlayRoute<T> extends Route<T> {
|
||||
@override
|
||||
void install(OverlayEntry insertionPoint) {
|
||||
assert(_overlayEntries.isEmpty);
|
||||
for (WidgetBuilder builder in builders)
|
||||
_overlayEntries.add(new OverlayEntry(builder: builder));
|
||||
_overlayEntries.addAll(createOverlayEntries());
|
||||
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
|
||||
}
|
||||
|
||||
@ -415,7 +414,7 @@ class _ModalScopeState extends State<_ModalScope> {
|
||||
Widget build(BuildContext context) {
|
||||
return new Focus(
|
||||
key: config.route.focusKey,
|
||||
child: new OffStage(
|
||||
child: new Offstage(
|
||||
offstage: config.route.offstage,
|
||||
child: new IgnorePointer(
|
||||
ignoring: config.route.animation?.status == AnimationStatus.reverse,
|
||||
@ -577,6 +576,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
/// be transparent.
|
||||
Color get barrierColor;
|
||||
|
||||
/// Whether the route should remain in memory when it is inactive. If this is
|
||||
/// true, then the route is maintained, so that any futures it is holding from
|
||||
/// the next route will properly resolve when the next route pops. If this is
|
||||
/// not necessary, this can be set to false to allow the framework to entirely
|
||||
/// discard the route's widget hierarchy when it is not visible.
|
||||
bool get maintainState;
|
||||
|
||||
|
||||
// The API for _ModalScope and HeroController
|
||||
|
||||
@ -654,10 +660,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
}
|
||||
|
||||
@override
|
||||
List<WidgetBuilder> get builders => <WidgetBuilder>[
|
||||
_buildModalBarrier,
|
||||
_buildModalScope
|
||||
];
|
||||
Iterable<OverlayEntry> createOverlayEntries() sync* {
|
||||
yield new OverlayEntry(builder: _buildModalBarrier);
|
||||
yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType($settings, animation: $_animation)';
|
||||
@ -671,6 +677,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
|
||||
@override
|
||||
bool get opaque => false;
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
@override
|
||||
void didChangeNext(Route<dynamic> nextRoute) {
|
||||
assert(nextRoute is! PageRoute<dynamic>);
|
||||
|
@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Navigator.push works within a PopupMenuButton ', (WidgetTester tester) async {
|
||||
testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
routes: <String, WidgetBuilder> {
|
||||
@ -46,9 +46,9 @@ void main() {
|
||||
expect(find.text('Next'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('One'));
|
||||
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1)); // finish the menu animation
|
||||
await tester.pump(); // return the future
|
||||
await tester.pump(); // start the navigation
|
||||
await tester.pump(const Duration(seconds: 1)); // end the navigation
|
||||
|
||||
expect(find.text('One'), findsNothing);
|
||||
expect(find.text('Next'), findsOneWidget);
|
||||
|
@ -15,7 +15,7 @@ void main() {
|
||||
// viewport incoming constraints are tight 800x600
|
||||
// viewport is vertical by default
|
||||
RenderBox root = new RenderViewport(
|
||||
child: new RenderOffStage(
|
||||
child: new RenderOffstage(
|
||||
child: new RenderCustomPaint(
|
||||
painter: new TestCallbackPainter(
|
||||
onPaint: () { painted = true; }
|
||||
|
@ -59,20 +59,20 @@ void main() {
|
||||
|
||||
// the initial setup.
|
||||
|
||||
expect(find.byKey(firstKey), isOnStage);
|
||||
expect(find.byKey(firstKey), isOnstage);
|
||||
expect(find.byKey(firstKey), isInCard);
|
||||
expect(find.byKey(secondKey), findsNothing);
|
||||
|
||||
await tester.tap(find.text('two'));
|
||||
await tester.pump(); // begin navigation
|
||||
|
||||
// at this stage, the second route is off-stage, so that we can form the
|
||||
// at this stage, the second route is offstage, so that we can form the
|
||||
// hero party.
|
||||
|
||||
expect(find.byKey(firstKey), isOnStage);
|
||||
expect(find.byKey(firstKey), isOnstage);
|
||||
expect(find.byKey(firstKey), isInCard);
|
||||
expect(find.byKey(secondKey), isOffStage);
|
||||
expect(find.byKey(secondKey), isInCard);
|
||||
expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
|
||||
expect(find.byKey(secondKey, skipOffstage: false), isInCard);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
@ -80,7 +80,7 @@ void main() {
|
||||
// seeing them at t=16ms. The original page no longer contains the hero.
|
||||
|
||||
expect(find.byKey(firstKey), findsNothing);
|
||||
expect(find.byKey(secondKey), isOnStage);
|
||||
expect(find.byKey(secondKey), isOnstage);
|
||||
expect(find.byKey(secondKey), isNotInCard);
|
||||
|
||||
await tester.pump();
|
||||
@ -88,16 +88,17 @@ void main() {
|
||||
// t=32ms for the journey. Surely they are still at it.
|
||||
|
||||
expect(find.byKey(firstKey), findsNothing);
|
||||
expect(find.byKey(secondKey), isOnStage);
|
||||
expect(find.byKey(secondKey), isOnstage);
|
||||
expect(find.byKey(secondKey), isNotInCard);
|
||||
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// t=1.032s for the journey. The journey has ended (it ends this frame, in
|
||||
// fact). The hero should now be in the new page, on-stage.
|
||||
// fact). The hero should now be in the new page, onstage. The original
|
||||
// widget will be back as well now (though not visible).
|
||||
|
||||
expect(find.byKey(firstKey), findsNothing);
|
||||
expect(find.byKey(secondKey), isOnStage);
|
||||
expect(find.byKey(secondKey), isOnstage);
|
||||
expect(find.byKey(secondKey), isInCard);
|
||||
|
||||
await tester.pump();
|
||||
@ -105,7 +106,7 @@ void main() {
|
||||
// Should not change anything.
|
||||
|
||||
expect(find.byKey(firstKey), findsNothing);
|
||||
expect(find.byKey(secondKey), isOnStage);
|
||||
expect(find.byKey(secondKey), isOnstage);
|
||||
expect(find.byKey(secondKey), isInCard);
|
||||
|
||||
// Now move on to view 3
|
||||
@ -113,13 +114,13 @@ void main() {
|
||||
await tester.tap(find.text('three'));
|
||||
await tester.pump(); // begin navigation
|
||||
|
||||
// at this stage, the second route is off-stage, so that we can form the
|
||||
// at this stage, the second route is offstage, so that we can form the
|
||||
// hero party.
|
||||
|
||||
expect(find.byKey(secondKey), isOnStage);
|
||||
expect(find.byKey(secondKey), isOnstage);
|
||||
expect(find.byKey(secondKey), isInCard);
|
||||
expect(find.byKey(thirdKey), isOffStage);
|
||||
expect(find.byKey(thirdKey), isInCard);
|
||||
expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
|
||||
expect(find.byKey(thirdKey, skipOffstage: false), isInCard);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
@ -127,7 +128,7 @@ void main() {
|
||||
// seeing them at t=16ms. The original page no longer contains the hero.
|
||||
|
||||
expect(find.byKey(secondKey), findsNothing);
|
||||
expect(find.byKey(thirdKey), isOnStage);
|
||||
expect(find.byKey(thirdKey), isOnstage);
|
||||
expect(find.byKey(thirdKey), isNotInCard);
|
||||
|
||||
await tester.pump();
|
||||
@ -135,16 +136,16 @@ void main() {
|
||||
// t=32ms for the journey. Surely they are still at it.
|
||||
|
||||
expect(find.byKey(secondKey), findsNothing);
|
||||
expect(find.byKey(thirdKey), isOnStage);
|
||||
expect(find.byKey(thirdKey), isOnstage);
|
||||
expect(find.byKey(thirdKey), isNotInCard);
|
||||
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// t=1.032s for the journey. The journey has ended (it ends this frame, in
|
||||
// fact). The hero should now be in the new page, on-stage.
|
||||
// fact). The hero should now be in the new page, onstage.
|
||||
|
||||
expect(find.byKey(secondKey), findsNothing);
|
||||
expect(find.byKey(thirdKey), isOnStage);
|
||||
expect(find.byKey(thirdKey), isOnstage);
|
||||
expect(find.byKey(thirdKey), isInCard);
|
||||
|
||||
await tester.pump();
|
||||
@ -152,7 +153,7 @@ void main() {
|
||||
// Should not change anything.
|
||||
|
||||
expect(find.byKey(secondKey), findsNothing);
|
||||
expect(find.byKey(thirdKey), isOnStage);
|
||||
expect(find.byKey(thirdKey), isOnstage);
|
||||
expect(find.byKey(thirdKey), isInCard);
|
||||
});
|
||||
|
||||
@ -187,8 +188,8 @@ void main() {
|
||||
|
||||
final Duration duration = const Duration(milliseconds: 300);
|
||||
final Curve curve = Curves.fastOutSlowIn;
|
||||
final double initialHeight = tester.getSize(find.byKey(firstKey)).height;
|
||||
final double finalHeight = tester.getSize(find.byKey(secondKey)).height;
|
||||
final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
|
||||
final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
|
||||
final double deltaHeight = finalHeight - initialHeight;
|
||||
final double epsilon = 0.001;
|
||||
|
||||
@ -267,18 +268,18 @@ void main() {
|
||||
navigator.pushNamed('/next');
|
||||
|
||||
expect(log, isEmpty);
|
||||
await tester.tap(find.text('foo'));
|
||||
await tester.tap(find.text('foo', skipOffstage: false));
|
||||
expect(log, isEmpty);
|
||||
|
||||
await tester.pump(new Duration(milliseconds: 10));
|
||||
await tester.tap(find.text('foo'));
|
||||
await tester.tap(find.text('foo', skipOffstage: false));
|
||||
expect(log, isEmpty);
|
||||
await tester.tap(find.text('bar'));
|
||||
await tester.tap(find.text('bar', skipOffstage: false));
|
||||
expect(log, isEmpty);
|
||||
|
||||
await tester.pump(new Duration(milliseconds: 10));
|
||||
expect(find.text('foo'), findsNothing);
|
||||
await tester.tap(find.text('bar'));
|
||||
await tester.tap(find.text('bar', skipOffstage: false));
|
||||
expect(log, isEmpty);
|
||||
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
@ -69,33 +69,51 @@ class ThirdWidget extends StatelessWidget {
|
||||
void main() {
|
||||
testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async {
|
||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => new FirstWidget(),
|
||||
'/second': (BuildContext context) => new SecondWidget(),
|
||||
'/': (BuildContext context) => new FirstWidget(), // X
|
||||
'/second': (BuildContext context) => new SecondWidget(), // Y
|
||||
};
|
||||
|
||||
await tester.pumpWidget(new MaterialApp(routes: routes));
|
||||
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsNothing);
|
||||
expect(find.text('Y', skipOffstage: false), findsNothing);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
await tester.pump();
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y', skipOffstage: false), isOffstage);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('X'), findsNothing);
|
||||
expect(find.text('X', skipOffstage: false), findsOneWidget);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Y'));
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('X'), findsNothing);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsNothing);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y'), findsOneWidget);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('X'), findsOneWidget);
|
||||
expect(find.text('Y', skipOffstage: false), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async {
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class TestTransition extends AnimatedWidget {
|
||||
TestTransition({
|
||||
@ -26,7 +27,7 @@ class TestTransition extends AnimatedWidget {
|
||||
}
|
||||
|
||||
class TestRoute<T> extends PageRoute<T> {
|
||||
TestRoute({ this.child, RouteSettings settings}) : super(settings: settings);
|
||||
TestRoute({ this.child, RouteSettings settings }) : super(settings: settings);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@ -36,6 +37,9 @@ class TestRoute<T> extends PageRoute<T> {
|
||||
@override
|
||||
Color get barrierColor => null;
|
||||
|
||||
@override
|
||||
bool get maintainState => false;
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
|
||||
return child;
|
||||
@ -50,21 +54,21 @@ void main() {
|
||||
|
||||
GlobalKey insideKey = new GlobalKey();
|
||||
|
||||
String state() {
|
||||
String state({ bool skipOffstage: true }) {
|
||||
String result = '';
|
||||
if (tester.any(find.text('A')))
|
||||
if (tester.any(find.text('A', skipOffstage: skipOffstage)))
|
||||
result += 'A';
|
||||
if (tester.any(find.text('B')))
|
||||
if (tester.any(find.text('B', skipOffstage: skipOffstage)))
|
||||
result += 'B';
|
||||
if (tester.any(find.text('C')))
|
||||
if (tester.any(find.text('C', skipOffstage: skipOffstage)))
|
||||
result += 'C';
|
||||
if (tester.any(find.text('D')))
|
||||
if (tester.any(find.text('D', skipOffstage: skipOffstage)))
|
||||
result += 'D';
|
||||
if (tester.any(find.text('E')))
|
||||
if (tester.any(find.text('E', skipOffstage: skipOffstage)))
|
||||
result += 'E';
|
||||
if (tester.any(find.text('F')))
|
||||
if (tester.any(find.text('F', skipOffstage: skipOffstage)))
|
||||
result += 'F';
|
||||
if (tester.any(find.text('G')))
|
||||
if (tester.any(find.text('G', skipOffstage: skipOffstage)))
|
||||
result += 'G';
|
||||
return result;
|
||||
}
|
||||
@ -112,7 +116,8 @@ void main() {
|
||||
navigator.pushNamed('/2');
|
||||
expect(state(), equals('BC')); // transition 1->2 is not yet built
|
||||
await tester.pump();
|
||||
expect(state(), equals('BCE')); // transition 1->2 is at 0.0
|
||||
expect(state(), equals('BC')); // transition 1->2 is at 0.0
|
||||
expect(state(skipOffstage: false), equals('BCE')); // E is offstage
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('BCE')); // transition 1->2 is at 0.4
|
||||
@ -122,7 +127,7 @@ void main() {
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('E')); // transition 1->2 is at 1.0
|
||||
|
||||
expect(state(skipOffstage: false), equals('E')); // B and C are gone, the route is inactive with maintainState=false
|
||||
|
||||
navigator.pop();
|
||||
expect(state(), equals('E')); // transition 1<-2 is at 1.0, just reversed
|
||||
@ -135,10 +140,12 @@ void main() {
|
||||
navigator.pushNamed('/3');
|
||||
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6
|
||||
await tester.pump();
|
||||
expect(state(), equals('BDEF')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
|
||||
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
|
||||
expect(state(skipOffstage: false), equals('BDEF')); // F is offstage since we're at 0.0
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('BCEF')); // transition 1<-2 is at 0.2, 1->3 is at 0.4
|
||||
expect(state(skipOffstage: false), equals('BCEF')); // nothing secret going on here
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('BDF')); // transition 1<-2 is done, 1->3 is at 0.8
|
||||
@ -157,7 +164,8 @@ void main() {
|
||||
navigator.pushNamed('/4');
|
||||
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is not yet built
|
||||
await tester.pump();
|
||||
expect(state(), equals('BCFG')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
|
||||
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
|
||||
expect(state(skipOffstage: false), equals('BCFG')); // G is offstage
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('BCG')); // transition 1<-3 is done, 1->4 is at 0.4
|
||||
@ -167,6 +175,7 @@ void main() {
|
||||
|
||||
await tester.pump(kFourTenthsOfTheTransitionDuration);
|
||||
expect(state(), equals('G')); // transition 1->4 is done
|
||||
expect(state(skipOffstage: false), equals('G')); // route 1 is not around any more
|
||||
|
||||
});
|
||||
}
|
||||
|
@ -7,7 +7,9 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class TestOverlayRoute extends OverlayRoute<Null> {
|
||||
@override
|
||||
List<WidgetBuilder> get builders => <WidgetBuilder>[ _build ];
|
||||
Iterable<OverlayEntry> createOverlayEntries() sync* {
|
||||
yield new OverlayEntry(builder: _build);
|
||||
}
|
||||
Widget _build(BuildContext context) => new Text('Overlay');
|
||||
}
|
||||
|
||||
@ -22,7 +24,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(new MaterialApp(routes: routes));
|
||||
|
||||
expect(find.text('Home'), isOnStage);
|
||||
expect(find.text('Home'), isOnstage);
|
||||
expect(find.text('Settings'), findsNothing);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
@ -32,20 +34,20 @@ void main() {
|
||||
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Home'), isOnStage);
|
||||
expect(find.text('Settings'), isOffStage);
|
||||
expect(find.text('Home'), isOnstage);
|
||||
expect(find.text('Settings', skipOffstage: false), isOffstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
|
||||
expect(find.text('Home'), isOnStage);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Home'), isOnstage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
Navigator.push(containerKey2.currentContext, new TestOverlayRoute());
|
||||
@ -53,40 +55,40 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Overlay'), isOnStage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), isOnstage);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Overlay'), isOnStage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), isOnstage);
|
||||
|
||||
expect(Navigator.canPop(containerKey2.currentContext), isTrue);
|
||||
Navigator.pop(containerKey2.currentContext);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Home'), findsNothing);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
expect(Navigator.canPop(containerKey2.currentContext), isTrue);
|
||||
Navigator.pop(containerKey2.currentContext);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Home'), isOnStage);
|
||||
expect(find.text('Settings'), isOnStage);
|
||||
expect(find.text('Home'), isOnstage);
|
||||
expect(find.text('Settings'), isOnstage);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Home'), isOnStage);
|
||||
expect(find.text('Home'), isOnstage);
|
||||
expect(find.text('Settings'), findsNothing);
|
||||
expect(find.text('Overlay'), findsNothing);
|
||||
|
||||
|
@ -2,10 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThePositiveNumbers extends StatelessWidget {
|
||||
ThePositiveNumbers({ @required this.from });
|
||||
final int from;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new ScrollableLazyList(
|
||||
@ -13,104 +16,110 @@ class ThePositiveNumbers extends StatelessWidget {
|
||||
itemBuilder: (BuildContext context, int start, int count) {
|
||||
List<Widget> result = new List<Widget>();
|
||||
for (int index = start; index < start + count; index += 1)
|
||||
result.add(new Text('$index', key: new ValueKey<int>(index)));
|
||||
result.add(new Text('${index + from}', key: new ValueKey<int>(index)));
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Null> performTest(WidgetTester tester, bool maintainState) async {
|
||||
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(new Navigator(
|
||||
key: navigatorKey,
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
if (settings.name == '/') {
|
||||
return new MaterialPageRoute<Null>(
|
||||
settings: settings,
|
||||
builder: (_) => new Container(child: new ThePositiveNumbers(from: 0)),
|
||||
maintainState: maintainState,
|
||||
);
|
||||
} else if (settings.name == '/second') {
|
||||
return new MaterialPageRoute<Null>(
|
||||
settings: settings,
|
||||
builder: (_) => new Container(child: new ThePositiveNumbers(from: 10000)),
|
||||
maintainState: maintainState,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// zero, so we should have exactly 6 items, 0..5.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text('2'), findsOneWidget);
|
||||
expect(find.text('3'), findsOneWidget);
|
||||
expect(find.text('4'), findsOneWidget);
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
expect(find.text('6'), findsNothing);
|
||||
expect(find.text('10'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
ScrollableState targetState = tester.state(find.byType(Scrollable));
|
||||
targetState.scrollTo(1000.0);
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// 1000, so we should have exactly 6 items, 10..15.
|
||||
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('8'), findsNothing);
|
||||
expect(find.text('9'), findsNothing);
|
||||
expect(find.text('10'), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
expect(find.text('12'), findsOneWidget);
|
||||
expect(find.text('13'), findsOneWidget);
|
||||
expect(find.text('14'), findsOneWidget);
|
||||
expect(find.text('15'), findsOneWidget);
|
||||
expect(find.text('16'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
navigatorKey.currentState.pushNamed('/second');
|
||||
await tester.pump(); // navigating always takes two frames, one to start...
|
||||
await tester.pump(new Duration(seconds: 1)); // ...and one to end the transition
|
||||
|
||||
// the second list is now visible, starting at 10000
|
||||
expect(find.text('10000'), findsOneWidget);
|
||||
expect(find.text('10001'), findsOneWidget);
|
||||
expect(find.text('10002'), findsOneWidget);
|
||||
expect(find.text('10003'), findsOneWidget);
|
||||
expect(find.text('10004'), findsOneWidget);
|
||||
expect(find.text('10005'), findsOneWidget);
|
||||
expect(find.text('10006'), findsNothing);
|
||||
expect(find.text('10010'), findsNothing);
|
||||
expect(find.text('10100'), findsNothing);
|
||||
|
||||
navigatorKey.currentState.pop();
|
||||
await tester.pump(); // again, navigating always takes two frames
|
||||
|
||||
// Ensure we don't clamp the scroll offset even during the navigation.
|
||||
// https://github.com/flutter/flutter/issues/4883
|
||||
LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport));
|
||||
expect(viewport.scrollOffset, equals(1000.0));
|
||||
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// 1000, so we should have exactly 6 items, 10..15.
|
||||
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('8'), findsNothing);
|
||||
expect(find.text('9'), findsNothing);
|
||||
expect(find.text('10'), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
expect(find.text('12'), findsOneWidget);
|
||||
expect(find.text('13'), findsOneWidget);
|
||||
expect(find.text('14'), findsOneWidget);
|
||||
expect(find.text('15'), findsOneWidget);
|
||||
expect(find.text('16'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
|
||||
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
|
||||
await tester.pumpWidget(new Navigator(
|
||||
key: navigatorKey,
|
||||
onGenerateRoute: (RouteSettings settings) {
|
||||
if (settings.name == '/') {
|
||||
return new MaterialPageRoute<Null>(
|
||||
settings: settings,
|
||||
builder: (_) => new Container(child: new ThePositiveNumbers())
|
||||
);
|
||||
} else if (settings.name == '/second') {
|
||||
return new MaterialPageRoute<Null>(
|
||||
settings: settings,
|
||||
builder: (_) => new Container(child: new ThePositiveNumbers())
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// zero, so we should have exactly 6 items, 0..5.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text('2'), findsOneWidget);
|
||||
expect(find.text('3'), findsOneWidget);
|
||||
expect(find.text('4'), findsOneWidget);
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
expect(find.text('6'), findsNothing);
|
||||
expect(find.text('10'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
ScrollableState targetState = tester.state(find.byType(Scrollable));
|
||||
targetState.scrollTo(1000.0);
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// 1000, so we should have exactly 6 items, 10..15.
|
||||
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('8'), findsNothing);
|
||||
expect(find.text('9'), findsNothing);
|
||||
expect(find.text('10'), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
expect(find.text('12'), findsOneWidget);
|
||||
expect(find.text('13'), findsOneWidget);
|
||||
expect(find.text('14'), findsOneWidget);
|
||||
expect(find.text('15'), findsOneWidget);
|
||||
expect(find.text('16'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
navigatorKey.currentState.pushNamed('/second');
|
||||
await tester.pump(); // navigating always takes two frames
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// same as the first list again
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
expect(find.text('1'), findsOneWidget);
|
||||
expect(find.text('2'), findsOneWidget);
|
||||
expect(find.text('3'), findsOneWidget);
|
||||
expect(find.text('4'), findsOneWidget);
|
||||
expect(find.text('5'), findsOneWidget);
|
||||
expect(find.text('6'), findsNothing);
|
||||
expect(find.text('10'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
navigatorKey.currentState.pop();
|
||||
await tester.pump(); // navigating always takes two frames
|
||||
|
||||
// Ensure we don't clamp the scroll offset even during the navigation.
|
||||
// https://github.com/flutter/flutter/issues/4883
|
||||
LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport));
|
||||
expect(viewport.scrollOffset, equals(1000.0));
|
||||
|
||||
await tester.pump(new Duration(seconds: 1));
|
||||
|
||||
// we're 600 pixels high, each item is 100 pixels high, scroll position is
|
||||
// 1000, so we should have exactly 6 items, 10..15.
|
||||
|
||||
expect(find.text('0'), findsNothing);
|
||||
expect(find.text('8'), findsNothing);
|
||||
expect(find.text('9'), findsNothing);
|
||||
expect(find.text('10'), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
expect(find.text('12'), findsOneWidget);
|
||||
expect(find.text('13'), findsOneWidget);
|
||||
expect(find.text('14'), findsOneWidget);
|
||||
expect(find.text('15'), findsOneWidget);
|
||||
expect(find.text('16'), findsNothing);
|
||||
expect(find.text('100'), findsNothing);
|
||||
|
||||
await performTest(tester, true);
|
||||
await performTest(tester, false);
|
||||
});
|
||||
}
|
||||
|
@ -327,4 +327,45 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(log, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Reparenting with multiple moves', (WidgetTester tester) async {
|
||||
final GlobalKey key1 = new GlobalKey();
|
||||
final GlobalKey key2 = new GlobalKey();
|
||||
final GlobalKey key3 = new GlobalKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Row(
|
||||
children: <Widget>[
|
||||
new StateMarker(
|
||||
key: key1,
|
||||
child: new StateMarker(
|
||||
key: key2,
|
||||
child: new StateMarker(
|
||||
key: key3,
|
||||
child: new StateMarker(child: new Container(width: 100.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
new Row(
|
||||
children: <Widget>[
|
||||
new StateMarker(
|
||||
key: key2,
|
||||
child: new StateMarker(child: new Container(width: 100.0))
|
||||
),
|
||||
new StateMarker(
|
||||
key: key1,
|
||||
child: new StateMarker(
|
||||
key: key3,
|
||||
child: new StateMarker(child: new Container(width: 100.0))
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Provides an iterable that efficiently returns all the elements
|
||||
@ -15,13 +17,18 @@ import 'package:flutter/widgets.dart';
|
||||
/// The same applies to any iterable obtained indirectly through this
|
||||
/// one, for example the results of calling `where` on this iterable
|
||||
/// are also cached.
|
||||
Iterable<Element> collectAllElementsFrom(Element rootElement) {
|
||||
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement));
|
||||
Iterable<Element> collectAllElementsFrom(Element rootElement, {
|
||||
@required bool skipOffstage
|
||||
}) {
|
||||
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement, skipOffstage));
|
||||
}
|
||||
|
||||
class _DepthFirstChildIterator implements Iterator<Element> {
|
||||
_DepthFirstChildIterator(Element rootElement)
|
||||
: _stack = _reverseChildrenOf(rootElement).toList();
|
||||
_DepthFirstChildIterator(Element rootElement, bool skipOffstage)
|
||||
: skipOffstage = skipOffstage,
|
||||
_stack = _reverseChildrenOf(rootElement, skipOffstage).toList();
|
||||
|
||||
final bool skipOffstage;
|
||||
|
||||
Element _current;
|
||||
|
||||
@ -37,14 +44,18 @@ class _DepthFirstChildIterator implements Iterator<Element> {
|
||||
|
||||
_current = _stack.removeLast();
|
||||
// Stack children in reverse order to traverse first branch first
|
||||
_stack.addAll(_reverseChildrenOf(_current));
|
||||
_stack.addAll(_reverseChildrenOf(_current, skipOffstage));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static Iterable<Element> _reverseChildrenOf(Element element) {
|
||||
static Iterable<Element> _reverseChildrenOf(Element element, bool skipOffstage) {
|
||||
final List<Element> children = <Element>[];
|
||||
element.visitChildren(children.add);
|
||||
if (skipOffstage) {
|
||||
element.visitChildrenForSemantics(children.add);
|
||||
} else {
|
||||
element.visitChildren(children.add);
|
||||
}
|
||||
return children.reversed;
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class WidgetController {
|
||||
/// using [Iterator.moveNext].
|
||||
Iterable<Element> get allElements {
|
||||
TestAsyncUtils.guardSync();
|
||||
return collectAllElementsFrom(binding.renderViewElement);
|
||||
return collectAllElementsFrom(binding.renderViewElement, skipOffstage: false);
|
||||
}
|
||||
|
||||
/// The matching element in the widget tree.
|
||||
|
@ -27,7 +27,10 @@ class CommonFinders {
|
||||
/// Example:
|
||||
///
|
||||
/// expect(tester, hasWidget(find.text('Back')));
|
||||
Finder text(String text) => new _TextFinder(text);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder text(String text, { bool skipOffstage: true }) => new _TextFinder(text, skipOffstage: skipOffstage);
|
||||
|
||||
/// Looks for widgets that contain a [Text] descendant with `text`
|
||||
/// in it.
|
||||
@ -41,8 +44,11 @@ class CommonFinders {
|
||||
///
|
||||
/// // You can find and tap on it like this:
|
||||
/// tester.tap(find.widgetWithText(Button, 'Update'));
|
||||
Finder widgetWithText(Type widgetType, String text) {
|
||||
return new _WidgetWithTextFinder(widgetType, text);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder widgetWithText(Type widgetType, String text, { bool skipOffstage: true }) {
|
||||
return new _WidgetWithTextFinder(widgetType, text, skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
/// Finds widgets by searching for one with a particular [Key].
|
||||
@ -50,7 +56,10 @@ class CommonFinders {
|
||||
/// Example:
|
||||
///
|
||||
/// expect(tester, hasWidget(find.byKey(backKey)));
|
||||
Finder byKey(Key key) => new _KeyFinder(key);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byKey(Key key, { bool skipOffstage: true }) => new _KeyFinder(key, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets by searching for widgets with a particular type.
|
||||
///
|
||||
@ -63,7 +72,10 @@ class CommonFinders {
|
||||
/// Example:
|
||||
///
|
||||
/// expect(tester, hasWidget(find.byType(IconButton)));
|
||||
Finder byType(Type type) => new _WidgetTypeFinder(type);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byType(Type type, { bool skipOffstage: true }) => new _WidgetTypeFinder(type, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets by searching for elements with a particular type.
|
||||
///
|
||||
@ -76,7 +88,10 @@ class CommonFinders {
|
||||
/// Example:
|
||||
///
|
||||
/// expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement)));
|
||||
Finder byElementType(Type type) => new _ElementTypeFinder(type);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byElementType(Type type, { bool skipOffstage: true }) => new _ElementTypeFinder(type, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets whose current widget is the instance given by the
|
||||
/// argument.
|
||||
@ -90,7 +105,10 @@ class CommonFinders {
|
||||
///
|
||||
/// // You can find and tap on it like this:
|
||||
/// tester.tap(find.byConfig(myButton));
|
||||
Finder byConfig(Widget config) => new _ConfigFinder(config);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byConfig(Widget config, { bool skipOffstage: true }) => new _ConfigFinder(config, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets using a widget predicate.
|
||||
///
|
||||
@ -99,8 +117,11 @@ class CommonFinders {
|
||||
/// expect(tester, hasWidget(find.byWidgetPredicate(
|
||||
/// (Widget widget) => widget is Tooltip && widget.message == 'Back'
|
||||
/// )));
|
||||
Finder byWidgetPredicate(WidgetPredicate predicate) {
|
||||
return new _WidgetPredicateFinder(predicate);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byWidgetPredicate(WidgetPredicate predicate, { bool skipOffstage: true }) {
|
||||
return new _WidgetPredicateFinder(predicate, skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
/// Finds widgets using an element predicate.
|
||||
@ -113,14 +134,21 @@ class CommonFinders {
|
||||
/// // (contrast with byElementType, which only returns exact matches)
|
||||
/// (Element element) => element is SingleChildRenderObjectElement
|
||||
/// )));
|
||||
Finder byElementPredicate(ElementPredicate predicate) {
|
||||
return new _ElementPredicateFinder(predicate);
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byElementPredicate(ElementPredicate predicate, { bool skipOffstage: true }) {
|
||||
return new _ElementPredicateFinder(predicate, skipOffstage: skipOffstage);
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches a widget tree and returns nodes that match a particular
|
||||
/// pattern.
|
||||
abstract class Finder {
|
||||
/// Initialises a Finder. Used by subclasses to initialize the [skipOffstage]
|
||||
/// property.
|
||||
Finder({ this.skipOffstage: true });
|
||||
|
||||
/// Describes what the finder is looking for. The description should be
|
||||
/// a brief English noun phrase describing the finder's pattern.
|
||||
String get description;
|
||||
@ -134,11 +162,19 @@ abstract class Finder {
|
||||
/// function, consider extending [MatchFinder] instead.
|
||||
Iterable<Element> apply(Iterable<Element> candidates);
|
||||
|
||||
// Right now this is hard-coded to just grab the elements from the binding.
|
||||
//
|
||||
// One could imagine a world where CommonFinders and Finder can be configured
|
||||
// to work from a specific subtree, but we'll implement that when it's needed.
|
||||
static Iterable<Element> get _allElements => collectAllElementsFrom(WidgetsBinding.instance.renderViewElement);
|
||||
/// Whether this finder skips nodes that are offstage.
|
||||
///
|
||||
/// If this is true, then the elements are walked using
|
||||
/// [Element.visitChildrenForSemantics]. This skips offstage children of
|
||||
/// [Offstage] widgets, as well as children of inactive [Route]s.
|
||||
final bool skipOffstage;
|
||||
|
||||
Iterable<Element> get _allElements {
|
||||
return collectAllElementsFrom(
|
||||
WidgetsBinding.instance.renderViewElement,
|
||||
skipOffstage: skipOffstage
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Element> _cachedResult;
|
||||
|
||||
@ -171,21 +207,26 @@ abstract class Finder {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
|
||||
final List<Element> widgets = evaluate().toList();
|
||||
final int count = widgets.length;
|
||||
if (count == 0)
|
||||
return 'zero widgets with $description';
|
||||
return 'zero widgets with $description$additional';
|
||||
if (count == 1)
|
||||
return 'exactly one widget with $description: ${widgets.single}';
|
||||
return 'exactly one widget with $description$additional: ${widgets.single}';
|
||||
if (count < 4)
|
||||
return '$count widgets with $description: $widgets';
|
||||
return '$count widgets with $description: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
|
||||
return '$count widgets with $description$additional: $widgets';
|
||||
return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches a widget tree and returns nodes that match a particular
|
||||
/// pattern.
|
||||
abstract class MatchFinder extends Finder {
|
||||
/// Initialises a predicate-based Finder. Used by subclasses to initialize the
|
||||
/// [skipOffstage] property.
|
||||
MatchFinder({ bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
/// Returns true if the given element matches the pattern.
|
||||
///
|
||||
/// When implementing your own MatchFinder, this is the main method to override.
|
||||
@ -198,7 +239,7 @@ abstract class MatchFinder extends Finder {
|
||||
}
|
||||
|
||||
class _TextFinder extends MatchFinder {
|
||||
_TextFinder(this.text);
|
||||
_TextFinder(this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final String text;
|
||||
|
||||
@ -215,7 +256,7 @@ class _TextFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _WidgetWithTextFinder extends Finder {
|
||||
_WidgetWithTextFinder(this.widgetType, this.text);
|
||||
_WidgetWithTextFinder(this.widgetType, this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Type widgetType;
|
||||
final String text;
|
||||
@ -249,7 +290,7 @@ class _WidgetWithTextFinder extends Finder {
|
||||
}
|
||||
|
||||
class _KeyFinder extends MatchFinder {
|
||||
_KeyFinder(this.key);
|
||||
_KeyFinder(this.key, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Key key;
|
||||
|
||||
@ -263,7 +304,7 @@ class _KeyFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _WidgetTypeFinder extends MatchFinder {
|
||||
_WidgetTypeFinder(this.widgetType);
|
||||
_WidgetTypeFinder(this.widgetType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Type widgetType;
|
||||
|
||||
@ -277,7 +318,7 @@ class _WidgetTypeFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _ElementTypeFinder extends MatchFinder {
|
||||
_ElementTypeFinder(this.elementType);
|
||||
_ElementTypeFinder(this.elementType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Type elementType;
|
||||
|
||||
@ -291,7 +332,7 @@ class _ElementTypeFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _ConfigFinder extends MatchFinder {
|
||||
_ConfigFinder(this.config);
|
||||
_ConfigFinder(this.config, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Widget config;
|
||||
|
||||
@ -305,7 +346,7 @@ class _ConfigFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _WidgetPredicateFinder extends MatchFinder {
|
||||
_WidgetPredicateFinder(this.predicate);
|
||||
_WidgetPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final WidgetPredicate predicate;
|
||||
|
||||
@ -319,7 +360,7 @@ class _WidgetPredicateFinder extends MatchFinder {
|
||||
}
|
||||
|
||||
class _ElementPredicateFinder extends MatchFinder {
|
||||
_ElementPredicateFinder(this.predicate);
|
||||
_ElementPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final ElementPredicate predicate;
|
||||
|
||||
|
@ -36,12 +36,19 @@ const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1);
|
||||
Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n);
|
||||
|
||||
/// Asserts that the [Finder] locates the a single widget that has at
|
||||
/// least one [OffStage] widget ancestor.
|
||||
const Matcher isOffStage = const _IsOffStage();
|
||||
/// least one [Offstage] widget ancestor.
|
||||
///
|
||||
/// It's important to use a full finder, since by default finders exclude
|
||||
/// offstage widgets.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// expect(find.text('Save', skipOffstage: false), isOffstage);
|
||||
const Matcher isOffstage = const _IsOffstage();
|
||||
|
||||
/// Asserts that the [Finder] locates the a single widget that has no
|
||||
/// [OffStage] widget ancestors.
|
||||
const Matcher isOnStage = const _IsOnStage();
|
||||
/// [Offstage] widget ancestors.
|
||||
const Matcher isOnstage = const _IsOnstage();
|
||||
|
||||
/// Asserts that the [Finder] locates the a single widget that has at
|
||||
/// least one [Card] widget ancestor.
|
||||
@ -54,7 +61,8 @@ const Matcher isNotInCard = const _IsNotInCard();
|
||||
/// Asserts that an object's toString() is a plausible one-line description.
|
||||
///
|
||||
/// Specifically, this matcher checks that the string does not contains newline
|
||||
/// characters and does not have leading or trailing whitespace.
|
||||
/// characters, and does not have leading or trailing whitespace, and is not
|
||||
/// empty.
|
||||
const Matcher hasOneLineDescription = const _HasOneLineDescription();
|
||||
|
||||
class _FindsWidgetMatcher extends Matcher {
|
||||
@ -66,18 +74,17 @@ class _FindsWidgetMatcher extends Matcher {
|
||||
@override
|
||||
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
|
||||
assert(min != null || max != null);
|
||||
assert(min == null || max == null || min <= max);
|
||||
matchState[Finder] = finder;
|
||||
int count = 0;
|
||||
Iterator<Element> iterator = finder.evaluate().iterator;
|
||||
if (min != null) {
|
||||
int count = 0;
|
||||
Iterator<Element> iterator = finder.evaluate().iterator;
|
||||
while (count < min && iterator.moveNext())
|
||||
count += 1;
|
||||
if (count < min)
|
||||
return false;
|
||||
}
|
||||
if (max != null) {
|
||||
int count = 0;
|
||||
Iterator<Element> iterator = finder.evaluate().iterator;
|
||||
while (count <= max && iterator.moveNext())
|
||||
count += 1;
|
||||
if (count > max)
|
||||
@ -137,9 +144,11 @@ class _FindsWidgetMatcher extends Matcher {
|
||||
}
|
||||
|
||||
bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
|
||||
expect(finder, findsOneWidget);
|
||||
Iterable<Element> nodes = finder.evaluate();
|
||||
if (nodes.length != 1)
|
||||
return false;
|
||||
bool result = false;
|
||||
finder.evaluate().single.visitAncestorElements((Element ancestor) {
|
||||
nodes.single.visitAncestorElements((Element ancestor) {
|
||||
if (predicate(ancestor.widget)) {
|
||||
result = true;
|
||||
return false;
|
||||
@ -153,16 +162,15 @@ bool _hasAncestorOfType(Finder finder, Type targetType) {
|
||||
return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
|
||||
}
|
||||
|
||||
class _IsOffStage extends Matcher {
|
||||
const _IsOffStage();
|
||||
class _IsOffstage extends Matcher {
|
||||
const _IsOffstage();
|
||||
|
||||
@override
|
||||
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
|
||||
return _hasAncestorMatching(finder, (Widget widget) {
|
||||
if (widget.runtimeType != OffStage)
|
||||
return false;
|
||||
OffStage offstage = widget;
|
||||
return offstage.offstage;
|
||||
if (widget is Offstage)
|
||||
return widget.offstage;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -170,18 +178,19 @@ class _IsOffStage extends Matcher {
|
||||
Description describe(Description description) => description.add('offstage');
|
||||
}
|
||||
|
||||
class _IsOnStage extends Matcher {
|
||||
const _IsOnStage();
|
||||
class _IsOnstage extends Matcher {
|
||||
const _IsOnstage();
|
||||
|
||||
@override
|
||||
bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
|
||||
expect(finder, findsOneWidget);
|
||||
Iterable<Element> nodes = finder.evaluate();
|
||||
if (nodes.length != 1)
|
||||
return false;
|
||||
bool result = true;
|
||||
finder.evaluate().single.visitAncestorElements((Element ancestor) {
|
||||
nodes.single.visitAncestorElements((Element ancestor) {
|
||||
Widget widget = ancestor.widget;
|
||||
if (widget.runtimeType == OffStage) {
|
||||
OffStage offstage = widget;
|
||||
result = !offstage.offstage;
|
||||
if (widget is Offstage) {
|
||||
result = !widget.offstage;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -219,9 +228,9 @@ class _HasOneLineDescription extends Matcher {
|
||||
@override
|
||||
bool matches(Object object, Map<dynamic, dynamic> matchState) {
|
||||
String description = object.toString();
|
||||
return description.isNotEmpty &&
|
||||
!description.contains('\n') &&
|
||||
description.trim() == description;
|
||||
return description.isNotEmpty
|
||||
&& !description.contains('\n')
|
||||
&& description.trim() == description;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -204,7 +204,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
|
||||
)?.target;
|
||||
if (innerTarget == null)
|
||||
return null;
|
||||
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement)
|
||||
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement, skipOffstage: true)
|
||||
.lastWhere((Element element) => element.renderObject == innerTarget);
|
||||
final List<Element> candidates = <Element>[];
|
||||
innerTargetElement.visitAncestorElements((Element element) {
|
||||
|
@ -15,7 +15,7 @@ void main() {
|
||||
testWidgets('fails with a descriptive message', (WidgetTester tester) async {
|
||||
TestFailure failure;
|
||||
try {
|
||||
expect(find.text('foo'), findsOneWidget);
|
||||
expect(find.text('foo', skipOffstage: false), findsOneWidget);
|
||||
} catch(e) {
|
||||
failure = e;
|
||||
}
|
||||
@ -38,7 +38,7 @@ void main() {
|
||||
|
||||
TestFailure failure;
|
||||
try {
|
||||
expect(find.text('foo'), findsNothing);
|
||||
expect(find.text('foo', skipOffstage: false), findsNothing);
|
||||
} catch(e) {
|
||||
failure = e;
|
||||
}
|
||||
@ -50,6 +50,24 @@ void main() {
|
||||
expect(message, contains('Actual: ?:<exactly one widget with text "foo": Text("foo")>\n'));
|
||||
expect(message, contains('Which: means one was found but none were expected\n'));
|
||||
});
|
||||
|
||||
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(new Text('foo'));
|
||||
|
||||
TestFailure failure;
|
||||
try {
|
||||
expect(find.text('foo'), findsNothing);
|
||||
} catch(e) {
|
||||
failure = e;
|
||||
}
|
||||
|
||||
expect(failure, isNotNull);
|
||||
String message = failure.message;
|
||||
|
||||
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
|
||||
expect(message, contains('Actual: ?:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo")>\n'));
|
||||
expect(message, contains('Which: means one was found but none were expected\n'));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user