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

showOnScreen Improvements (#18252)

parent c53245c6
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4; import 'package:vector_math/vector_math_64.dart' show Matrix4;
...@@ -614,27 +615,50 @@ class RenderListWheelViewport ...@@ -614,27 +615,50 @@ class RenderListWheelViewport
} }
@override @override
double getOffsetToReveal(RenderObject target, double alignment) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
final ListWheelParentData parentData = target.parentData; // `target` is only fully revealed when in the selected/center position. Therefore,
final double centerPosition = parentData.offset.dy; // this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values.
if (alignment < 0.5) {
return centerPosition + _topScrollMarginExtent * alignment * 2.0; rect ??= target.paintBounds;
} else if (alignment > 0.5) {
return centerPosition - _topScrollMarginExtent * (alignment - 0.5) * 2.0; // `child` will be the last RenderObject before the viewport when walking up from `target`.
} else { RenderObject child = target;
return centerPosition; 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 @override
void showOnScreen([RenderObject child]) { void showOnScreen({
if (child != null) { RenderObject descendant,
// Shows the child in the selected/center position. Rect rect,
offset.jumpTo(getOffsetToReveal(child, 0.5)); 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 @@ ...@@ -5,6 +5,7 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder; import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
...@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// An estimate of the bounds within which this render object will paint. /// An estimate of the bounds within which this render object will paint.
/// Useful for debugging flags such as [debugPaintLayerBordersEnabled]. /// 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; Rect get paintBounds;
/// Override this method to paint debugging information. /// Override this method to paint debugging information.
...@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@override @override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[]; List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
/// Attempt to make this or a descendant RenderObject visible on screen. /// Attempt to make (a portion of) 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. /// If `descendant` is provided, that [RenderObject] is made visible. If
void showOnScreen([RenderObject child]) { /// `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) { if (parent is RenderObject) {
final RenderObject renderParent = parent; final RenderObject renderParent = parent;
renderParent.showOnScreen(child ?? this); renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
} }
} }
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
...@@ -40,7 +41,14 @@ abstract class RenderAbstractViewport extends RenderObject { ...@@ -40,7 +41,14 @@ abstract class RenderAbstractViewport extends RenderObject {
return null; 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 /// The `alignment` argument describes where the target should be positioned
/// after applying the returned offset. If `alignment` is 0.0, the child must /// after applying the returned offset. If `alignment` is 0.0, the child must
...@@ -52,7 +60,15 @@ abstract class RenderAbstractViewport extends RenderObject { ...@@ -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 /// 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 /// descendant of the viewport and there must not be any other
/// [RenderAbstractViewport] objects between the target and this object. /// [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. /// The default value for the cache extent of the viewport.
/// ///
...@@ -63,6 +79,59 @@ abstract class RenderAbstractViewport extends RenderObject { ...@@ -63,6 +79,59 @@ abstract class RenderAbstractViewport extends RenderObject {
static const double defaultCacheExtent = 250.0; 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. /// A base class for render objects that are bigger on the inside.
/// ///
/// This render object provides the shared code for render objects that host /// This render object provides the shared code for render objects that host
...@@ -512,14 +581,16 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -512,14 +581,16 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
} }
@override @override
double getOffsetToReveal(RenderObject target, double alignment) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
double leadingScrollOffset; double leadingScrollOffset;
double targetMainAxisExtent; double targetMainAxisExtent;
RenderObject descendant; RenderObject descendant;
rect ??= target.paintBounds;
if (target is RenderBox) { if (target is RenderBox) {
final RenderBox targetBox = target; final RenderBox targetBox = target;
// The pivot will be the topmost child before we hit a RenderSliver.
RenderBox pivot = targetBox; RenderBox pivot = targetBox;
while (pivot.parent is RenderBox) while (pivot.parent is RenderBox)
pivot = pivot.parent; pivot = pivot.parent;
...@@ -527,14 +598,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -527,14 +598,11 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(pivot.parent != null); assert(pivot.parent != null);
assert(pivot.parent != this); assert(pivot.parent != this);
assert(pivot != 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 Matrix4 transform = targetBox.getTransformTo(pivot);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds); final Rect bounds = MatrixUtils.transformRect(transform, rect);
target = pivot;
// TODO(abarth): Support other kinds of render objects besides slivers.
assert(target.parent is RenderSliver);
final RenderSliver pivotParent = target.parent;
final GrowthDirection growthDirection = pivotParent.constraints.growthDirection; final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) { switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
...@@ -580,7 +648,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -580,7 +648,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
targetMainAxisExtent = targetSliver.geometry.scrollExtent; targetMainAxisExtent = targetSliver.geometry.scrollExtent;
descendant = targetSliver; descendant = targetSliver;
} else { } 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. // The child will be the topmost object before we get to the viewport.
...@@ -615,7 +683,29 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -615,7 +683,29 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
break; 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. /// The offset at which the given `child` should be painted.
...@@ -646,8 +736,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -646,8 +736,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
return null; return null;
} }
// TODO(ianh): semantics - shouldn't walk the invisible children
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -783,29 +871,63 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -783,29 +871,63 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
Iterable<RenderSliver> get childrenInHitTestOrder; Iterable<RenderSliver> get childrenInHitTestOrder;
@override @override
void showOnScreen([RenderObject child]) { void showOnScreen({
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset); RenderObject descendant,
// Make sure the viewport itself is on screen. Rect rect,
super.showOnScreen(); 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 /// Make (a portion of) the given `descendant` of the given `viewport` fully
/// `viewport` by manipulating the provided [ViewportOffset] `offset`. /// 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. /// The parameters `viewport` and `offset` are required and cannot be null.
/// If `child` is null this is a no-op. /// If `descendant` is null, this is a no-op and `rect` is returned.
static void showInViewport({ ///
RenderObject child, /// 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 RenderAbstractViewport viewport,
@required ViewportOffset offset, @required ViewportOffset offset,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) { }) {
assert(viewport != null); assert(viewport != null);
assert(offset != null); assert(offset != null);
if (child == null) { if (descendant == null) {
return; return rect;
} }
final double leadingEdgeOffset = viewport.getOffsetToReveal(child, 0.0); final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
final double trailingEdgeOffset = viewport.getOffsetToReveal(child, 1.0); final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
final double currentOffset = offset.pixels; final double currentOffset = offset.pixels;
// scrollOffset // scrollOffset
...@@ -813,9 +935,9 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -813,9 +935,9 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
// | | // | |
// _ | | // _ | |
// viewport position | | | // viewport position | | |
// with `child` at | | | _ // with `descendant` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position // trailing edge |_ | xxxxxxx | | viewport position
// | | | with `child` at // | | | with `descendant` at
// | | _| leading edge // | | _| leading edge
// | | // | |
// 800 +---------+ // 800 +---------+
...@@ -829,19 +951,36 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -829,19 +951,36 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
// to `trailingEdgeOffset`, the one on the right by setting it to // to `trailingEdgeOffset`, the one on the right by setting it to
// `leadingEdgeOffset`. // `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);
}
if (currentOffset > leadingEdgeOffset) { assert(targetOffset != null);
// `child` currently starts above the leading edge and can be shown fully
// on screen by scrolling down (which means: moving viewport up). if (duration == Duration.zero) {
offset.jumpTo(leadingEdgeOffset); offset.jumpTo(targetOffset.offset);
} else if (currentOffset < trailingEdgeOffset ) { } else {
// `child currently ends below the trailing edge and can be shown fully offset.animateTo(targetOffset.offset, duration: duration, curve: curve);
// on screen by scrolling up (which means: moving viewport down)
offset.jumpTo(trailingEdgeOffset);
} }
// else: `child` is between leading and trailing edge and hence already return targetOffset.rect;
// fully shown on screen. No action necessary.
} }
} }
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// The direction of a scroll, relative to the positive scroll offset axis given /// The direction of a scroll, relative to the positive scroll offset axis given
...@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier {
/// [jumpTo] applies the change immediately and notifies its listeners. /// [jumpTo] applies the change immediately and notifies its listeners.
void correctBy(double correction); 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. /// without animation, and without checking if the new value is in range.
/// ///
/// See also: /// See also:
...@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier {
/// and that defers the notification of its listeners until after layout. /// and that defers the notification of its listeners until after layout.
void jumpTo(double pixels); 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 direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewport.axisDirection]. /// the viewport's [RenderViewport.axisDirection].
/// ///
...@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset { ...@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset {
// Do nothing, viewport is fixed. // Do nothing, viewport is fixed.
} }
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) async => null;
@override @override
ScrollDirection get userScrollDirection => ScrollDirection.idle; ScrollDirection get userScrollDirection => ScrollDirection.idle;
} }
...@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null); 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) if (target == pixels)
return new Future<Null>.value(); return new Future<Null>.value();
...@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// animation, use [jumpTo]. /// animation, use [jumpTo].
/// ///
/// The animation is typically handled by an [DrivenScrollActivity]. /// The animation is typically handled by an [DrivenScrollActivity].
@override
Future<Null> animateTo(double to, { Future<Null> animateTo(double to, {
@required Duration duration, @required Duration duration,
@required Curve curve, @required Curve curve,
......
...@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
} }
Offset get _paintOffset { Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
assert(axisDirection != null); assert(axisDirection != null);
switch (axisDirection) { switch (axisDirection) {
case AxisDirection.up: 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: case AxisDirection.down:
return new Offset(0.0, -_offset.pixels); return new Offset(0.0, -position);
case AxisDirection.left: 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: case AxisDirection.right:
return new Offset(-_offset.pixels, 0.0); return new Offset(-position, 0.0);
} }
return null; return null;
} }
...@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
} }
@override @override
double getOffsetToReveal(RenderObject target, double alignment) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
rect ??= target.paintBounds;
if (target is! RenderBox) if (target is! RenderBox)
return offset.pixels; return new RevealedOffset(offset: offset.pixels, rect: rect);
final RenderBox targetBox = target; final RenderBox targetBox = target;
final Matrix4 transform = targetBox.getTransformTo(this); 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; final Size contentSize = child.size;
double leadingScrollOffset; double leadingScrollOffset;
...@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
break; 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 @override
void showOnScreen([RenderObject child]) { void showOnScreen({
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset); RenderObject descendant,
// Make sure the viewport itself is on screen. Rect rect,
super.showOnScreen(); 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 @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() { ...@@ -743,4 +743,127 @@ void main() {
debugDefaultTargetPlatformOverride = null; 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 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
...@@ -341,4 +342,487 @@ void main() { ...@@ -341,4 +342,487 @@ void main() {
semantics.dispose(); 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