Commit 7fea0593 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Scrollable2.ensureVisible for box-based viewports (#7868)

This patch makes Scrollable2.ensureVisible with SingleChildScrollView. A future
patch will extend the implementation to work with slivers. (Although the patch
does include some of the infrastructure for that part of the implementation as
well.)
parent 883bae07
...@@ -599,6 +599,11 @@ String _debugCompareFloats(String labelA, double valueA, String labelB, double v ...@@ -599,6 +599,11 @@ String _debugCompareFloats(String labelA, double valueA, String labelB, double v
// /// // ///
// /// The [paint] method is called with an [Offset] to the top-left corner of the // /// The [paint] method is called with an [Offset] to the top-left corner of the
// /// sliver, _regardless of the axis direction_. // /// sliver, _regardless of the axis direction_.
// ///
// /// ### childScrollOffset
// ///
// /// If the subclass positions children anywhere other than at scroll offset
// /// 0.0, you need to override [childScrollOffset]...
abstract class RenderSliver extends RenderObject { abstract class RenderSliver extends RenderObject {
// layout input // layout input
@override @override
...@@ -823,6 +828,11 @@ abstract class RenderSliver extends RenderObject { ...@@ -823,6 +828,11 @@ abstract class RenderSliver extends RenderObject {
/// This is used by [RenderSliverHelpers.hitTestBoxChild]. If you do not use /// This is used by [RenderSliverHelpers.hitTestBoxChild]. If you do not use
/// the [RenderSliverHelpers] mixin and do not call this method yourself, you /// the [RenderSliverHelpers] mixin and do not call this method yourself, you
/// do not need to implement this method. /// do not need to implement this method.
///
/// This method differs from [childScrollOffset] in that
/// [childMainAxisPosition] gives the distance from the leading _visible_ edge
/// of the sliver whereas [childScrollOffset] gives the distance from sliver's
/// zero scroll offset.
@protected @protected
double childMainAxisPosition(@checked RenderObject child) { double childMainAxisPosition(@checked RenderObject child) {
assert(() { assert(() {
...@@ -834,6 +844,19 @@ abstract class RenderSliver extends RenderObject { ...@@ -834,6 +844,19 @@ abstract class RenderSliver extends RenderObject {
@protected @protected
double childCrossAxisPosition(@checked RenderObject child) => 0.0; double childCrossAxisPosition(@checked RenderObject child) => 0.0;
/// Returns the scroll offset for the leading edge of the given child.
///
/// The `child` must be a child of this sliver.
///
/// This method differs from [childMainAxisPosition] in that
/// [childMainAxisPosition] gives the distance from the leading _visible_ edge
/// of the sliver whereas [childScrollOffset] gives the distance from sliver's
/// zero scroll offset.
double childScrollOffset(@checked RenderObject child) {
assert(child.parent == this);
return 0.0;
}
@override @override
void applyPaintTransform(RenderObject child, Matrix4 transform) { void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(() { assert(() {
...@@ -1068,6 +1091,19 @@ abstract class RenderSliverHelpers implements RenderSliver { ...@@ -1068,6 +1091,19 @@ abstract class RenderSliverHelpers implements RenderSliver {
typedef RenderSliver _Advancer(RenderSliver child); typedef RenderSliver _Advancer(RenderSliver child);
abstract class RenderAbstractViewport implements RenderObject {
static RenderAbstractViewport of(RenderObject object) {
while (object != null) {
if (object is RenderAbstractViewport)
return object;
object = object.parent;
}
return null;
}
double getOffsetToReveal(RenderObject descendant, double alignment);
}
// /// // ///
// /// See also: // /// See also:
// /// // ///
...@@ -1075,7 +1111,9 @@ typedef RenderSliver _Advancer(RenderSliver child); ...@@ -1075,7 +1111,9 @@ typedef RenderSliver _Advancer(RenderSliver child);
// /// - [RenderBox], which explains more about the Box protocol. // /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be // /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// /// placed inside a [RenderSliver] (the opposite of this class). // /// placed inside a [RenderSliver] (the opposite of this class).
abstract class RenderViewportBase2<ParentDataClass extends ContainerParentDataMixin<RenderSliver>> extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass> { abstract class RenderViewportBase2<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
implements RenderAbstractViewport {
RenderViewportBase2({ RenderViewportBase2({
AxisDirection axisDirection: AxisDirection.down, AxisDirection axisDirection: AxisDirection.down,
@required ViewportOffset offset, @required ViewportOffset offset,
...@@ -1282,6 +1320,12 @@ abstract class RenderViewportBase2<ParentDataClass extends ContainerParentDataMi ...@@ -1282,6 +1320,12 @@ abstract class RenderViewportBase2<ParentDataClass extends ContainerParentDataMi
return false; return false;
} }
@override
double getOffsetToReveal(RenderObject descendant, double alignment) {
// TODO(abath): Implement this function for sliver-based viewports.
return 0.0;
}
@protected @protected
Offset computeAbsolutePaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) { Offset computeAbsolutePaintOffset(RenderSliver child, double paintOffset, GrowthDirection growthDirection) {
assert(hasSize); // this is only usable once we have a size assert(hasSize); // this is only usable once we have a size
......
...@@ -61,9 +61,9 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -61,9 +61,9 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
// Find the last child that is at or before the scrollOffset. // Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild; RenderBox earliestUsefulChild = firstChild;
for (double earliestScrollOffset = offsetOf(earliestUsefulChild); for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset; earliestScrollOffset > scrollOffset;
earliestScrollOffset = offsetOf(earliestUsefulChild)) { earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// We have to add children before the earliestUsefulChild. // We have to add children before the earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true); earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) { if (earliestUsefulChild == null) {
...@@ -71,7 +71,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -71,7 +71,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
// We must inform our parent that this sliver cannot fulfill // We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction. // its contract and that we need a scroll offset correction.
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollOffsetCorrection: -offsetOf(firstChild), scrollOffsetCorrection: -childScrollOffset(firstChild),
); );
return; return;
} }
...@@ -90,7 +90,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -90,7 +90,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
// scroll offset. // scroll offset.
assert(earliestUsefulChild == firstChild); assert(earliestUsefulChild == firstChild);
assert(offsetOf(earliestUsefulChild) <= scrollOffset); assert(childScrollOffset(earliestUsefulChild) <= scrollOffset);
// Make sure we've laid out at least one child. // Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) { if (leadingChildWithLayout == null) {
...@@ -107,7 +107,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -107,7 +107,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
bool inLayoutRange = true; bool inLayoutRange = true;
RenderBox child = earliestUsefulChild; RenderBox child = earliestUsefulChild;
int index = indexOf(child); int index = indexOf(child);
double endScrollOffset = offsetOf(child) + paintExtentOf(child); double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() { // returns true if we advanced, false if we have no more children bool advance() { // returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication. // This function is used in two different places below, to avoid code duplication.
assert(child != null); assert(child != null);
...@@ -138,7 +138,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -138,7 +138,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData; final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = endScrollOffset; childParentData.scrollOffset = endScrollOffset;
assert(childParentData.index == index); assert(childParentData.index == index);
endScrollOffset = offsetOf(child) + paintExtentOf(child); endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true; return true;
} }
...@@ -151,7 +151,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -151,7 +151,7 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
// we want to make sure we keep the last child around so we know the end scroll offset // we want to make sure we keep the last child around so we know the end scroll offset
collectGarbage(leadingGarbage - 1, 0); collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild); assert(firstChild == lastChild);
final double extent = offsetOf(lastChild) + paintExtentOf(lastChild); final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollExtent: extent, scrollExtent: extent,
paintExtent: 0.0, paintExtent: 0.0,
...@@ -192,14 +192,14 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor { ...@@ -192,14 +192,14 @@ class RenderSliverBlock extends RenderSliverMultiBoxAdaptor {
constraints, constraints,
firstIndex: indexOf(firstChild), firstIndex: indexOf(firstChild),
lastIndex: indexOf(lastChild), lastIndex: indexOf(lastChild),
leadingScrollOffset: offsetOf(firstChild), leadingScrollOffset: childScrollOffset(firstChild),
trailingScrollOffset: endScrollOffset, trailingScrollOffset: endScrollOffset,
); );
assert(estimatedMaxScrollOffset >= endScrollOffset - offsetOf(firstChild)); assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
} }
final double paintedExtent = calculatePaintOffset( final double paintedExtent = calculatePaintOffset(
constraints, constraints,
from: offsetOf(firstChild), from: childScrollOffset(firstChild),
to: endScrollOffset, to: endScrollOffset,
); );
geometry = new SliverGeometry( geometry = new SliverGeometry(
......
...@@ -356,7 +356,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -356,7 +356,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
trailingScrollOffset = gridGeometry.scrollOffset; trailingScrollOffset = gridGeometry.scrollOffset;
} }
assert(offsetOf(firstChild) <= scrollOffset); assert(childScrollOffset(firstChild) <= scrollOffset);
if (trailingChildWithLayout == null) { if (trailingChildWithLayout == null) {
firstChild.layout(firstChildGridGeometry.getBoxConstraints(constraints)); firstChild.layout(firstChildGridGeometry.getBoxConstraints(constraints));
......
...@@ -66,7 +66,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -66,7 +66,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
trailingChildWithLayout ??= child; trailingChildWithLayout ??= child;
} }
assert(offsetOf(firstChild) <= scrollOffset); assert(childScrollOffset(firstChild) <= scrollOffset);
if (trailingChildWithLayout == null) { if (trailingChildWithLayout == null) {
firstChild.layout(childConstraints); firstChild.layout(childConstraints);
......
...@@ -268,15 +268,6 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -268,15 +268,6 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
return childParentData.index; return childParentData.index;
} }
/// Returns the scroll offset of the given child, as given by the
/// [SliverMultiBoxAdaptorParentData.scrollOffset] field of the child's [parentData].
double offsetOf(RenderBox child) {
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.scrollOffset != null);
return childParentData.scrollOffset;
}
/// Returns the dimension of the given child in the main axis, as given by the /// Returns the dimension of the given child in the main axis, as given by the
/// child's [RenderBox.size] property. This is only valid after layout. /// child's [RenderBox.size] property. This is only valid after layout.
@protected @protected
...@@ -305,7 +296,16 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -305,7 +296,16 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
@override @override
double childMainAxisPosition(RenderBox child) { double childMainAxisPosition(RenderBox child) {
return offsetOf(child) - constraints.scrollOffset; return childScrollOffset(child) - constraints.scrollOffset;
}
@override
double childScrollOffset(RenderObject child) {
assert(child != null);
assert(child.parent == this);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData.scrollOffset != null);
return childParentData.scrollOffset;
} }
@override @override
......
...@@ -280,6 +280,12 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R ...@@ -280,6 +280,12 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
return startPadding; return startPadding;
} }
@override
double childScrollOffset(RenderObject child) {
assert(child.parent == this);
return beforePadding;
}
@override @override
void applyPaintTransform(RenderObject child, Matrix4 transform) { void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null); assert(child != null);
......
...@@ -142,6 +142,28 @@ class ScrollPosition extends ViewportOffset { ...@@ -142,6 +142,28 @@ class ScrollPosition extends ViewportOffset {
double get pixels => _pixels; double get pixels => _pixels;
double _pixels = 0.0; double _pixels = 0.0;
Future<Null> ensureVisible(RenderObject object, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
}) {
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null);
final double to = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
if (to == pixels)
return new Future<Null>.value();
if (duration == Duration.ZERO) {
jumpTo(to);
return new Future<Null>.value();
}
return animate(to: to, duration: duration, curve: curve);
}
/// Animates the position from its current value to the given value `to`. /// Animates the position from its current value to the given value `to`.
/// ///
/// Any active animation is canceled. If the user is currently scrolling, that /// Any active animation is canceled. If the user is currently scrolling, that
......
...@@ -60,6 +60,39 @@ class Scrollable2 extends StatefulWidget { ...@@ -60,6 +60,39 @@ class Scrollable2 extends StatefulWidget {
if (physics != null) if (physics != null)
description.add('physics: $physics'); description.add('physics: $physics');
} }
/// The state from the closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// Scrollable2State scrollable = Scrollable2.of(context);
/// ```
static Scrollable2State of(BuildContext context) {
return context.ancestorStateOfType(const TypeMatcher<Scrollable2State>());
}
/// Scrolls the closest enclosing scrollable to make the given context visible.
static Future<Null> ensureVisible(BuildContext context, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
}) {
final List<Future<Null>> futures = <Future<Null>>[];
Scrollable2State scrollable = Scrollable2.of(context);
while (scrollable != null) {
futures.add(scrollable.position.ensureVisible(context.findRenderObject(), alignment: alignment));
context = scrollable.context;
scrollable = Scrollable2.of(context);
}
if (futures.isEmpty || duration == Duration.ZERO)
return new Future<Null>.value();
if (futures.length == 1)
return futures.first;
return Future.wait<Null>(futures);
}
} }
/// State object for a [Scrollable2] widget. /// State object for a [Scrollable2] widget.
......
...@@ -21,6 +21,7 @@ class SingleChildScrollView extends StatelessWidget { ...@@ -21,6 +21,7 @@ class SingleChildScrollView extends StatelessWidget {
SingleChildScrollView({ SingleChildScrollView({
Key key, Key key,
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.reverse: false,
this.padding, this.padding,
this.child, this.child,
}) : super(key: key) { }) : super(key: key) {
...@@ -29,6 +30,8 @@ class SingleChildScrollView extends StatelessWidget { ...@@ -29,6 +30,8 @@ class SingleChildScrollView extends StatelessWidget {
final Axis scrollDirection; final Axis scrollDirection;
final bool reverse;
final EdgeInsets padding; final EdgeInsets padding;
final Widget child; final Widget child;
...@@ -37,9 +40,9 @@ class SingleChildScrollView extends StatelessWidget { ...@@ -37,9 +40,9 @@ class SingleChildScrollView extends StatelessWidget {
// TODO(abarth): Consider reading direction. // TODO(abarth): Consider reading direction.
switch (scrollDirection) { switch (scrollDirection) {
case Axis.horizontal: case Axis.horizontal:
return AxisDirection.right; return reverse ? AxisDirection.left : AxisDirection.right;
case Axis.vertical: case Axis.vertical:
return AxisDirection.down; return reverse ? AxisDirection.up : AxisDirection.down;
} }
return null; return null;
} }
...@@ -94,7 +97,7 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget { ...@@ -94,7 +97,7 @@ class _SingleChildViewport extends SingleChildRenderObjectWidget {
} }
} }
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> { class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
_RenderSingleChildViewport({ _RenderSingleChildViewport({
AxisDirection axisDirection: AxisDirection.down, AxisDirection axisDirection: AxisDirection.down,
ViewportOffset offset, ViewportOffset offset,
...@@ -188,7 +191,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -188,7 +191,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
assert(hasSize); assert(hasSize);
if (child == null) if (child == null)
return 0.0; return 0.0;
return math.max(0.0, child.size.height - size.height); switch (axis) {
case Axis.horizontal:
return math.max(0.0, child.size.width - size.width);
case Axis.vertical:
return math.max(0.0, child.size.height - size.height);
}
return null;
} }
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
...@@ -247,15 +256,15 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -247,15 +256,15 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
} }
Offset get _scrollOffsetAsOffset { Offset get _paintOffset {
assert(axisDirection != null); assert(axisDirection != null);
switch (axisDirection) { switch (axisDirection) {
case AxisDirection.up: case AxisDirection.up:
return new Offset(0.0, _offset.pixels); return new Offset(0.0, _offset.pixels - child.size.height + size.height);
case AxisDirection.down: case AxisDirection.down:
return new Offset(0.0, -_offset.pixels); return new Offset(0.0, -_offset.pixels);
case AxisDirection.left: case AxisDirection.left:
return new Offset(_offset.pixels, 0.0); return new Offset(_offset.pixels - child.size.width + size.width, 0.0);
case AxisDirection.right: case AxisDirection.right:
return new Offset(-_offset.pixels, 0.0); return new Offset(-_offset.pixels, 0.0);
} }
...@@ -270,7 +279,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -270,7 +279,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
final Offset paintOffset = _scrollOffsetAsOffset; final Offset paintOffset = _paintOffset;
void paintContents(PaintingContext context, Offset offset) { void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset + paintOffset); context.paintChild(child, offset + paintOffset);
...@@ -286,13 +295,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -286,13 +295,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override @override
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset paintOffset = _scrollOffsetAsOffset; final Offset paintOffset = _paintOffset;
transform.translate(paintOffset.dx, paintOffset.dy); transform.translate(paintOffset.dx, paintOffset.dy);
} }
@override @override
Rect describeApproximatePaintClip(RenderObject child) { Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtPaintOffset(_scrollOffsetAsOffset)) if (child != null && _shouldClipAtPaintOffset(_paintOffset))
return Point.origin & size; return Point.origin & size;
return null; return null;
} }
...@@ -300,9 +309,51 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -300,9 +309,51 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override @override
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) { if (child != null) {
final Point transformed = position + -_scrollOffsetAsOffset; final Point transformed = position + -_paintOffset;
return child.hitTest(result, position: transformed); return child.hitTest(result, position: transformed);
} }
return false; return false;
} }
@override
double getOffsetToReveal(RenderObject descendant, double alignment) {
if (descendant is! RenderBox)
return offset.pixels;
final RenderBox target = descendant;
final Matrix4 transform = target.getTransformTo(this);
final Rect bounds = MatrixUtils.transformRect(transform, target.paintBounds);
final Size contentSize = child.size;
double leading;
double trailing;
double viewportExtent;
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
viewportExtent = size.height;
leading = contentSize.height - bounds.bottom;
trailing = contentSize.height - bounds.top;
break;
case AxisDirection.right:
viewportExtent = size.width;
leading = bounds.left;
trailing = bounds.right;
break;
case AxisDirection.down:
viewportExtent = size.height;
leading = bounds.top;
trailing = bounds.bottom;
break;
case AxisDirection.left:
viewportExtent = size.width;
leading = contentSize.width - bounds.right;
trailing = contentSize.width - bounds.left;
break;
}
final double targetExtent = trailing - leading;
return leading - (viewportExtent - targetExtent) * alignment;
}
} }
// Copyright 2017 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:math' as math;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
Finder findKey(int i) => find.byKey(new ValueKey<int>(i));
Widget buildSingleChildScrollView(Axis scrollDirection, { bool reverse: false }) {
return new Center(
child: new SizedBox(
width: 600.0,
height: 400.0,
child: new SingleChildScrollView(
scrollDirection: scrollDirection,
reverse: reverse,
child: new BlockBody(
mainAxis: scrollDirection,
children: <Widget>[
new Container(key: new ValueKey<int>(0), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(1), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(2), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(3), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(4), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(5), width: 200.0, height: 200.0),
new Container(key: new ValueKey<int>(6), width: 200.0, height: 200.0),
],
),
),
),
);
}
void main() {
testWidgets('SingleChildScollView ensureVisible Axis.vertical', (WidgetTester tester) async {
BuildContext findContext(int i) => tester.element(findKey(i));
await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical));
Scrollable2.ensureVisible(findContext(3));
await tester.pump();
expect(tester.getTopLeft(findKey(3)).y, equals(100.0));
Scrollable2.ensureVisible(findContext(6));
await tester.pump();
expect(tester.getTopLeft(findKey(6)).y, equals(300.0));
Scrollable2.ensureVisible(findContext(4), alignment: 1.0);
await tester.pump();
expect(tester.getBottomRight(findKey(4)).y, equals(500.0));
Scrollable2.ensureVisible(findContext(0), alignment: 1.0);
await tester.pump();
expect(tester.getTopLeft(findKey(0)).y, equals(100.0));
Scrollable2.ensureVisible(findContext(3), duration: const Duration(seconds: 1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1020));
expect(tester.getTopLeft(findKey(3)).y, equals(100.0));
});
testWidgets('SingleChildScollView ensureVisible Axis.horizontal', (WidgetTester tester) async {
BuildContext findContext(int i) => tester.element(findKey(i));
await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal));
Scrollable2.ensureVisible(findContext(3));
await tester.pump();
expect(tester.getTopLeft(findKey(3)).x, equals(100.0));
Scrollable2.ensureVisible(findContext(6));
await tester.pump();
expect(tester.getTopLeft(findKey(6)).x, equals(500.0));
Scrollable2.ensureVisible(findContext(4), alignment: 1.0);
await tester.pump();
expect(tester.getBottomRight(findKey(4)).x, equals(700.0));
Scrollable2.ensureVisible(findContext(0), alignment: 1.0);
await tester.pump();
expect(tester.getTopLeft(findKey(0)).x, equals(100.0));
Scrollable2.ensureVisible(findContext(3), duration: const Duration(seconds: 1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1020));
expect(tester.getTopLeft(findKey(3)).x, equals(100.0));
});
testWidgets('SingleChildScollView ensureVisible Axis.vertical reverse', (WidgetTester tester) async {
BuildContext findContext(int i) => tester.element(findKey(i));
await tester.pumpWidget(buildSingleChildScrollView(Axis.vertical, reverse: true));
Scrollable2.ensureVisible(findContext(3));
await tester.pump();
expect(tester.getBottomRight(findKey(3)).y, equals(500.0));
Scrollable2.ensureVisible(findContext(0));
await tester.pump();
expect(tester.getBottomRight(findKey(0)).y, equals(300.0));
Scrollable2.ensureVisible(findContext(2), alignment: 1.0);
await tester.pump();
expect(tester.getTopLeft(findKey(2)).y, equals(100.0));
Scrollable2.ensureVisible(findContext(6), alignment: 1.0);
await tester.pump();
expect(tester.getBottomRight(findKey(6)).y, equals(500.0));
Scrollable2.ensureVisible(findContext(3), duration: const Duration(seconds: 1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1020));
expect(tester.getBottomRight(findKey(3)).y, equals(500.0));
});
testWidgets('SingleChildScollView ensureVisible Axis.horizontal', (WidgetTester tester) async {
BuildContext findContext(int i) => tester.element(findKey(i));
await tester.pumpWidget(buildSingleChildScrollView(Axis.horizontal, reverse: true));
Scrollable2.ensureVisible(findContext(3));
await tester.pump();
expect(tester.getBottomRight(findKey(3)).x, equals(700.0));
Scrollable2.ensureVisible(findContext(0));
await tester.pump();
expect(tester.getBottomRight(findKey(0)).x, equals(300.0));
Scrollable2.ensureVisible(findContext(2), alignment: 1.0);
await tester.pump();
expect(tester.getTopLeft(findKey(2)).x, equals(100.0));
Scrollable2.ensureVisible(findContext(6), alignment: 1.0);
await tester.pump();
expect(tester.getBottomRight(findKey(6)).x, equals(700.0));
Scrollable2.ensureVisible(findContext(3), duration: const Duration(seconds: 1));
await tester.pump();
await tester.pump(const Duration(milliseconds: 1020));
expect(tester.getBottomRight(findKey(3)).x, equals(700.0));
});
testWidgets('SingleChildScollView ensureVisible rotated child', (WidgetTester tester) async {
BuildContext findContext(int i) => tester.element(findKey(i));
await tester.pumpWidget(
new Center(
child: new SizedBox(
width: 600.0,
height: 400.0,
child: new SingleChildScrollView(
child: new BlockBody(
children: <Widget>[
new Container(height: 200.0),
new Container(height: 200.0),
new Container(height: 200.0),
new Container(
height: 200.0,
child: new Center(
child: new Transform(
transform: new Matrix4.rotationZ(math.PI),
child: new Container(
key: new ValueKey<int>(0),
width: 100.0,
height: 100.0,
decoration: const BoxDecoration(
backgroundColor: const Color(0xFFFFFFFF),
),
),
),
),
),
new Container(height: 200.0),
new Container(height: 200.0),
new Container(height: 200.0),
],
),
),
),
)
);
Scrollable2.ensureVisible(findContext(0));
await tester.pump();
expect(tester.getBottomRight(findKey(0)).y, closeTo(100.0, 0.1));
Scrollable2.ensureVisible(findContext(0), alignment: 1.0);
await tester.pump();
expect(tester.getTopLeft(findKey(0)).y, closeTo(500.0, 0.1));
});
}
...@@ -68,6 +68,15 @@ class TestScrollPosition extends ScrollPosition { ...@@ -68,6 +68,15 @@ class TestScrollPosition extends ScrollPosition {
); );
} }
} }
@override
Future<Null> ensureVisible(RenderObject object, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
}) {
return new Future<Null>.value();
}
} }
class TestScrollPhysics extends ScrollPhysics { class TestScrollPhysics extends ScrollPhysics {
......
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