Unverified Commit 3b9b5ace authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

showOnScreen Improvements (#18252)

parent c53245c6
......@@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
......@@ -614,27 +615,50 @@ class RenderListWheelViewport
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
final ListWheelParentData parentData = target.parentData;
final double centerPosition = parentData.offset.dy;
if (alignment < 0.5) {
return centerPosition + _topScrollMarginExtent * alignment * 2.0;
} else if (alignment > 0.5) {
return centerPosition - _topScrollMarginExtent * (alignment - 0.5) * 2.0;
} else {
return centerPosition;
}
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
// `target` is only fully revealed when in the selected/center position. Therefore,
// this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values.
rect ??= target.paintBounds;
// `child` will be the last RenderObject before the viewport when walking up from `target`.
RenderObject child = target;
while (child.parent != this)
child = child.parent;
final ListWheelParentData parentData = child.parentData;
final double targetOffset = parentData.offset.dy; // the so-called "centerPosition"
final Matrix4 transform = target.getTransformTo(this);
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Rect targetRect = bounds.translate(0.0, (size.height - itemExtent) / 2);
return new RevealedOffset(offset: targetOffset, rect: targetRect);
}
@override
void showOnScreen([RenderObject child]) {
if (child != null) {
// Shows the child in the selected/center position.
offset.jumpTo(getOffsetToReveal(child, 0.5));
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (descendant != null) {
// Shows the descendant in the selected/center position.
final RevealedOffset revealedOffset = getOffsetToReveal(descendant, 0.5, rect: rect);
if (duration == Duration.zero) {
offset.jumpTo(revealedOffset.offset);
} else {
offset.animateTo(revealedOffset.offset, duration: duration, curve: curve);
}
rect = revealedOffset.rect;
}
// Make sure the viewport itself is on screen.
super.showOnScreen();
super.showOnScreen(
rect: rect,
duration: duration,
curve: curve,
);
}
}
......@@ -5,6 +5,7 @@
import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
......@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// An estimate of the bounds within which this render object will paint.
/// Useful for debugging flags such as [debugPaintLayerBordersEnabled].
///
/// These are also the bounds used by [showOnScreen] to make a [RenderObject]
/// visible on screen.
Rect get paintBounds;
/// Override this method to paint debugging information.
......@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
/// Attempt to make this or a descendant RenderObject visible on screen.
///
/// If [child] is provided, that [RenderObject] is made visible. If [child] is
/// omitted, this [RenderObject] is made visible.
void showOnScreen([RenderObject child]) {
/// Attempt to make (a portion of) this or a descendant [RenderObject] visible
/// on screen.
///
/// If `descendant` is provided, that [RenderObject] is made visible. If
/// `descendant` is omitted, this [RenderObject] is made visible.
///
/// The optional `rect` parameter describes which area of that [RenderObject]
/// should be shown on screen. If `rect` is null, the entire
/// [RenderObject] (as defined by its [paintBounds]) will be revealed. The
/// `rect` parameter is interpreted relative to the coordinate system of
/// `descendant` if that argument is provided and relative to this
/// [RenderObject] otherwise.
///
/// The `duration` parameter can be set to a non-zero value to bring the
/// target object on screen in an animation defined by `curve`.
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (parent is RenderObject) {
final RenderObject renderParent = parent;
renderParent.showOnScreen(child ?? this);
renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
}
}
}
......
......@@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
......@@ -40,7 +41,14 @@ abstract class RenderAbstractViewport extends RenderObject {
return null;
}
/// Returns the offset that would be needed to reveal the target render object.
/// Returns the offset that would be needed to reveal the `target`
/// [RenderObject].
///
/// The optional `rect` parameter describes which area of that `target` object
/// should be revealed in the viewport. If `rect` is null, the entire
/// `target` [RenderObject] (as defined by its [RenderObject.paintBounds])
/// will be revealed. If `rect` is provided it has to be given in the
/// coordinate system of the `target` object.
///
/// The `alignment` argument describes where the target should be positioned
/// after applying the returned offset. If `alignment` is 0.0, the child must
......@@ -52,7 +60,15 @@ abstract class RenderAbstractViewport extends RenderObject {
/// The target might not be a direct child of this viewport but it must be a
/// descendant of the viewport and there must not be any other
/// [RenderAbstractViewport] objects between the target and this object.
double getOffsetToReveal(RenderObject target, double alignment);
///
/// This method assumes that the content of the viewport moves linearly, i.e.
/// when the offset of the viewport is changed by x then `target` also moves
/// by x within the viewport.
///
/// See also:
///
/// * [RevealedOffset], which describes the return value of this method.
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect});
/// The default value for the cache extent of the viewport.
///
......@@ -63,6 +79,59 @@ abstract class RenderAbstractViewport extends RenderObject {
static const double defaultCacheExtent = 250.0;
}
/// Return value for [RenderAbstractViewport.getOffsetToReveal].
///
/// It indicates the [offset] required to reveal an element in a viewport and
/// the [rect] position said element would have in the viewport at that
/// [offset].
class RevealedOffset {
/// Instantiates a return value for [RenderAbstractViewport.getOffsetToReveal].
const RevealedOffset({
@required this.offset,
@required this.rect,
}) : assert(offset != null), assert(rect != null);
/// Offset for the viewport to reveal a specific element in the viewport.
///
/// See also:
///
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
/// value for a specific element.
final double offset;
/// The [Rect] in the outer coordinate system of the viewport at which the
/// to-be-revealed element would be located if the viewport's offset is set
/// to [offset].
///
/// A viewport usually has two coordinate systems and works as an adapter
/// between the two:
///
/// The inner coordinate system has its origin at the top left corner of the
/// content that moves inside the viewport. The origin of this coordinate
/// system usually moves around relative to the leading edge of the viewport
/// when the viewport offset changes.
///
/// The outer coordinate system has its origin at the top left corner of the
/// visible part of the viewport. This origin stays at the same position
/// regardless of the current viewport offset.
///
/// In other words: [rect] describes where the revealed element would be
/// located relative to the top left corner of the visible part of the
/// viewport if the viewport's offset is set to [offset].
///
/// See also:
///
/// * [RenderAbstractViewport.getOffsetToReveal], which calculates this
/// value for a specific element.
final Rect rect;
@override
String toString() {
return '$runtimeType(offset: $offset, rect: $rect)';
}
}
/// A base class for render objects that are bigger on the inside.
///
/// This render object provides the shared code for render objects that host
......@@ -512,14 +581,16 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
double leadingScrollOffset;
double targetMainAxisExtent;
RenderObject descendant;
rect ??= target.paintBounds;
if (target is RenderBox) {
final RenderBox targetBox = target;
// The pivot will be the topmost child before we hit a RenderSliver.
RenderBox pivot = targetBox;
while (pivot.parent is RenderBox)
pivot = pivot.parent;
......@@ -527,14 +598,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(pivot.parent != null);
assert(pivot.parent != this);
assert(pivot != this);
assert(pivot.parent is RenderSliver); // TODO(abarth): Support other kinds of render objects besides slivers.
final RenderSliver pivotParent = pivot.parent;
final Matrix4 transform = targetBox.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);
target = pivot;
// TODO(abarth): Support other kinds of render objects besides slivers.
assert(target.parent is RenderSliver);
final RenderSliver pivotParent = target.parent;
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
......@@ -580,7 +648,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
targetMainAxisExtent = targetSliver.geometry.scrollExtent;
descendant = targetSliver;
} else {
return offset.pixels;
return new RevealedOffset(offset: offset.pixels, rect: rect);
}
// The child will be the topmost object before we get to the viewport.
......@@ -615,7 +683,29 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
break;
}
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final double offsetDifference = offset.pixels - targetOffset;
final Matrix4 transform = target.getTransformTo(this);
applyPaintTransform(child, transform);
Rect targetRect = MatrixUtils.transformRect(transform, rect);
switch (axisDirection) {
case AxisDirection.down:
targetRect = targetRect.translate(0.0, offsetDifference);
break;
case AxisDirection.right:
targetRect = targetRect.translate(offsetDifference, 0.0);
break;
case AxisDirection.up:
targetRect = targetRect.translate(0.0, -offsetDifference);
break;
case AxisDirection.left:
targetRect = targetRect.translate(-offsetDifference, 0.0);
break;
}
return new RevealedOffset(offset: targetOffset, rect: targetRect);
}
/// The offset at which the given `child` should be painted.
......@@ -646,8 +736,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
return null;
}
// TODO(ianh): semantics - shouldn't walk the invisible children
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
......@@ -783,42 +871,76 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
Iterable<RenderSliver> get childrenInHitTestOrder;
@override
void showOnScreen([RenderObject child]) {
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset);
// Make sure the viewport itself is on screen.
super.showOnScreen();
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect newRect = RenderViewportBase.showInViewport(
descendant: descendant,
viewport: this,
offset: offset,
rect: rect,
duration: duration,
curve: curve,
);
super.showOnScreen(
rect: newRect,
duration: duration,
curve: curve,
);
}
/// Make the given `child` of the given `viewport` fully visible in the
/// `viewport` by manipulating the provided [ViewportOffset] `offset`.
/// Make (a portion of) the given `descendant` of the given `viewport` fully
/// visible in the `viewport` by manipulating the provided [ViewportOffset]
/// `offset`.
///
/// The optional `rect` parameter describes which area of the `descendant`
/// should be shown in the viewport. If `rect` is null, the entire
/// `descendant` will be revealed. The `rect` parameter is interpreted
/// relative to the coordinate system of `descendant`.
///
/// The returned [Rect] describes the new location of `descendant` or `rect`
/// in the viewport after it has been revealed. See [RevealedOffset.rect]
/// for a full definition of this [Rect].
///
/// The parameters `viewport` and `offset` are required and cannot be null.
/// If `child` is null this is a no-op.
static void showInViewport({
RenderObject child,
/// If `descendant` is null, this is a no-op and `rect` is returned.
///
/// If both `decedent` and `rect` are null, null is returned because there is
/// nothing to be shown in the viewport.
///
/// The `duration` parameter can be set to a non-zero value to animate the
/// target object into the viewport with an animation defined by `curve`.
static Rect showInViewport({
RenderObject descendant,
Rect rect,
@required RenderAbstractViewport viewport,
@required ViewportOffset offset,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
assert(viewport != null);
assert(offset != null);
if (child == null) {
return;
if (descendant == null) {
return rect;
}
final double leadingEdgeOffset = viewport.getOffsetToReveal(child, 0.0);
final double trailingEdgeOffset = viewport.getOffsetToReveal(child, 1.0);
final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
final double currentOffset = offset.pixels;
// scrollOffset
// 0 +---------+
// | |
// _ | |
// viewport position | | |
// with `child` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position
// | | | with `child` at
// | | _| leading edge
// | |
// 800 +---------+
// scrollOffset
// 0 +---------+
// | |
// _ | |
// viewport position | | |
// with `descendant` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position
// | | | with `descendant` at
// | | _| leading edge
// | |
// 800 +---------+
//
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the left in image above.
......@@ -829,19 +951,36 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
// to `trailingEdgeOffset`, the one on the right by setting it to
// `leadingEdgeOffset`.
assert(leadingEdgeOffset >= trailingEdgeOffset);
RevealedOffset targetOffset;
if (leadingEdgeOffset.offset < trailingEdgeOffset.offset) {
// `descendant` is too big to be visible on screen in its entirety. Let's
// align it with the edge that requires the least amount of scrolling.
final double leadingEdgeDiff = (offset.pixels - leadingEdgeOffset.offset).abs();
final double trailingEdgeDiff = (offset.pixels - trailingEdgeOffset.offset).abs();
targetOffset = leadingEdgeDiff < trailingEdgeDiff ? leadingEdgeOffset : trailingEdgeOffset;
} else if (currentOffset > leadingEdgeOffset.offset) {
// `descendant` currently starts above the leading edge and can be shown
// fully on screen by scrolling down (which means: moving viewport up).
targetOffset = leadingEdgeOffset;
} else if (currentOffset < trailingEdgeOffset.offset) {
// `descendant currently ends below the trailing edge and can be shown
// fully on screen by scrolling up (which means: moving viewport down)
targetOffset = trailingEdgeOffset;
} else {
// `descendant` is between leading and trailing edge and hence already
// fully shown on screen. No action necessary.
final Matrix4 transform = descendant.getTransformTo(viewport.parent);
return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
}
assert(targetOffset != null);
if (currentOffset > leadingEdgeOffset) {
// `child` currently starts above the leading edge and can be shown fully
// on screen by scrolling down (which means: moving viewport up).
offset.jumpTo(leadingEdgeOffset);
} else if (currentOffset < trailingEdgeOffset ) {
// `child currently ends below the trailing edge and can be shown fully
// on screen by scrolling up (which means: moving viewport down)
offset.jumpTo(trailingEdgeOffset);
if (duration == Duration.zero) {
offset.jumpTo(targetOffset.offset);
} else {
offset.animateTo(targetOffset.offset, duration: duration, curve: curve);
}
// else: `child` is between leading and trailing edge and hence already
// fully shown on screen. No action necessary.
return targetOffset.rect;
}
}
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
/// The direction of a scroll, relative to the positive scroll offset axis given
......@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier {
/// [jumpTo] applies the change immediately and notifies its listeners.
void correctBy(double correction);
/// Jumps the scroll position from its current value to the given value,
/// Jumps [pixels] from its current value to the given value,
/// without animation, and without checking if the new value is in range.
///
/// See also:
......@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier {
/// and that defers the notification of its listeners until after layout.
void jumpTo(double pixels);
/// Animates [pixels] from its current value to the given value.
///
/// The returned [Future] will complete when the animation ends, whether it
/// completed successfully or whether it was interrupted prematurely.
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
});
/// The direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewport.axisDirection].
///
......@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset {
// Do nothing, viewport is fixed.
}
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) async => null;
@override
ScrollDirection get userScrollDirection => ScrollDirection.idle;
}
......@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null);
final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
final double target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent);
if (target == pixels)
return new Future<Null>.value();
......@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// animation, use [jumpTo].
///
/// The animation is typically handled by an [DrivenScrollActivity].
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
......
......@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
}
Offset get _paintOffset {
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return new Offset(0.0, _offset.pixels - child.size.height + size.height);
return new Offset(0.0, position - child.size.height + size.height);
case AxisDirection.down:
return new Offset(0.0, -_offset.pixels);
return new Offset(0.0, -position);
case AxisDirection.left:
return new Offset(_offset.pixels - child.size.width + size.width, 0.0);
return new Offset(position - child.size.width + size.width, 0.0);
case AxisDirection.right:
return new Offset(-_offset.pixels, 0.0);
return new Offset(-position, 0.0);
}
return null;
}
......@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
rect ??= target.paintBounds;
if (target is! RenderBox)
return offset.pixels;
return new RevealedOffset(offset: offset.pixels, rect: rect);
final RenderBox targetBox = target;
final Matrix4 transform = targetBox.getTransformTo(this);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Size contentSize = child.size;
double leadingScrollOffset;
......@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
break;
}
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
return new RevealedOffset(offset: targetOffset, rect: targetRect);
}
@override
void showOnScreen([RenderObject child]) {
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset);
// Make sure the viewport itself is on screen.
super.showOnScreen();
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect newRect = RenderViewportBase.showInViewport(
descendant: descendant,
viewport: this,
offset: offset,
rect: rect,
duration: duration,
curve: curve,
);
super.showOnScreen(
rect: newRect,
duration: duration,
curve: curve,
);
}
@override
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void main() {
testWidgets('Viewport getOffsetToReveal - down', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new ListView(
controller: new ScrollController(initialScrollOffset: 300.0),
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - right', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 300.0,
width: 200.0,
child: new ListView(
scrollDirection: Axis.horizontal,
controller: new ScrollController(initialScrollOffset: 300.0),
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 300.0,
width: 100.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, new Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - up', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new ListView(
controller: new ScrollController(initialScrollOffset: 300.0),
reverse: true,
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
});
testWidgets('Viewport getOffsetToReveal - left', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 300.0,
width: 200.0,
child: new ListView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: new ScrollController(initialScrollOffset: 300.0),
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 300.0,
width: 100.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, new Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
});
testWidgets('Nested Viewports showOnScreen', (WidgetTester tester) async {
final List<List<Widget>> children = new List<List<Widget>>(10);
final List<ScrollController> controllersX = new List<ScrollController>.generate(10, (int i) => new ScrollController(initialScrollOffset: 400.0));
final ScrollController controllerY = new ScrollController(initialScrollOffset: 400.0);
/// Builds a gird:
///
/// <- x ->
/// 0 1 2 3 4 5 6 7 8 9
/// 0 c c c c c c c c c c
/// 1 c c c c c c c c c c
/// 2 c c c c c c c c c c
/// 3 c c c c c c c c c c y
/// 4 c c c c v v c c c c
/// 5 c c c c v v c c c c
/// 6 c c c c c c c c c c
/// 7 c c c c c c c c c c
/// 8 c c c c c c c c c c
/// 9 c c c c c c c c c c
///
/// Each c is a 100x100 container, v are containers visible in initial
/// viewport.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 200.0,
child: new ListView(
controller: controllerY,
children: new List<Widget>.generate(10, (int y) {
return Container(
height: 100.0,
child: new ListView(
scrollDirection: Axis.horizontal,
controller: controllersX[y],
children: children[y] = new List<Widget>.generate(10, (int x) {
return new Container(
height: 100.0,
width: 100.0,
child: new Text('$x,$y'),
);
}),
),
);
}),
),
),
),
),
);
// Already in viewport
tester.renderObject(find.byWidget(children[4][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 400.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above viewport
tester.renderObject(find.byWidget(children[3][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 400.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below viewport
tester.renderObject(find.byWidget(children[6][4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 400.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Left of viewport
tester.renderObject(find.byWidget(children[4][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 300.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Right of viewport
tester.renderObject(find.byWidget(children[4][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[4].offset, 500.0);
expect(controllerY.offset, 400.0);
controllersX[4].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and left of viewport
tester.renderObject(find.byWidget(children[3][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 300.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and left of viewport
tester.renderObject(find.byWidget(children[6][3], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 300.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and right of viewport
tester.renderObject(find.byWidget(children[3][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[3].offset, 500.0);
expect(controllerY.offset, 300.0);
controllersX[3].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport
tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controllersX[6].offset, 500.0);
expect(controllerY.offset, 500.0);
controllersX[6].jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport with animations
tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controllersX[6].offset, greaterThan(400.0));
expect(controllersX[6].offset, lessThan(500.0));
expect(controllerY.offset, greaterThan(400.0));
expect(controllerY.offset, lessThan(500.0));
await tester.pumpAndSettle();
expect(controllersX[6].offset, 500.0);
expect(controllerY.offset, 500.0);
});
group('Nested viewports (same orientation) showOnScreen', () {
List<Widget> children;
Future<Null> buildNestedScroller({WidgetTester tester, ScrollController inner, ScrollController outer}) {
return tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new ListView(
controller: outer,
children: <Widget>[
new Container(
height: 200.0,
),
new Container(
height: 200.0,
width: 300.0,
child: new ListView(
controller: inner,
children: children = new List<Widget>.generate(10, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('$i'),
);
}),
),
),
new Container(
height: 200.0,
)
],
),
),
),
),
);
}
testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 0.0);
expect(outer.offset, 100.0);
});
testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[4], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 300.0);
expect(outer.offset, 200.0);
});
testWidgets('in view in inner and outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = new ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[2])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
});
testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async {
final ScrollController inner = new ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = new ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[5], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 400.0);
});
testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController(initialScrollOffset: 100.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 100.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[1])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 0.0);
});
});
testWidgets('Viewport showOnScreen with objects larger than viewport', (WidgetTester tester) async {
List<Widget> children;
ScrollController controller;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
child: new ListView(
controller: controller = new ScrollController(initialScrollOffset: 300.0),
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 300.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
);
expect(controller.offset, 300.0);
// Already aligned with leading edge, nothing happens.
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
// Above leading edge aligns trailing edges
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 100.0);
// Below trailing edge aligns leading edges
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
controller.jumpTo(250.0);
await tester.pumpAndSettle();
expect(controller.offset, 250.0);
// Partly visible across leading edge aligns trailing edges
tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 100.0);
controller.jumpTo(150.0);
await tester.pumpAndSettle();
expect(controller.offset, 150.0);
// Partly visible across trailing edge aligns leading edges
tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 300.0);
});
}
......@@ -743,4 +743,127 @@ void main() {
debugDefaultTargetPlatformOverride = null;
});
});
testWidgets('ListWheelScrollView getOffsetToReveal', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = new List<Widget>(10);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 500.0,
width: 300.0,
child: new ListWheelScrollView(
controller: new ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children: outerChildren = new List<Widget>.generate(10, (int i) {
return new Container(
child: new Center(
child: innerChildren[i] = new Container(
height: 50.0,
width: 50.0,
child: new Text('Item $i'),
),
),
);
}),
),
),
),
),
);
final RenderListWheelViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderListWheelViewport);
// direct child of viewport
RenderObject target = tester.renderObject(find.byWidget(outerChildren[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
// descendant of viewport, not direct child
target = tester.renderObject(find.byWidget(innerChildren[5]));
revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
});
testWidgets('ListWheelScrollView showOnScreen', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = new List<Widget>(10);
ScrollController controller;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 500.0,
width: 300.0,
child: new ListWheelScrollView(
controller: controller = new ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children:
outerChildren = new List<Widget>.generate(10, (int i) {
return new Container(
child: new Center(
child: innerChildren[i] = new Container(
height: 50.0,
width: 50.0,
child: new Text('Item $i'),
),
),
);
}),
),
),
),
),
);
expect(controller.offset, 300.0);
tester.renderObject(find.byWidget(outerChildren[5])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 500.0);
tester.renderObject(find.byWidget(innerChildren[9])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 900.0);
tester.renderObject(find.byWidget(outerChildren[7])).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controller.offset, lessThan(900.0));
expect(controller.offset, greaterThan(700.0));
await tester.pumpAndSettle();
expect(controller.offset, 700.0);
});
}
......@@ -5,6 +5,7 @@
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'semantics_tester.dart';
......@@ -341,4 +342,487 @@ void main() {
semantics.dispose();
});
testWidgets('SingleChildScrollView getOffsetToReveal - down', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new SingleChildScrollView(
controller: new ScrollController(initialScrollOffset: 300.0),
child: new Column(
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
});
testWidgets('SingleChildScrollView getOffsetToReveal - up', (WidgetTester tester) async {
final List<Widget> children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('Tile $i'),
);
});
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new SingleChildScrollView(
controller: new ScrollController(initialScrollOffset: 300.0),
reverse: true,
child: new Column(
children: children.reversed.toList(),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 100.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 190.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 0.0, 10.0, 10.0));
});
testWidgets('SingleChildScrollView getOffsetToReveal - right', (WidgetTester tester) async {
List<Widget> children;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 300.0,
width: 200.0,
child: new SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: new ScrollController(initialScrollOffset: 300.0),
child: new Row(
children: children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 300.0,
width: 100.0,
child: new Text('Tile $i'),
);
}),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 540.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 350.0);
expect(revealed.rect, new Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
});
testWidgets('SingleChildScrollView getOffsetToReveal - left', (WidgetTester tester) async {
final List<Widget> children = new List<Widget>.generate(20, (int i) {
return new Container(
height: 300.0,
width: 100.0,
child: new Text('Tile $i'),
);
});
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 300.0,
width: 200.0,
child: new SingleChildScrollView(
scrollDirection: Axis.horizontal,
reverse: true,
controller: new ScrollController(initialScrollOffset: 300.0),
child: new Row(
children: children.reversed.toList(),
),
),
),
),
),
);
final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport);
final RenderObject target = tester.renderObject(find.byWidget(children[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(100.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 400.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 0.0, 100.0, 300.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 550.0);
expect(revealed.rect, new Rect.fromLTWH(190.0, 40.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 360.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 40.0, 10.0, 10.0));
});
testWidgets('Nested SingleChildScrollView showOnScreen', (WidgetTester tester) async {
final List<List<Widget>> children = new List<List<Widget>>(10);
ScrollController controllerX;
ScrollController controllerY;
/// Builds a gird:
///
/// <- x ->
/// 0 1 2 3 4 5 6 7 8 9
/// 0 c c c c c c c c c c
/// 1 c c c c c c c c c c
/// 2 c c c c c c c c c c
/// 3 c c c c c c c c c c y
/// 4 c c c c v v c c c c
/// 5 c c c c v v c c c c
/// 6 c c c c c c c c c c
/// 7 c c c c c c c c c c
/// 8 c c c c c c c c c c
/// 9 c c c c c c c c c c
///
/// Each c is a 100x100 container, v are containers visible in initial
/// viewport.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 200.0,
child: new SingleChildScrollView(
controller: controllerY = new ScrollController(initialScrollOffset: 400.0),
child: new SingleChildScrollView(
controller: controllerX = new ScrollController(initialScrollOffset: 400.0),
scrollDirection: Axis.horizontal,
child: new Column(
children: new List<Widget>.generate(10, (int y) {
return new Row(
children: children[y] = new List<Widget>.generate(10, (int x) {
return new Container(
height: 100.0,
width: 100.0,
);
})
);
}),
),
),
),
),
),
),
);
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 400.0);
// Already in viewport
tester.renderObject(find.byWidget(children[4][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above viewport
tester.renderObject(find.byWidget(children[3][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below viewport
tester.renderObject(find.byWidget(children[6][4])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 400.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Left of viewport
tester.renderObject(find.byWidget(children[4][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Right of viewport
tester.renderObject(find.byWidget(children[4][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 400.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and left of viewport
tester.renderObject(find.byWidget(children[3][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and left of viewport
tester.renderObject(find.byWidget(children[6][3])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 300.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Above and right of viewport
tester.renderObject(find.byWidget(children[3][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 300.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport
tester.renderObject(find.byWidget(children[6][6])).showOnScreen();
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 500.0);
controllerX.jumpTo(400.0);
controllerY.jumpTo(400.0);
await tester.pumpAndSettle();
// Below and right of viewport with animations
tester.renderObject(find.byWidget(children[6][6])).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controllerX.offset, greaterThan(400.0));
expect(controllerX.offset, lessThan(500.0));
expect(controllerY.offset, greaterThan(400.0));
expect(controllerY.offset, lessThan(500.0));
await tester.pumpAndSettle();
expect(controllerX.offset, 500.0);
expect(controllerY.offset, 500.0);
});
group('Nested SingleChildScrollView (same orientation) showOnScreen', () {
List<Widget> children;
Future<Null> buildNestedScroller({WidgetTester tester, ScrollController inner, ScrollController outer}) {
return tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 200.0,
width: 300.0,
child: new SingleChildScrollView(
controller: outer,
child: new Column(
children: <Widget>[
new Container(
height: 200.0,
),
new Container(
height: 200.0,
width: 300.0,
child: new SingleChildScrollView(
controller: inner,
child: new Column(
children: children = new List<Widget>.generate(10, (int i) {
return new Container(
height: 100.0,
width: 300.0,
child: new Text('$i'),
);
}),
),
),
),
new Container(
height: 200.0,
)
],
),
),
),
),
),
);
}
testWidgets('in view in inner, but not in outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[0])).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 0.0);
expect(outer.offset, 100.0);
});
testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController();
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 0.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[5])).showOnScreen();
await tester.pumpAndSettle();
expect(inner.offset, 400.0);
expect(outer.offset, 200.0);
});
testWidgets('in view in inner and outer', (WidgetTester tester) async {
final ScrollController inner = new ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = new ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[2])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
});
testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async {
final ScrollController inner = new ScrollController(initialScrollOffset: 200.0);
final ScrollController outer = new ScrollController(initialScrollOffset: 200.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 200.0);
expect(inner.offset, 200.0);
tester.renderObject(find.byWidget(children[5])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 400.0);
});
testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async {
final ScrollController inner = new ScrollController();
final ScrollController outer = new ScrollController(initialScrollOffset: 100.0);
await buildNestedScroller(
tester: tester,
inner: inner,
outer: outer,
);
expect(outer.offset, 100.0);
expect(inner.offset, 0.0);
tester.renderObject(find.byWidget(children[1])).showOnScreen();
await tester.pumpAndSettle();
expect(outer.offset, 200.0);
expect(inner.offset, 0.0);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment